Java关键字(八)——synchronized
synchronized 这个关键字,我相信对于并发编程有一定了解的人,一定会特别熟悉,对于一些可能在多线程环境下可能会有并发问题的代码,或者方法,直接加上synchronized,问题就搞定了。
但是用归用,你明白它为什么要这么用?为什么就能解决我们所说的线程安全问题?
下面,可乐将和大家一起深入的探讨这个关键字用法。
首先大家看一段代码,大家想想最后的打印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?????@Override12?????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。
直接上代码:
?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?????@Override14?????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的值。
?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?????@Override14?????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 方法中,也能解决线程安全问题。
这个我们就不贴代码演示了,将 addCount() 声明为一个 static 修饰的方法,然后在加上 synchronized ,也能解决线程安全问题。
通过 synchronized 修饰的方法或代码块,能够同时保证这段代码的原子性、可见性和有序性,进而能够保证这段代码的线程安全。
比如通过 synchronized 修饰的代码块:
?
? 其中 objMonitor 表示锁对象(下文会介绍这个锁对象),只有获取到这个锁对象之后,才能执行里面的代码,执行完毕之后,在释放这个锁对象。那么同一时刻就会只有一个线程去执行这段代码,把多线程变成了单线程,当然不会存在并发问题了。
这个过程,大家可以想象在公司排队上厕所的情景。
对于原子性,由于同一时刻单线程操作,肯定能够保证原子性。
对于有序性,在JMM内存模型中的Happens-Before规定如下,所以也是能够保证有序性的。
程序的顺序性规则(Program?Order?Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
最后对于可见性,JMM内存模型也规定了:
对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
大家可能会奇怪,synchronized 并没有lock和unlock操作啊,怎么也能够保证可见性,大家不要急,其实JVM对于这个关键字已经隐式的实现了,下文看字节码会明白的。
大家要注意,我在通过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?????@Override14?????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 对象,也就是多个线程用多把锁,肯定会有线程安全问题。
可重入什么意思?字面意思就是一个线程获取到这个锁了,在未释放这把锁之前,还能进入获取锁,如下:
在 addCount() 方法的 synchronized 代码块中继续调用 printCount() 方法,里面也有一个 synchronized ,而且都是获取的同一把锁——objMonitor。
synchronized 是能够保证这段代码正确运行的。至于为什么具有这个特性,可以看下文的实现原理。
? 对于如下这段代码:
?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.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
二、monitorexit
?
?
执行monitorexit的线程必须是object ref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过上面介绍,我们可以知道 synchronized 底层就是通过这两个命令来执行的同步机制,由此我们也可以看出synchronized 具有可重入性。
③、修饰普通方法和静态方法汇编代码
?
?
? 可以看到都是通过指令 ACC_SYNCHRONIZED 来控制的,虽然没有看到方法的同步并没有通过指令monitorenter和monitorexit来完成,但其本质也是通过这两条指令来实现。
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实和修饰代码块本质上没有区别,只是方法的同步是一种隐式的方式来实现。
可能会有细心的朋友发现,我在介绍 synchronized 修饰代码块时,给出的汇编代码,用红框圈住了两个 monitorexit,根据我们前面介绍,获取monitor加1,退出monitor减1,等于0时,就没有锁了。那为啥会有两个 monitorexit,而只有一个?monitorenter 呢?
?
第 6 行执行 monitorenter,然后第16行执行monitorexit,然后执行第17行指令 goto 25,表示跳到第25行代码,第25行是 return,也就是直接结束了。
那第20-24行代码中是什么意思呢?其中第 24 行指令 athrow 表示Java虚拟机隐式处理方法完成异常结束时的监视器退出,也就是执行发生异常了,然后去执行 monitorexit。
进而可以得到结论:
synchronized?修饰的方法或代码块,在执行过程中抛出异常了,也能释放锁(unlock)
我们可以看如下方法,手动抛出异常:
?
? 然后获取其汇编代码,就只有一个?monitorexit 指令了。
?
原文:https://blog.51cto.com/u_12749768/2823629