龙空技术网

Java同步关键字synchronized详解

Java二师兄 185

前言:

今天看官们对“java static synchronized”大体比较看重,你们都想要知道一些“java static synchronized”的相关内容。那么小编同时在网上收集了一些对于“java static synchronized””的相关内容,希望看官们能喜欢,小伙伴们快快来学习一下吧!

前言

多线程编程可以极大地提高了效率,但也会带来线程安全问题。比如说多个线程向数据库插入数据,就可能会导致数据库中数据重复。

什么时候会引发线程安全问题

首先我需要了解什么是临界资源?有这样一种资源,在某一时刻只能被一个线程所使用,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等。

举个简单的例子:

现在有两个线程分别从网络上读取数据,然后插入一张数据库表中,要求不能插入重复的数据。那么必然在插入数据的过程中存在两个操作:

1)检查数据库中是否存在该条数据;

2)如果存在,则不插入;如果不存在,则插入到数据库中。假如两个线程分别用thread-a和thread-b表示,某一时刻,thread-a和thread-b都读取到了数据X,那么可能会发生这种情况:thread-a去检查数据库中是否存在数据X,然后thread-b也接着去检查数据库中是否存在数据X,如果数据x不存在,就会导致重复插入。

如何解决线程安全问题

基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问,即通过加锁实现

synchronized同步方法或者同步块

在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁,顾名思义:能到达到互斥访问目的的锁。举个简单的例子:如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。 在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

下面通过几个简单的例子来说明synchronized关键字的使用:

1.synchronized方法

下面这段代码中两个线程分别调用insertData对象插入数据:

 1public class Test {  2 public static void main(String[] args) {  3 final InsertData insertData = new InsertData();  4 new Thread() {  5 public void run() {  6 insertData.insert(Thread.currentThread());  7 };  8 }.start();  9 new Thread() { 10 public void run() { 11 insertData.insert(Thread.currentThread()); 12 }; 13 }.start(); 14 } 15} 16class InsertData { 17 private ArrayList<Integer> arrayList = new ArrayList<Integer>(); 18 public void insert(Thread thread){ 19 for(int i=0;i<5;i++){ 20 System.out.println(thread.getName()+"在插入数据"+i); 21 arrayList.add(i); 22 } 23 }24 }

输出结果

说明两个线程在同时执行insert方法。而如果在insert方法前面加上关键字synchronized的话,运行结果为:

1class InsertData { 2 private ArrayList<Integer> arrayList = new ArrayList<Integer>(); 3 public synchronized void insert(Thread thread){ 4 for(int i=0;i<5;i++){ 5 System.out.println(thread.getName()+"在插入数据"+i); 6 arrayList.add(i); 7 } 8 }9}

从上输出结果说明,Thread-1插入数据是等Thread-0插入完数据之后才进行的。说明Thread-0和Thread-1是顺序执行insert方法的。

这就是synchronized方法。

不过有几点需要注意:

1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,

3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

 2.synchronized代码块

synchronized代码块类似于以下这种形式:

1synchronized(synObject) {2 // 代码逻辑3}

当在某个线程中执行这段代码块,该线程会获取对象synObject的锁,从而使得其他线程无法同时访问该代码块。synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。

比如上面的insert方法可以改成以下两种形式:

 1class InsertData { 2 private ArrayList<Integer> arrayList = new ArrayList<Integer>();  3 public void insert(Thread thread){  4 synchronized (this){  5 for(int i=0;i<100;i++){  6 System.out.println(thread.getName()+"在插入数据"+i);  7 arrayList.add(i);  8 }  9 } 10 }11}1213class InsertData { 14 private ArrayList<Integer> arrayList = new ArrayList<Integer>();15 private Object object = new Object(); 16 public void insert(Thread thread){ 17 synchronized (object){ 18 for(int i=0;i<100;i++){ 19 System.out.println(thread.getName()+"在插入数据"+i); 20 arrayList.add(i); 21 } 22 } 23 }24}

从上面可以看出,synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。

另外,每个类也会有一个锁,它可以用来控制对static数据成员的并发访问。并且如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

看下面这段代码就明白了:

 1public class Test {  2 public static void main(String[] args) {  3 final InsertData insertData = new InsertData();  4 new Thread(){  5 @Override  6 public void run() {  7 insertData.insert();  8 }  9 }.start(); 10 new Thread(){ 11 @Override 12 public void run() { 13 insertData.insert1(); 14 } 15 }.start(); 16 } 17} 18class InsertData{ 19 public synchronized void insert(){ 20 System.out.println("执行insert"); 21 try { 22 Thread.sleep(5000); 23 } catch (InterruptedException e){ 24 e.printStackTrace(); 25 } 26 System.out.println("执行insert完毕"); 27 } 28 public synchronized static void insert1() { 29 System.out.println("执行insert1"); 30 System.out.println("执行insert1完毕"); 31 }32 }

输出结果

 第一个线程里面执行的是insert方法,不会导致第二个线程执行insert1方法发生阻塞现象。

 下面我们看一下synchronized关键字到底做了什么事情,我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为: 1public class InsertData {  2 private Object object = new Object();  3 public void insert(Thread thread){  4 synchronized (object) {  5 }  6 }  7 public synchronized void insert1(Thread thread){ 8 9 } 10 public void insert2(Thread thread){ 1112 }13}

从反编译获得的字节码可以看出,synchronized代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。对于synchronized方法,执行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

有一点要注意:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

标签: #java static synchronized