Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 298|回复: 0

[Java线程学习]是同步方法还是用 synchronized 代码?

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-11-2 23:59:17 | 显示全部楼层 |阅读模式
    熟悉 java 的多线程的一般都知道会有数据不一致的情况发生,比如两个线程在操作同一个类变量时,而保护数据不至于错乱的办法就是让方法同步或者代码块同步。同步时非原子操作就得同步,比如一个简单的 1.2+1 运算也该同步,以保证一个代码块或方法成为一个原子操作。

         简单点说就是给在多线程环境中可能会造成数据破坏的方法做同步,做法有两种,以及一些疑问:

    1. 不论是静态的或非静态的方法都加上 synchronized 关键字,那静态的方法和非静态的方法前加上 synchronized 关键字有区别吗?

    2. 或者在可疑的代码块两旁用 synchronized(this) 或 synchronized(someObject) 包裹起来,而选用 this 还是某一个对象--someObject,又有什么不同呢?  
      
       
       
         
       

         
       
      

    3. 对方法加了 synchronized 关键字或用 synchronized(xxx) 包裹了代码,就一定能避免多线程环境下的数据破坏吗?

    4. 对方法加 synchronized 关键字与用 synchronized(xxx) 同步代码块两种规避方法又有什么分别和联系呢?

    为了理解上面的问题,我们还得从 Java 对线程同步的原理上说起。我们知道 Java 直接在语言级上支持多线程的。在多线程环境中我们要小心的数据是:

    1) 保存在堆中的实例变量
    2) 保存在方法区中的类变量。

    现实点说呢就是某个方法会触及到的同一个变量,如类变量或单态实例的实例变量。避免冲突的最容易想到的办法就是同一时刻只让一个线程去执行某段代码块或方法,于是我们就要给一段代码块或整个方法体标记出来,被保护的代码块或方法体在 Java 里叫做监视区域(Monitor Region),类似的东西在 C++ 中叫做临界区(Critical Section)。

    比如说一段代码:



              public void operate() {
                    flag ++;
                    try {
                            //休眠一个随机时间,让不同线程能在此交替执行
                            Thread.sleep(new Random().nextInt(10));
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                    }
                    flag --;
                    System.out.println("Current flag: " + flag);
            }
      
    用 synchronized 标记起来的话,可以写成:
      

      

              public void operate() {
                    synchronized(this){//只需要把可能造成麻烦的代码标记起来
                            flag ++;
                            try {
                                    //休眠一个随机时间,让不同线程能在此交替执行
                                    Thread.sleep(new Random().nextInt(5));
                            } catch (InterruptedException e) {
                                    e.printStackTrace();
                            }
                            flag --;
                           
                            System.out.println("Current flag: " + flag);
                    }
                   
                    //some code out of the monitor region
                    System.out.println("线程安全的代码放外面就行啦");
                   
            }
      
    那如果我们悲观,或许是偷点懒,直接给方法加个 synchronized 关键字就行,就是这样:
      

      

              public synchronized void operate() {
                    flag ++;
                    try {
                            //休眠一个随机时间,让不同线程能在此交替执行
                            Thread.sleep(new Random().nextInt(10));
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                    }
                    flag --;
                    System.out.println("Current flag: " + flag);
            }
      
    给方法加个关键字 synchronized 其实就是相当于把方法中的所有代码行框到了 synchronized(xxx) 块中。同步肯定会影响到效率,这也是大家知道的,因为它会造成方法调用的等待。方法中有些代码可能是线程安全的,所以可不用包裹在 synchronized(xxx) 中。
      

      
    那么
      只要给方法加上关键字 synchronized,或者 synchronized(this) 括起一段代码一定就是线程安全的吗?现在来看个例子,比如类 TestMultiThread:
      
      
      

      package com.unmi;
    import java.util.Random;
    /**
    * 多线程测试程序
    *
    * @author Unmi
    */
    public class TestMultiThread {
            // 一个标志值
            private static int flag = 1;
            /**
             * @param args
             */
            public static void main(String[] args) {
                    new Thread("Thread-01") {
                            public void run() {
                                    new TestMultiThread().operate();
                            }
                    }.start(); // 启动第一个线程
                    new Thread("Thread-02") {
                            public void run() {
                                    new TestMultiThread().operate();
                            }
                    }.start(); // 启动第二个线程
            }
            /**
             * 对 flag 进行一个自增,然后自减的操作,正常情况下 flag 还应是 1
             */
            public void operate() {
                    flag++;
                    try {
                            // 增加随机性,让不同线程能在此交替执行
                            Thread.sleep(new Random().nextInt(5));
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                    }
                    flag--;
                    System.out.println("Thread: " + Thread.currentThread().getName()
                                    + " /Current flag: " + flag);
            }
    }
      
    有一个静态变量 flag = 1,还有一个实例方法 operate() 方法,对 flag 进行 flag ++,然后 flag -- 操作,最后输出当前的 flag 值,理想情况下,输出的 flag 应该仍然是 1。可实际上是两个线程执行行的输出很大的机会得到:
      

      
    Thread: Thread-01 /Current flag: 2
      
    Thread: Thread-02 /Current flag: 1
      

      
    好,我们也知道那是因为线程在对 flag 操作不同步引起的,对照代码来理解就是:
      

      
    当线程 Thread-01 执行到 flag ++ 后,此时 flag 等于 2,有个 sleep,能使得 Thread-01 稍事休息
      
    此时线程 Thread-02 进入方法 operate,并相执行 flag ++,即当前的 2 ++,flag 为 3 了,碰到 sleep 也停顿一下
      
    Thread-01 又再执行剩下的 flag --,在当前的 flag 为 3 基础上进行 flag --,最后输出 Thread: Thread-01 /Current flag: 2
      
    Thread-02 接着执行 flag --,当前 flag 为 2,flag -- 后输出就是 Thread: Thread-02 /Current flag: 1
      

      
    注:在 flag++ 与 flag -- 之前加个随机的 sleep 是为了模拟有些环境,比如某个线程执行快,另一个线程执行慢的可能性,多执行几遍,你也能看到另外几种输出:
      

      
    Thread: Thread-02 /Current flag: 2
      
    Thread: Thread-01 /Current flag: 1
      

      

      

      
    Thread: Thread-02 /Current flag: 1
      
    Thread: Thread-01 /Current flag: 1
      

      

      

      
    Thread: Thread-01 /Current flag: 1
      
    Thread: Thread-02 /Current flag: 1
      

      
    出现不同状况的可能性都好理解。为确保 flag 的完整性,于是加上 synchronized(this) 把代码 flag ++ 和 flag -- 代码块同步了,最后的 operate() 方法的代码如下:
      

      

              public void operate() {
                    synchronized(this){//只需要把可能制造麻烦的代码标记起来
                            flag ++;
                            try {
                                    //增加随机性,让不同线程能在此交替执行
                                    Thread.sleep(new Random().nextInt(5));
                            } catch (InterruptedException e) {
                                    e.printStackTrace();
                            }
                            flag --;
                           
                            System.out.println("Thread: "+ Thread.currentThread().getName() +
                                            " /Current flag: " + flag);
                    }
                   
                    //some code out of the monitor region
                    System.out.print("");
            }
      
    再次执行上面的测试代码,仍然会看到如下的输出:
      

      
    Thread: Thread-01 /Current flag: 2
      
    Thread: Thread-02 /Current flag: 1
      

      
    而不是我们所期盼的两次输出 flag 值都应为 1 的结果。难道 synchronized 也灵验了,非也,玄机就在 synchronized() 中的那个对象的选取上,我们用 this 在这里不可行。
      

      
    现在来解析跟在 synchronized 后面的那个对象参数。在 JVM 中,每个对象和类(其实是类本身的实例) 在逻辑上都是和一个监视器相关联的,监视器指的就是被同步的方法或代码块。这句话不好理解,主谓调换一下再加上另外几条规则:
      

      
    1) Java 程序中每一个监视区域都和一个对象引用相关联,譬如 synchronized(this)  中的 this 对象。
      

      
    2) 线程在进入监视区域前必须对相关联的对象进行加锁,退出监视区域后释放该锁。
      

      
    3) 不同线程在进入同一监视区域不能对关联对象加锁多次。意即 A 线程在进入 M 监视区域时,获得了关联对象 O 的锁,在未释放该锁之前,另一线程 B 无法获得 M 监视区域的对象锁,此时就要等待 A 线程释放锁。但是 A 线程可能对 O 加锁多次(递归调用就可能出现这种情况)。
      

      
    4) 线程只能获得了监视区域相关联的对象锁,才能执行监视区域内的代码,否则等待。JVM 维护了一个监视区域相关联的对象锁的计数,比如 A 线程对监视区域 M 相关联的 O 对象加锁了 N 次,计数则为一,要等锁全部释放了,计数即为零,此时另一线程 B 才能获得该对象锁。
      

      
    好了,明白了线程,监视区域,相关联对象,对象锁的关系之后,我们就可以理解上面的程序为何加了 synchronized(this)  后还是未能如我们所愿呢?
      

      
    监视区域与 this 对象相关联的
      
    线程 Thread-01 进入监视区域时,对此时的 this 对象加锁,也就是获得了 this 对象锁。因为代码中有意加了个 sleep 语句,所以还不会立即释放该锁
      
    这时候线程 Thread-02 要求进入同一监视区域,也试图获得此时的 this 对象锁,并执行其中的代码
      

      
    从执行的结果,或者可进行断点调试,你会发现,尽管 Thread-01 获得了 this 对象锁后,还未释放该锁时,另一线程 Thread-02 也可轻而易举的获得 this 对象锁,并同时执行监视区域中的代码。
      

      
    前面不是说过,某一线程对监视区域相关联对象加锁上后,另一线程将不能同时对该对象加锁,必须等待其他线程释放该对象锁才行吗?这句话千真万确,原因就在于此 this 非彼 this,也就是 this 指代的对象一直在变。Thread-01 进入监视区域是对 this 代表的 new TestMultiThread() 对象,即使你没有释放该锁,Thread-02 在进入同一监视区域时当然还能对 this 代表的另一 new TestMultiThread() 对象加锁的。
      

      
    所以说这里机械的框上 synchronized(this) 其实起不到任何效果,正确的做法,可以写成
      

      
    synchronized(TestMultiThread.class){...};  //TestMultiThread 类实例在同一个 JVM 中指的就是同一个对象(不同 ClassLoader 时不考虑)
      

      
    或者预先在 TestMultiThread 中声明一个静态变量,如 private static Object object = new Ojbect();,然后 synchronized 部分写成
      

      
    synchronized(object){...}
      

      
    然后再执行前面的测试代码,保管每回执行后,输出的两次 flag 的值都为 1。
      

      
    又有人会有疑问了,难道就不能用 synchronized(this) 这样的写法了吗?这种写法也没少见啊,不能说人家总是错的吧。在有些时候,能确保每一次 this 会指向到与前面相同的对象时都不会有问题的,如单态类的 this。
      

      
    到这里,前面的第二次疑问也同时得到解决了,答案是不一定,看关联对象是否同一个,有时候应分析实际的运行环境。
      

      
    参考:1.
      The Java Virtual Machine Specification
      
             2.
      Inside the Java Virtual Machine  (by Bill Venners)
      
             3.
      Books Related to the JVM
      

      
      
       
       

         
       

         
       
      
    复制代码
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-25 15:59 , Processed in 0.308318 second(s), 36 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表