前言
线程并发系列文章:
Java 线程基础
Java “优雅”地中断线程
Java 线程状态
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
上篇文章从无到有分析了如何实现"锁",虽然仅仅实现了最简单的锁,但"锁"的精华已经提取出来了,有了这些知识,本篇将分析系统提供的锁-synchronized关键字的使用与实现。
通过本篇文章,你将了解到:
1、synchronized 如何使用
2、synchronized 源码初探
3、总结
1、synchronized 如何使用
多线程访问临界区
由上篇文章可知,多线程访问临界区需要锁:
临界区可以是一段代码,也可以是某个方法。
synchronized 各种使用方式
按锁作用区域划分,可分为两类:
修饰方法
修饰方法又分为两类:实例方法与静态方法。先来看看实例方法:
实例方法
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、多线程需要获取"同一把锁"才能实现互斥
2、synchronized 源码初探
上面的例子离不开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 指令对应代码
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()函数用来判断是否是同步方法。
方法锁出口
方法结束后运行此段代码,里边判断是否是同步方法,进而进行释放锁等操作。
3、总结
synchronized修饰代码块和方法,两者异同:
1、修饰代码块时编译后会在临界区前后加入monitorenter、monitorexit 指令
2、修饰方法时进入/退出方法时会判断ACC_SYNCHRONIZED 标记是否存在
3、不管是用monitorenter/monitorexit 还是ACC_SYNCHRONIZED,最终都是在对象头上做文章,都需要获取锁。
了解了synchronized使用及其源码入口,接下来将深入探析其工作机制。下篇将会分析无锁、偏向锁、轻量级锁、重量级锁的实现机制。
本文基于jdk8。
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android
1、Android各种Context的前世今生
2、Android DecorView 一窥全貌(上)
3、Android DecorView 一窥全貌(下)
4、Window/WindowManager 不可不知之事
5、View Measure/Layout/Draw 真明白了
6、Android事件分发全套服务
7、Android invalidate/postInvalidate/requestLayout 彻底厘清
8、Android Window 如何确定大小/onMeasure()多次执行原因
9、Android事件驱动Handler-Message-Looper解析
10、Android 键盘一招搞定
11、Android 各种坐标彻底明了
12、Android Activity/Window/View 的background
13、Android IPC 之Service 还可以这么理解
14、Android IPC 之Binder基础
15、Android IPC 之Binder应用
16、Android IPC 之AIDL应用(上)
17、Android IPC 之AIDL应用(下)
18、Android IPC 之Messenger 原理及应用
19、Android IPC 之获取服务(IBinder)
20、Android 存储基础
21、Android 10、11 存储完全适配(上)
22、Android 10、11 存储完全适配(下)
23、Java 并发系列不再疑惑