上篇文章从无到有分析了如何实现"锁",虽然仅仅实现了最简单的锁,但"锁"的精华已经提取出来了,有了这些知识,本篇将分析系统提供的锁-synchronized关键字的使用与实现。
通过本篇文章,你将了解到:
1、synchronized 如何使用
2、synchronized 源码初探
3、总结
由上篇文章可知,多线程访问临界区需要锁:
临界区可以是一段代码,也可以是某个方法。
按锁作用区域划分,可分为两类:
修饰方法又分为两类:实例方法与静态方法。先来看看实例方法:
实例方法
public class TestSynchronized { //共享变量 private int a = 0; public static void main(String args[]) { final TestSynchronized testSynchronized = new TestSynchronized(); Thread t1 = new Thread(new Runnable() { @Override public void run() { int count = 0; while (count < 10000) { testSynchronized.func1(); count++; } System.out.println("a = " + testSynchronized.getA() + " in thread1"); } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { int count = 0; while (count < 10000) { testSynchronized.func1(); count++; } System.out.println("a = " + testSynchronized.getA() + " in thread2"); } }); t2.start(); try { t1.join(); t2.join(); //等待t1,t2执行完毕,再打印结果 System.out.println("a = " + testSynchronized.getA() + " in mainThread"); } catch (Exception e) { } } private synchronized void func1() { //修改a a++; } private int getA() { return a; } }
以上两个线程t1、t2都需要修改共享变量a的值,同时调用TestSynchronized 的对象方法: func1()进行自增。每个线程调用func1() 10000次,循环结束后线程停止运行。理论上每个线程都对a的值增加了10000次,也就是说最后a的值应为为:a==20000,来看看在主线程里打印a的最终值:
可以看出,多线程访问的结果正确,说明synchronized修饰的实例方法能够正确实现了多线程并发。
静态方法
再来看看静态方法:
public class TestSynchronized { //共享变量 private static int a = 0; public static void main(String args[]) { Thread t1 = new Thread(new Runnable() { @Override public void run() { int count = 0; while (count < 10000) { func1(); count++; } System.out.println("a = " + getA() + " in thread1"); } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { int count = 0; while (count < 10000) { func1(); count++; } System.out.println("a = " + getA() + " in thread2"); } }); t2.start(); try { t1.join(); t2.join(); //等待t1,t2执行完毕,再打印结果 System.out.println("a = " + getA() + " in mainThread"); } catch (Exception e) { } } private static synchronized void func1() { //修改a a++; } private static int getA() { return a; } }
相对于修饰实例方法,只是更改了a为static类型,并且将func1()变为静态方法,最终的结果与前面实例方法是一致的。
说明synchronized修饰的静态方法能够正确实现了多线程并发。
synchronized 修饰方法时(静态方法/实例方法),在进入方法前先申请锁,退出方法后释放锁。假若有个方法里执行的操作比较多,而需要并发访问的就只有一小段,如果为了这小段临界区将方法用synchronized修饰,那么将是大材小用。为此synchronized提供了修饰一段代码块的方法。
按锁类型划分,修饰代码块也分为两类:
获取对象锁
//声明锁对象 private static Object object = new Object(); private void func1() { //无需互斥访问的区域 int b = 1000; int c = 0; if (c < b) { c++; } //修改a //需要互斥访问的区域 synchronized (object) { a++; } }
可以看出虽然func1方法里有其它操作,但是对于多线程操作不敏感,只有共享变量a需要互斥访问,因此仅仅需要对操作a使用synchronized修饰。
synchronized (object) 表示获取实例对象:object的锁。
获取类锁
再来看看如何使用类锁:
private void func1() { //无需互斥访问的区域 int b = 1000; int c = 0; if (c < b) { c++; } //修改a //需要互斥访问的区域 synchronized (TestSynchronized.class) { a++; } }
这次没有实例化对象了,而是直接使用TestSynchronized.class,表示获取TestSynchronized 类锁。
将上述关系用图表示:
1、无论是修饰方法还是代码块,最终都是获取对象锁(类锁是Class对象的锁)
2、实例方法与对象锁获取的是同一把锁(普通对象锁)
3、静态方法与类锁获取的是同一把锁(类锁-Class对象锁)
对象锁
private void func1() { synchronized (this) { } } private synchronized void func2() { } private void func3() { }
func1()与func2()都需要获取对象锁(this指的是本对象,也就是调用方法的对象本身),因此两者的访问是互斥的,而访问func3()则不受影响。
类锁
private void func1() { synchronized (TestSynchronized.class) { } } private static synchronized void func2() { } private static synchronized void func3() { } private static void func4() { }
func1()、func2()、func3()都需要获取类锁,此处的类锁为TestSynchronized.class 对象,因此三者的访问是互斥的,而访问func4()则不受影响。
由此可知:
1、类锁与对象锁互不影响
2、多线程需要获取"同一把锁"才能实现互斥
上面的例子离不开synchronized 修饰符,这是个关键字,JVM是如何识别这个关键字的呢?首先来看看synchronized编译后的结果:
先来看Demo:
public class TestSynchronized { //共享变量 int a = 0; Object object = new Object(); public static void main(String args[]) { } private void add() { synchronized (object) { a++; } } }
以上是使用对象锁修饰了代码块。现在将它编译为.class文件,定位到TestSynchronized.java 文件目录,打开命令行,输入如下命令:
javac TestSynchronized.java
与TestSynchronized.java文件同目录下将生成TestSynchronized.class。
.class 文件肉眼看不出所以然,因此将它反编译看看,依然在同级目录下使用如下命令:
javap -verbose -p TestSynchronized.class
然后命令行输出一串结果,当然如果你觉得不方便查看,可以将输出结果放在文件里,使用如下命令:
javap -verbose -p TestSynchronized.class > mytest.txt
来看看输出的重点内容:
上图重点圈出了两个指令:monitorenter与monitorexit。
- monitorenter 表示获取锁
- monitorexit 表示释放锁
- 两者之间的操作就是被锁住的临界区
其中monitorexit 有两个,后面一个是发生异常时会执行
monitorenter/monitorexit 指令对应的代码在哪呢?
网上有不同的解释,我倾向于:https://github.com/farmerjohngit/myblog/issues/13 中所作的分析:
- 在Hotspot中只用到了模板解释器(templateTable_x86_64.cpp)
,字节码解释器(bytecodeInterpreter.cpp)根本就没用到- 模板解释器里都是汇编代码,字节码解释器用的是C++实现的,两者逻辑是大同小异的,为了更方便阅读以字节码解释器为例
monitorenter指令对应代码:
在bytecodeInterpreter.cpp#1804行。
monitorexit指令对应代码:
在bytecodeInterpreter.cpp#1911行。
由以上可知,我们找到了monitorenter/monitorexit 指令对应的代码入口,也就是指令具体的实现位置。
先来看Demo:
public class TestSynchronized { //共享变量 int a = 0; Object object = new Object(); public static void main(String args[]) { } private synchronized void add() { a++; } }
同样的使用javap指令,结果如下:
与修饰代码块不一样的是:并没有monitorenter/monitorexit 指令,但是多了ACC_SYNCHRONIZED 标记,这个标记是怎么解析的呢?
先看看锁的入口和出口对应的代码:
方法锁入口
在bytecodeInterpreter.cpp#643行。
上图标红的部分从名字可以看出判断该方法是否是同步方法,若是同步方法,则进行获取锁的步骤。
寻找is_synchronized()函数,在method.hpp里。
继续看accessFlags.hpp:
最终看jvm.h
可以看出:
用synchronized关键字修饰方法后,反编译出来的代码里带有:ACC_SYNCHRONIZED 标记与JVM里的JVM_ACC_SYNCHRONIZED 对应,而这个参数最终使用的地方是通过is_synchronized()函数用来判断是否是同步方法。
方法锁出口
方法结束后运行此段代码,里边判断是否是同步方法,进而进行释放锁等操作。
synchronized修饰代码块和方法,两者异同:
1、修饰代码块时编译后会在临界区前后加入monitorenter、monitorexit 指令
2、修饰方法时进入/退出方法时会判断ACC_SYNCHRONIZED 标记是否存在
3、不管是用monitorenter/monitorexit 还是ACC_SYNCHRONIZED,最终都是在对象头上做文章,都需要获取锁。
了解了synchronized使用及其源码入口,接下来将深入探析其工作机制。下篇将会分析无锁、偏向锁、轻量级锁、重量级锁的实现机制。
本文基于jdk8。