zoukankan      html  css  js  c++  java
  • 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个线程访问的是实例的不同的方法,依然不能同时访问。因为决定是否可以同时访问的不再是门,而是锁。只要使用的是相同的对象锁,就会互斥访问

    上文中关于门的比喻已经不合适了,因为在代码中你可以发现两个门(do1、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 }

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

     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使用的锁总是变化的。所以我们再补充一点:只有使用相同的对象锁,才能互斥访问。所以识别所使用的锁,是很重要的。

     
    下面再看一段代码:
     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的几种方式,以及相互的区别,写的很好,建议也看一下,相互印证。
  • 相关阅读:
    企业生产环境不同业务linux系统分区方案
    linux 文件 s 权限
    shell中的命令与特殊符号
    Linux数组基础
    shell脚本学习(1)
    文件的压缩与打包
    Linux 磁盘管理基础命令df,du,fdisk,mke2fs
    mkpasswd的使用
    P1080 国王游戏
    P1315 观光公交
  • 原文地址:https://www.cnblogs.com/ywjy/p/5650141.html
Copyright © 2011-2022 走看看