Java-JUC(十五):synchronized执行流程分析
一、锁对象及 synchronized 的使用
synchronized 通过互斥锁(Mutex Lock)来实现,同一时刻,只有获得锁的线程才可以执行锁内的代码。
锁对象分为两种:
实例对象(一个类有多个)和 Class 对象(一个类只有一个)。
不同锁对象之间的代码执行互不干扰,同一个类中加锁方法与不加锁方法执行互不干扰。
使用 synchronized 有以下种方式:
修饰普通方法,锁当前实例对象。
修饰静态方法,锁当前类的 Class 对象。
修饰代码块,锁括号中的对象(实例对象或 Class 对象)。
示例:
class SynchronizedDemo {
// 类锁(修饰静态方法:锁当前类的 Class 对象。)
public static synchronized void inStaticMethod() {
for (int i = 0; i < 10; i++) {
System.out.println("aaa");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 类锁(修饰代码块,锁括号中的 Class 对象)
public static void inStaticMethodLockClassObj() {
synchronized(SynchronizedDemo.class){
for (int i = 0; i < 10; i++) {
System.out.println("aaa");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 对象锁(修饰普通方法:锁当前实例对象)
public synchronized void inNormalMethod() {
for (int i = 0; i < 10; i++) {
System.out.println("bbb");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 对象锁(修饰代码块:锁括号中的实例对象)
public void bb() {
synchronized(this){
for (int i = 0; i < 10; i++) {
System.out.println("bbb");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 无锁
public void cc() {
for (int i = 0; i < 10; i++) {
System.out.println("ccc");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
二、特性
原子性
被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java 中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。
可见性
对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。
有序性
synchronized 本身是无法禁止指令重排和处理器优化的,
as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。
编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。
synchronized 修饰的代码,同一时间只能被同一线程执行。所以,可以保证其有序性。
三、静态方法内部的代码块执行分析
测试代码:
public class DriverInstance {
private static DriverInstance instance = null; private DriverInstance() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static DriverInstance getInstance() {
if (instance == null) {
synchronized (DriverInstance.class) {
System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->locking ");
if (instance == null) {
System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->begin set value ");
instance = new DriverInstance();
System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->end set value ");
}
System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->unlock ");
}
} return instance;
}
} public class Main {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DriverInstance.getInstance());
countDownLatch.countDown();
}
}).start();
} try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("first complete..."); for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DriverInstance.getInstance());
}
}).start();
}
}
}
打印结果:
1027791747517115-> Thread-0 ->locking
1027791747791125-> Thread-0 ->begin set value
1027792830581000-> Thread-0 ->end set value
1027792830905707-> Thread-0 ->unlock
1027792831314389-> Thread-9 ->locking
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792831593375-> Thread-9 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792831900975-> Thread-8 ->locking
1027792832238745-> Thread-8 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792832385236-> Thread-6 ->locking
1027792832555676-> Thread-6 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792832681639-> Thread-7 ->locking
1027792832829374-> Thread-7 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792832990484-> Thread-4 ->locking
1027792833090010-> Thread-4 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833279111-> Thread-5 ->locking
1027792833473500-> Thread-5 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833549389-> Thread-3 ->locking
1027792833643007-> Thread-3 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833709254-> Thread-2 ->locking
1027792833797895-> Thread-2 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833875651-> Thread-1 ->locking
1027792833993217-> Thread-1 ->unlock
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
first complete...
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
测试结果解读:
1)多个线程是同时进入getInstance方法执行,在执行getInstance方法时(除了synchronized代码块以外的代码)线程之间是异步执行的;
2)从上边测试结果可以看出,线程Thread-0优先进入了synchronized中(优先获取到了synchronized锁),这时其他9个线程都在排队等待进入synchronized中;
3)当thread-0释放锁后,才其他线程排队依次执行“获取锁、初始化 instance、释放锁”。
4)因为是第一次执行,多个线程中 instance 初始值都是null,因此当它们进入到第一个if(instance ==null),然后排队获取锁,上边测试结果中thread-0获取到锁后给 instance 赋值,赋值之后释放锁,释放锁的同时更新 instance 变量内存值(同时把所有的thread的本地副本变量刷新),当thread-0已经释放了锁后,队列中的等待获取锁的其他线程依次“获取到锁,进入锁内部代码执行第二个if(instance ==null)发现变量 instance 值已经不为空,不执行 instance 赋值操作,释放锁”。
5)经过CountDownLatch的wait之后,重新启动的10个线程,此时这10个线程在初始化的时候 instance 的内存值不为空,每个线程赋值 instance 到本地线程,然后执行getInstalce,进度第一个if(instance==null)判断,发现不为空,直接返回 instance 变量,根本不会进入到锁内部。
四、synchronized 的实现:monitor 和 ACC_SYNCHRONIZED
synchronized的实现是基于monitor实现的,但是使用方式不同(修饰方法、修饰代码块),其内部实现有差别:
同步代码块
JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.14
使用 monitorenter 和 monitorexit 两个指令实现。
每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。
当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增(可重入性)。当同一个线程释放锁(执行 monitorexit)后,该计数器自减。当计数器为0的时候,锁将被释放。
同步方法
JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当线程访问时候,会检查是否有 ACC_SYNCHRONIZED,有则需要先获得锁,然后才能执行方法,执行完或执行发生异常都会自动释放锁。
ACC_SYNCHRONIZED 也是基于 Monitor 实现的。
举例分析
来查看下具体的静态方法内部的静态代码块的一个示例:
编写一个测试类DriverInstance.java:
package com.dx.test; public class DriverInstance {
private static DriverInstance instance = null; private DriverInstance() {
} public static DriverInstance getInstance() {
if (instance == null) {
synchronized (DriverInstance.class) {
if (instance == null) {
instance = new DriverInstance();
}
}
} return instance;
} public void test(){ }
}
使用javac编译DriverInstance.java代码:
E:\work>javac DriverInstance.java
生成DriverInstance.class
使用javap -v DriverInstance.class反编译DriverInstance.class
E:\work>javap -v DriverInstance.class
Classfile /E:/work/DriverInstance.class
Last modified 2019-8-28; size 610 bytes
MD5 checksum 73f4e1682a85c9a8cf854edfdd09261b
Compiled from "DriverInstance.java"
public class com.dx.test.DriverInstance
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#22 // com/dx/test/DriverInstance.instance:Lcom/dx/test/DriverInstance;
#3 = Class #23 // com/dx/test/DriverInstance
#4 = Methodref #3.#21 // com/dx/test/DriverInstance."<init>":()V
#5 = Class #24 // java/lang/Object
#6 = Utf8 instance
#7 = Utf8 Lcom/dx/test/DriverInstance;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 getInstance
#13 = Utf8 ()Lcom/dx/test/DriverInstance;
#14 = Utf8 StackMapTable
#15 = Class #24 // java/lang/Object
#16 = Class #25 // java/lang/Throwable
#17 = Utf8 test
#18 = Utf8 <clinit>
#19 = Utf8 SourceFile
#20 = Utf8 DriverInstance.java
#21 = NameAndType #8:#9 // "<init>":()V
#22 = NameAndType #6:#7 // instance:Lcom/dx/test/DriverInstance;
#23 = Utf8 com/dx/test/DriverInstance
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/Throwable
{
public static com.dx.test.DriverInstance getInstance();
descriptor: ()Lcom/dx/test/DriverInstance;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: getstatic #2 // Field instance:Lcom/dx/test/DriverInstance;
3: ifnonnull 37
6: ldc #3 // class com/dx/test/DriverInstance
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field instance:Lcom/dx/test/DriverInstance;
14: ifnonnull 27
17: new #3 // class com/dx/test/DriverInstance
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field instance:Lcom/dx/test/DriverInstance;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field instance:Lcom/dx/test/DriverInstance;
40: areturn
Exception table:
from to target type
11 29 32 any
32 35 32 any
LineNumberTable:
line 10: 0
line 11: 6
line 12: 11
line 13: 17
line 15: 27
line 18: 37
StackMapTable: number_of_entries = 3
frame_type = 252 /* append */
offset_delta = 27
locals = [ class java/lang/Object ]
frame_type = 68 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4 public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 23: 0 static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: aconst_null
1: putstatic #2 // Field instance:Lcom/dx/test/DriverInstance;
4: return
LineNumberTable:
line 4: 0
}
SourceFile: "DriverInstance.java"
备注:
aload 指令的解释:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶
aload_0把this装载到了操作数栈中aload_0是一组格式为aload_的操作码中的一个,这一组操作码把对象的引用装载到操作数栈中标志了待处理的局部变量表中的位置,但取值仅可为0、1、2或者3。
还有一些其他相似的操作码用来装载非对象引用,包括iload_、lload_、fload_和dload_,这里的i代表int型,l代表long型,f代表float型以及d代表double型。在局部变量表中的索引位置大于3的变量的装载可以使用iload、lload、fload,、dload和aload,这些操作码都需要一个操作数的参数,用于确认需要装载的局部变量的位置。
astore 指令的解释:将栈顶数值(objectref)存入当前frame的局部变量数组中指定下标(index)处的变量中,栈顶数值出栈。
在monitorexit之前都会调用aload操作,实际上我们可以理解为“这里就是实现内存可见性实现的,在释放锁之前把变量同步回主存中”。
五、锁获取和锁释放的内存语义
线程A.B同时开始执行,获取主存中的x变量,x的变量初始值是0,线程A优先拿到锁,此时线程A在“同步代码块”或者“同步方法”内修改了x变量的值为1,当线程A释放锁之前会将修改x变量值刷新到主存中。
整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。
线程B获取锁的时候同样会从主内存中共享变量x的值,这个时候就是最新的值1,然后将该值拷贝到线程B的工作内存中去,释放锁的时候同样会重写到主内存中。
从整体上来看,线程A的执行结果(a=1)对线程B是可见的,实现原理为:释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。
从横向来看,这就像线程A通过主内存中的共享变量和线程B进行通信,A 告诉 B 我们俩的共享数据现在为1啦,这种线程间的通信机制正好吻合java的内存模型正好是共享内存的并发模型结构。
六、Java对象如何与Monitor关联
JVM堆中存放的是对象实例,每一个对象都有对象头,对象头里有Mark Word,里面存储着对象的hashCode、GC分代年龄以及锁信息。
如图所示,重量级锁中存有指向monitor的指针。
32位的HotSpot虚拟机对象头存储结构:
为了证实上图的正确性,这里我们看openJDK--》hotspot源码markOop.hpp(新的定义类:MarkWord,hpp),虚拟机对象头存储结构:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/markWord.hpp
上图中有源码中对锁标志位这样枚举
openJDK中ObjectMonitor类定义:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/objectMonitor.hpp
ObjectMonitor中几个关键字段的含义:
_recursions:锁的重入次数。这句话很好理解,这也决定了synchronized是可重入的。
_owner:指向拥有该对象的线程
_WaitSet:主要存放所有wait的线程的对象,也就是说如果有线程处于wait状态,将被挂入这个队列,调用了wait()方法线程会进入该队列。
_EntryList:所有在等待获取锁的线程的对象,也就是说如果有线程处于等待获取锁的状态的时候,将被挂入这个队列。
对象,对象监视器,同步队列和线程状态的关系:
图中描述内容解释:
在Synchronized使用中,任意线程对Object的访问,首先要获得Object的监视器;
如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED;
当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
实际上如果synchronized内部使用wait还会存在另外一种wait状态。
示例:
private static List<Integer> lists = new ArrayList<>(); public static void main(String[] args) {
final Object lock = new Object();
//监控线程
new Thread(()->{
synchronized (lock) {
System.out.println("thread 2 start...");
if(lists.size() != 5) {
try {
System.out.println("thread 2 start wait");
lock.wait();
System.out.println("thread 2 end wait");
} catch (Exception e) { e.printStackTrace(); }
}
System.out.println("thread 2 start notify");
lock.notify();
System.out.println("thread 2 end notify");
}
}, "t2").start(); new Thread(()->{
synchronized (lock) {
for(int i = 0; i < 10; i++) {
System.out.println("thread 1, add " + i);
lists.add(i);
if(lists.size() == 5) {
System.out.println("thread 1 start notify");
lock.notify();
System.out.println("thread 1 end notify");
try {
System.out.println("thread 1 start wait");
lock.wait();
System.out.println("thread 1 end wait");
} catch (Exception e) { e.printStackTrace(); }
}
try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); }
}
}
}, "t1").start(); }
当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 _count 加 1。即获得对象锁。
若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor,_owner 变量恢复为 null,_count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒。
若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁)。
示意图如下:
参考:
《jdk源码剖析二: 对象内存布局、synchronized终极原理》
《Java-内存模型 synchronized 的内存语义》
Java-JUC(十五):synchronized执行流程分析的更多相关文章
- “全栈2019”Java第二十五章:流程控制语句中循环语句while
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- 深入浅出Mybatis系列(十)---SQL执行流程分析(源码篇)
最近太忙了,一直没时间继续更新博客,今天忙里偷闲继续我的Mybatis学习之旅.在前九篇中,介绍了mybatis的配置以及使用, 那么本篇将走进mybatis的源码,分析mybatis 的执行流程, ...
- “全栈2019”Java第二十四章:流程控制语句中决策语句switch下篇
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析
java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...
- “全栈2019”Java第九十五章:方法中可以定义静态局部内部类吗?
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java第十五章:Unicode与转义字符
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java第二十六章:流程控制语句中循环语句do-while
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- 《Java基础复习》-控制执行流程
最近任务太多了,肝哭我了,boom 参考书目:Thinking in Java <Java基础复习>-控制执行流程 Java使用了C的所有流程控制语句 涉及关键字:if-else.whil ...
- 报时机器人的rasa shell执行流程分析
本文以报时机器人为载体,介绍了报时机器人的对话能力范围.配置文件功能和训练和运行命令,重点介绍了rasa shell命令启动后的程序执行过程. 一.报时机器人项目结构 1.对话能力范围 (1)能够 ...
随机推荐
- 小tips:在JS语句执行机制涉及的一种基础类型Completion
看一个如下的例子.在函数 foo 中,使用了一组 try 语句.在 try 中有 return 语句,finally 中的内容还会执行吗? function foo(){ try{ return 0; ...
- Android-----CheckBox复选使用(实现简单选餐)
直接上代码: xml布局: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmln ...
- GoLang 中用 MongoDB Watch 监听指定字段的变化
需要 MongoDB 3.6 及以上, 需要 ReplicaSet 模式. 监听一个字段的变化: func watch(coll *mongo.Collection) { match := bson. ...
- Ingress使用示例
Ingress概念介绍 service只能做四层代理 无法做七层代理(如https服务) lvs只能根据第四层的数据进行转发 无法对七层协议数据进行调度 Ingress Controller ...
- Prometheus学习
简介 Prometheus 最初是 SoundCloud 构建的开源系统监控和报警工具,是一个独立的开源项目,于2016年加入了 CNCF 基金会,作为继 Kubernetes 之后的第二个托管项目. ...
- 【转】在Keil uv5里面添加STC元器件库,不影响其他元件
先到网上下载stc.CBD(http://download.csdn.net/detail/mao0514/9699117) 还有STC新系列单片机的头文件,宏晶的网站就有 1.在Keil/C51/I ...
- Kubernetes架构及相关服务详解
11.1.了解架构 K8s分为两部分: 1.Master节点 2.node节点 Master节点组件: 1.etcd分布式持久化存储 2.api服务器 3.scheduler 4.controller ...
- js--同步运动json下
这一节针对上一节讲述的bug,我们来处理一下. 这个bug存在的原因就是,一旦只要有一个属性值达到目标值就会清除定时器,所以我们要改变 的就是清除定时器的那么部分.看下面的修改 var timer; ...
- Beta冲刺博客汇总(校园帮-追光的人)
所属课程 软件工程1916 作业要求 Beta冲刺博客汇总 团队名称 追光的人 作业目标 汇总Beta阶段的博客,方便查看 冲刺日志 Beta之前-凡事预则立(校园帮-追光的人)5-22 Beta冲刺 ...
- 28、Python网络编程
一.基于TCP协议的socket套接字编程 1.套接字工作流程 先从服务器端说起.服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客 ...