当前位置 博文首页 > YSOcean:Java关键字(八)——synchronized

    YSOcean:Java关键字(八)——synchronized

    作者:YSOcean 时间:2021-05-24 18:26

    Java关键字(八)——synchronized

      synchronized 这个关键字,我相信对于并发编程有一定了解的人,一定会特别熟悉,对于一些可能在多线程环境下可能会有并发问题的代码,或者方法,直接加上synchronized,问题就搞定了。

      但是用归用,你明白它为什么要这么用?为什么就能解决我们所说的线程安全问题?

      下面,可乐将和大家一起深入的探讨这个关键字用法。

    1、示例代码结果?

      首先大家看一段代码,大家想想最后的打印count结果是多少?

     1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
     2 
     3 
     4 /**
     5  * Create by ItCoke
     6  */
     7 public class SynchronizedTest implements Runnable{
     8 
     9     public static int count = 0;
    10 
    11     @Override
    12     public void run() {
    13         addCount();
    14 
    15     }
    16 
    17     public void addCount(){
    18         int i = 0;
    19         while (i++ < 100000) {
    20             count++;
    21         }
    22     }
    23 
    24     public static void main(String[] args) throws Exception{
    25         SynchronizedTest obj = new SynchronizedTest();
    26         Thread t1 = new Thread(obj);
    27         Thread t2 = new Thread(obj);
    28         t1.start();
    29         t2.start();
    30         t1.join();
    31         t2.join();
    32         System.out.println(count);
    33 
    34     }
    35 
    36 
    37 }

      代码很简单,主线程中启动两个线程t1和t2,分别调用 addCount() 方法,将count的值都加100000,然后调用 join() 方法,表示主线程等待这两个线程执行完毕。最后打印 count 的值。

      应该没有答案一定是 200000 的同学吧,很好,大家都具备一定的并发知识。

      这题的答案是一定小于等于 200000,至于原因也很好分析,比如 t1线程获取count的值为0,然后执行了加1操作,但是还没来得及同步到主内存,这时候t2线程去获取主内存的count值,发现还是0,然后继续自己的加1操作。也就是t1和t2都执行了加1操作,但是最后count的值依然是1。

      那么我们应该如何保证结果一定是 200000呢?答案就是用 synchronized。

    2、修饰代码块

      直接上代码:

     1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
     2 
     3 
     4 /**
     5  * Create by ItCoke
     6  */
     7 public class SynchronizedTest implements Runnable{
     8 
     9     public static int count = 0;
    10 
    11     private Object objMonitor = new Object();
    12 
    13     @Override
    14     public void run() {
    15         addCount();
    16 
    17     }
    18 
    19     public void addCount(){
    20         synchronized (objMonitor){
    21             int i = 0;
    22             while (i++ < 100000) {
    23                 count++;
    24             }
    25         }
    26 
    27     }
    28 
    29     public static void main(String[] args) throws Exception{
    30         SynchronizedTest obj = new SynchronizedTest();
    31         Thread t1 = new Thread(obj);
    32         Thread t2 = new Thread(obj);
    33         t1.start();
    34         t2.start();
    35         t1.join();
    36         t2.join();
    37         System.out.println(count);
    38 
    39     }
    40 
    41 
    42 }
    View Code

      我们在 addCount 方法体中增加了一个 synchronized 代码块,将里面的 while 循环包括在其中,保证同一时刻只能有一个线程进入这个循环去改变count的值。

    3、修饰普通方法

     1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
     2 
     3 
     4 /**
     5  * Create by ItCoke
     6  */
     7 public class SynchronizedTest implements Runnable{
     8 
     9     public static int count = 0;
    10 
    11     private Object objMonitor = new Object();
    12 
    13     @Override
    14     public void run() {
    15         addCount();
    16 
    17     }
    18 
    19     public synchronized void addCount(){
    20         int i = 0;
    21         while (i++ < 100000) {
    22             count++;
    23         }
    24 
    25     }
    26 
    27     public static void main(String[] args) throws Exception{
    28         SynchronizedTest obj = new SynchronizedTest();
    29         Thread t1 = new Thread(obj);
    30         Thread t2 = new Thread(obj);
    31         t1.start();
    32         t2.start();
    33         t1.join();
    34         t2.join();
    35         System.out.println(count);
    36 
    37     }
    38 
    39 
    40 }
    View Code

      对比上面修饰代码块,直接将 synchronized 加到 addCount 方法中,也能解决线程安全问题。

    4、修饰静态方法

      这个我们就不贴代码演示了,将 addCount() 声明为一个 static 修饰的方法,然后在加上 synchronized ,也能解决线程安全问题。

    5、原子性、可见性、有序性

      通过 synchronized 修饰的方法或代码块,能够同时保证这段代码的原子性、可见性和有序性,进而能够保证这段代码的线程安全。

      比如通过 synchronized 修饰的代码块:

      

     

       其中 objMonitor 表示锁对象(下文会介绍这个锁对象),只有获取到这个锁对象之后,才能执行里面的代码,执行完毕之后,在释放这个锁对象。那么同一时刻就会只有一个线程去执行这段代码,把多线程变成了单线程,当然不会存在并发问题了。

      这个过程,大家可以想象在公司排队上厕所的情景。

      对于原子性,由于同一时刻单线程操作,肯定能够保证原子性。

      对于有序性,在JMM内存模型中的Happens-Before规定如下,所以也是能够保证有序性的。

    程序的顺序性规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。

      最后对于可见性,JMM内存模型也规定了:

    对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

      大家可能会奇怪,synchronized 并没有lock和unlock操作啊,怎么也能够保证可见性,大家不要急,其实JVM对于这个关键字已经隐式的实现了,下文看字节码会明白的。

    6、锁对象

      大家要注意,我在通过synchronized修饰同步代码块时,使用了一个 Object 对象,名字叫 objMonitor。而对于修饰普通方法和静态方法时,只是在方法声明时说明了,并没有锁住什么对象,其实这三者都有各自的锁对象,只有获取了锁对象,线程才能进入执行里面的代码。

    1、修饰代码块:锁定锁的是synchonized括号里配置的对象
    2、修饰普通方法:锁定调用当前方法的this对象
    3、修饰静态方法:锁定当前类的Class对象

       多个线程之间,如果要通过 synchronized 保证线程安全,获取的要是同一把锁。如果多个线程多把锁,那么就会有线程安全问题。如下:

     1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
     2 
     3 
     4 /**
     5  * Create by ItCoke
     6  */
     7 public class SynchronizedTest implements Runnable{
     8 
     9     public static int count = 0;
    10 
    11 
    12 
    13     @Override
    14     public void run() {
    15         addCount();
    16 
    17     }
    18 
    19     public void addCount(){
    20         Object objMonitor = new Object();
    21         synchronized(objMonitor){
    22             int i = 0;
    23             while (i++ < 100000) {
    24                 count++;
    25             }
    26         }
    27     }
    28 
    29     public static void main(String[] args) throws Exception{
    30         SynchronizedTest obj = new SynchronizedTest();
    31         Thread t1 = new Thread(obj);
    32         Thread t2 = new Thread(obj);
    33         t1.start();
    34         t2.start();
    35         t1.join();
    36         t2.join();
    37         System.out.println(count);
    38 
    39     }
    40 
    41 
    42 }
    View Code

      我们把原来的锁 objMonitor 对象从全局变量移到 addCount() 方法中,那么每个线程进入每次进入addCount() 方法都会新建一个 objMonitor 对象,也就是多个线程用多把锁,肯定会有线程安全问题。

    7、可重入

      可重入什么意思?字面意思就是一个线程获取到这个锁了,在未释放这把锁之前,还能进入获取锁,如下:

      

      在 addCount() 方法的 synchronized 代码块中继续调用 printCount() 方法,里面也有一个 synchronized ,而且都是获取的同一把锁——objMonitor。

      synchronized 是能够保证这段代码正确运行的。至于为什么具有这个特性,可以看下文的实现原理。

    8、实现原理

       对于如下这段代码:

     1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
     2 
     3 /**
     4  * Create by YSOcean
     5  */
     6 public class SynchronizedByteClass {
     7     Object objMonitor = new Object();
     8 
     9     public synchronized void method1(){
    10         System.out.println("Hello synchronized 1");
    11     }
    12 
    13     public synchronized static void method2(){
    14         System.out.println("Hello synchronized 2");
    15     }
    16 
    17     public void method3(){
    18         synchronized(objMonitor){
    19             System.out.println("Hello synchronized 2");
    20         }
    21 
    22     }
    23 
    24     public static void main(String[] args) {
    25 
    26     }
    27 }
    View Code

      我们可以通过两种方法查看其class文件的汇编代码。

      ①、IDEA下载 jclasslib 插件

      

       然后点击 View——Show Bytecode With jclasslib

      

      ②、通过 javap 命令

    javap -v 文件名(不要后缀)

      注意:这里生成汇编的命令是根据编译之后的字节码文件(class文件),所以要先编译。

      ③、修饰代码块汇编代码

      我们直接看method3() 的汇编代码:

      

     

       对于上图出现的 monitorenter 和 monitorexit 指令,我们查看 JVM虚拟机规范:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html,可以看到对这两个指令的介绍。

      下面我们说明一下这两个指令:

      一、monitorenter

      

      每个对象与一个监视器锁(monitor)关联。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

      1、如果 monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

      2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

    下一篇:没有了