Java synchronized的原理解析

开始


 

类有一个特性叫封装,若是一个类,全部的field都是private的,并且没有任何的method,那么这个类就像是四面围墙+天罗地网,没有门。看起来就是一个封闭的箱子,外面的进不来,里面的出不去,通常来讲,这样的类是没用的。多线程


如今为这个类定义一个public的method,这个method可以修改这个类的field,至关于为这个箱子开了一个门。门有了,而后访问者就有了,当一个时间段,有多个访问者进来,就可能会发生并发问题。
 
并发问题是个什么问题?最经典的例子就是转帐,一个访问者从帐户A扣取一部分金额,加到帐户B上。在A帐户扣取以后,B帐户转入以前,数据处于不一致的状态,另外一个访问者若是在这个时候访问B帐户,获取的数据就是有问题的。这就是并发问题,致使这个问题的出现基于2个条件:1.访问者的操做致使数据在一段时间内是不一致的;2.能够有多个访问者同时操做。若是可以破坏其中一个条件,就能够解决并发问题了。咱们的关注点是在第2个条件上。
 
回到那个箱子,回到那个门。咱们设想为这个门加一把锁,一个访问者进了这个门,就上锁,期间其余访问者不能再进来;等进去的访问者出来,锁打开,容许另外一个访问者进去。

1. 给一个代码块上锁

synchronized能够上锁、解锁。可是它自己并非锁,它使用的锁来自于一个对象: 任何对象实例都有一把内部锁,只有一把synchronized不只仅能够对整个method上锁,还能够对method内的某个代码块上锁。
好比下面这种用法:
synchronized(obj){
    // some code...
}

这个用法就是使用了obj的锁,来锁定一个代码块。并发

对整个方法上锁,如:
1 publicsynchronizedvoid aMethod(){
2     // some code...
3 }

这个时候它使用的是当前实例this的锁,至关于下面的模式:性能

publicvoid aMethod(){
    synchronized(this){
        // some code...
    }
}

2. 两个代码块的互斥

一个代码块,被上了锁,就没法同时接纳多个线程的访问。若是是2个不一样的代码块,都被上了锁,它们之间是否会有影响呢?请看下面的代码:
 1 class SyncData {
 2     public void do1() {
 3         synchronized(this) {
 4             for (int i=0; i < 4; i++) {
 5                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 6                 try{
 7                     Thread.sleep(1000);
 8                 }catch(InterruptedException e) {
 9                     e.printStackTrace();
10                 }
11             }
12         }
13         
14     }
15     
16     public void do2() {
17         synchronized(this) {
18             for (int i=0; i < 4; i++) {
19                 System.out.println(Thread.currentThread().getName() + "-do2-" + i);
20                 try{
21                     Thread.sleep(1000);
22                 }catch(InterruptedException e) {
23                     e.printStackTrace();
24                 }
25             }
26         }
27     }
28 }

建立1个SyncData的实例,开启2个线程,一个线程调用实例的do1方法,另外一个线程调用实例的do2方法,你会看到他们之间是互斥的——即便2个线程访问的是实例的不一样的方法,依然不能同时访问。由于决定是否能够同时访问的再也不是门,而是锁。只要使用的是相同的对象锁,就会互斥访问this

上文中关于门的比喻已经不合适了,由于在代码中你能够发现两个门(do一、do2)使用了同一把锁(this),而这和咱们的常识经验是相违背的,下文也不会再出现“门”。

3. 锁的识别

可使用任何对象的锁,好比你能够专门建立一个对象,只提供锁的功能:
 1 class SyncData {
 2     private Object lock = new byte[0];
 3     
 4     public void do1() {
 5         synchronized(lock) {
 6             for (int i=0; i < 4; i++) {
 7                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 8                 try{
 9                     Thread.sleep(1000);
10                 }catch(InterruptedException e) {
11                     e.printStackTrace();
12                 }
13             }
14         }
15     }
16 }

思考下面的代码是否能起到互斥访问的做用:spa

 1 class SyncData {
 2     public void do1() {
 3         Object lock = new byte[0];
 4         synchronized(lock) {
 5             for (int i=0; i < 4; i++) {
 6                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 7                 try{
 8                     Thread.sleep(1000);
 9                 }catch(InterruptedException e) {
10                     e.printStackTrace();
11                 }
12             }
13         }
14     }
15 }

这个是不能起到互斥做用的,由于每一次调用,局部变量lock都是不一样的实例。也就是说,synchronized使用的锁老是变化的。因此咱们再补充一点:只有使用相同的对象锁,才能互斥访问。因此识别所使用的锁,是很重要的。.net

 
下面再看一段代码:
 1 class SyncData {
 2     public void do1() {
 3         synchronized(this) {
 4             for (int i=0; i < 4; i++) {
 5                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 6                 try{
 7                     Thread.sleep(1000);
 8                 }catch(InterruptedException e) {
 9                     e.printStackTrace();
10                 }
11             }
12         }
13         
14     }
15 }

建立2个实例,分别交给2个线程中的1个去访问,能互斥吗?线程

不能够,由于每个实例使用的都是自身的锁,相互之间是不一样的锁,因此不能互斥。若是把代码改为这样呢:
class SyncData {
    public void do1() {
        synchronized(this.getClass()) {
            for (int i=0; i < 4; i++) {
                System.out.println(Thread.currentThread().getName() + "-do1-" + i);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
    }
}

能够互斥,无论一个类有多少个实例,它们调用getClass()返回的结果都是同一个实例。设计

讨论这个问题,是由于能够在static的method上使用synchronized,而其本质,就是使用了上面那种实例的锁,因此不一样的synchronized static方法之间,也是互斥的。

总结


总结一下咱们的结论:
  1. 任何对象实例都有一把内部锁,只有一把。
  2. 相同的对象锁是互斥访问的充要条件。
这2个结论已经够了,重要的是识别使用的对象的锁是否是相同的。
 
多线程设计,考虑同步问题,我有几点想法:
  1. 一个类的实例,可能被多个线程并发访问,才考虑同步控制。
  2. 在1的前提下,只有会致使数据状态出现一段时间的不一致,相关的代码片断才须要同步控制。
  3. 在2的前提下,只有两块代码会相互干扰时,才必须使用同一把对象锁,来实现互斥;若是相互之间没有影响,建议使用不一样的对象锁,以保持并发性能。
固然,在判断“数据状态是否会不一致”、“两块代码是否有干扰”的时候,是比较困难的,因此再补充2点:
  1. 在不能确认数据状态是否会不一致的状况下,按照会不一致的状况考虑
  2. 在不能确认两块代码是否有干扰的状况下,按照会有干扰的状况考虑
咱们的讨论到此结束。
 

参考


  1. Java中Synchronized的用法介绍了使用synchronized的几种方式,以及相互的区别,写的很好,建议也看一下,相互印证。
相关文章
相关标签/搜索