【万字长文,建议收藏】关于Synchronized锁升级,你

前言

毫无疑问,synchronized是我们用过的第一个并发关键字,很多博文都在讲解这个技术。不过大多数讲解还停留在对synchronized的使用层面,其底层的很多原理和优化,很多人可能并不知晓。因此本文将通过对synchronized的大量C源码分析,让大家对他的了解更加透彻点。

本篇将从为什么要引入synchronized,常见的使用方式,存在的问题以及优化部分这四个方面描述,话不多说,开始表演。

可见性问题及解决

概念描述

指一个线程对共享变量进行修改,另一个能立刻获取到修改后的最新值。

代码展示

类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码public class Example1 {
//1.创建共享变量
private static boolean flag = true;

public static void main(String[] args) throws Exception {
//2.t1空循环,如果flag为true,不退出
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if(!flag){
System.out.println("进入if");
break;
}
}
}
});
t1.start();

Thread.sleep(2000L);
//2.t2修改flag为false
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
flag = false;
System.out.println("修改了");
}
});

t2.start();
}
}

运行结果:

分析

这边先要了解下Java的内存模式,不明白的可点击传送门,todo。

下图线程t1,t2从主内存分别获取flag=true,t1空循环,直到flag为false的时候退出循环。t2拿到flag的值,将其改为false,并写入到主内存。此时主内存和线程t2的工作内存中flag均为false,但是线程t1工作内存中的flag还是true,所以一直退不了循环,程序将一直执行。

synchronized如何解决可见性

首先我们尝试在t1线程中加一行打印语句,看看效果。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码public class Example1 {
//1.创建共享变量
private static boolean flag = true;

public static void main(String[] args) throws Exception {
//2.t1空循环,如果flag为true,不退出
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
//新增的打印语句
System.out.println(flag);
if(!flag){
System.out.println("进入if");
break;
}
}
}
});
t1.start();

Thread.sleep(2000L);
//2.t2修改flag为false
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
flag = false;
System.out.println("修改了");
}
});

t2.start();
}
}

运行结果:

我们发现if里面的语句已经打印出来了,线程1已经感知到线程2对flag的修改,即这条打印语句已经影响了可见性。这是为啥?

答案就是println中,我们看下源码:

println有个上锁的过程,即操作如下:

1.获取同步锁。

2.清空自己工作内存上的变量。

3.从主内存获取最新值,并加载到工作内存中。

4.打印并输出。

所以这里解释了为什么线程t1加了打印语句之后,t1立刻能感知t2对flag的修改。因为每次打印的时候其都从主内存上获取了最新值,当t2修改的时候,t1立刻从主内存获取了值,所以进入了if语句,并最终能跳出循环。

synchronized的原理就是清空自己工作内存上的值,通过将主内存最新值刷新到工作内存中,让各个线程能互相感知修改。

原子性问题及解决

概念描述

在一次或多个操作中,要不所有操作都执行,要不所有操作都不执行。

代码展示

类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码public class Example2 {
//1.定义全局变量number
private static int number = 0;

public static void main(String[] args) throws Exception {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
number++;
}
};
//2.t1让其自增10000
Thread t1 = new Thread(runnable);
t1.start();

//3.t2让其自增10000
Thread t2 = new Thread(runnable);
t2.start();

//4.等待t1,t2运行结束
t1.join();
t2.join();
System.out.println("number=" + number);
}
}

运行结果:

分析

每个线程执行的逻辑是循环1万次,每次加1,那我们希望的结果是2万,但是实际上结果是不足2万的。我们先用javap命令反汇编,我们看到很多代码,但是number++涉及的指令有四句,具体看第二张图。

如果有多条线程执行这段number++代码,当前number为0,线程1先执行到iconst_1指令,即将执行iadd操作,而线程2执行到getstatic指令,这个时候number值还没有改变,所以线程2获取到的静态字段是0,线程1执行完iadd操作,number变为1,线程2执行完iadd操作,number还是1。这个时候就发现问题了,做了两次number++操作,但是number只增加了1。

并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半的时候,另外一个线程也有可能来操作共享变量,这个时候就出现了问题。

synchronized如何解决原子性问题

在上面的分析中,我们已经知道发生问题的原因,number++是由四条指令组成,没有保证原子操作。所以,我们只要将number++作为一个整体就行,即保证他的原子性。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码public class Example2 {
//1.定义全局变量number
private static int number = 0;
//新增一个静态变量object
private static Object object = new Object();

public static void main(String[] args) throws Exception {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
//将number++的操作用object对象锁住
synchronized (object) {
number++;
}
}
};
//2.t1让其自增10000
Thread t1 = new Thread(runnable);
t1.start();

//3.t2让其自增10000
Thread t2 = new Thread(runnable);
t2.start();

//4.等待t1,t2运行结束
t1.join();
t2.join();
System.out.println("number=" + number);
}
}

我们看到最终number为20000,那为什么要加上synchronized,结果就正确了?我们再反编译下Example2,可以看到在四行指令前后分别有monitorenter和monitorexist,线程1在执行中间指令时,其他线程不可以进入monitorenter,需要等线程1执行完monitorexist,其他进程才能继续monitorenter,进行自增操作。

有序性问题及解决

概念描述

代码中程序执行的顺序,Java在编译和运行时会对代码进行优化,这样会导致我们最终的执行顺序并不是我们编写代码的书写顺序。

代码展示

咱先来看一个概念,重排序,也就是语句的执行顺序会被重新安排。其主要分为三种:

1.编译器优化的重排序:可以重新安排语句的执行顺序。

2.指令级并行的重排序:现代处理器采用指令级并行技术,将多条指令重叠执行。

3.内存系统的重排序:由于处理器使用缓存和读写缓冲区,所以看上去可能是乱序的。

上面代码中的a = new A();可能被被JVM分解成如下代码:

1
2
3
4
复制代码// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址复制代码
1
2
3
4
复制代码 // 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象 复制代码

一旦假设发生了这样的重排序,比如线程A在执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候线程B进入了第一个语句,它会判断a不为空,即直接返回了a。其实这是一个未初始化完成的a,即会出现问题。

synchronized如何解决有序性问题

给上面的三个步骤加上一个synchronized关键字,即使发生重排序也不会出现问题。线程A在执行步骤1和步骤3时,线程B因为没法获取到锁,所以也不能进入第一个语句。只有线程A都执行完,释放锁,线程B才能重新获取锁,再执行相关操作。

synchronized的常见使用方式

修饰代码块(同步代码块)

1
2
3
复制代码synchronized (object) {
//具体代码
}

修饰方法

1
2
3
复制代码synchronized void test(){
//具体代码
}

synchronized不能继承?(插曲)

父类A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class A {
synchronized void test() throws Exception {
try {
System.out.println("main 下一步 sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("main 下一步 sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
}

子类B:(未重写test方法)

1
2
3
复制代码public class B extends A {

}

子类C:(重写test方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class C extends A {

@Override
void test() throws Exception{
try {
System.out.println("sub 下一步 sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("sub 下一步 sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
}

线程A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class ThreadA extends Thread {
private A a;

public void setter (A a) {
this.a = a;
}

@Override
public void run() {
try{
a.test();
}catch (Exception e){

}
}
}

线程B:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class ThreadB extends Thread {
private B b;
public void setB(B b){
this.b=b;
}

@Override
public void run() {
try{
b.test();
}catch (Exception e){

}
}
}

线程C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class ThreadC extends Thread{
private C c;
public void setC(C c){
this.c=c;
}

@Override
public void run() {
try{
c.test();
}catch (Exception e){

}
}
}

测试类test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码public class test {
public static void main(String[] args) throws Exception {
A a = new A();
ThreadA A1 = new ThreadA();
A1.setter(a);
A1.setName("A1");
A1.start();
ThreadA A2 = new ThreadA();
A2.setter(a);
A2.setName("A2");
A2.start();
A1.join();
A2.join();

System.out.println("=============");
B b = new B();
ThreadB B1 = new ThreadB();
B1.setB(b);
B1.setName("B1");
B1.start();
ThreadB B2 = new ThreadB();
B2.setB(b);
B2.setName("B2");
B2.start();
B1.join();
B2.join();
System.out.println("=============");

C c = new C();
ThreadC C1 = new ThreadC();
C1.setName("C1");
C1.setC(c);
C1.start();
ThreadC C2 = new ThreadC();
C2.setName("C2");
C2.setC(c);
C2.start();
C1.join();
C2.join();
}
}

运行结果:

子类B继承了父类A,但是没有重写test方法,ThreadB仍然是同步的。子类C继承了父类A,也重写了test方法,但是未明确写上synchronized,所以这个方法并不是同步方法。只有显式的写上synchronized关键字,才是同步方法。

所以synchronized不能继承这句话有歧义,我们只要记住子类如果想要重写父类的同步方法,synchronized关键字一定要显示写出,否则无效。

修饰静态方法

1
2
3
复制代码synchronized static void test(){
//具体代码
}

修饰类

1
2
3
复制代码 synchronized (Example2.class) {
//具体代码
}

Java对象 Mark Word

在JVM中,对象在内存中的布局分为三块区域:对象头,实例数据和对齐数据,如下图:

其中Mark Word值在不同锁状态下的展示如下:(重点看线程id,是否为偏向锁,锁标志位信息)

在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。Talk is cheap. Show me the code. 咱来看代码。

  • 我们想要看Java对象的Mark Word,先要加载一个jar包,在pom.xml添加即可。
1
2
3
4
5
复制代码<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
  • 新建一个对象A,拥有初始值为666的变量x。
1
2
3
复制代码public class A {
private int x=666;
}
  • 新建一个测试类test,这涉及到刚才加载的jar,我们打印Java对象。
1
2
3
4
5
6
7
8
复制代码import org.openjdk.jol.info.ClassLayout;

public class test {
public static void main(String[] args) {
A a=new A();
System.out.println( ClassLayout.parseInstance(a).toPrintable());
}
}
  • 我们发现对象头(object header)占了12个字节,为啥和上面说的16个字节不一样。

  • 其实上是默认开启了指针压缩,我们需要关闭指针压缩,也就是添加-XX:-UseCompressedOops配置。

  • 再次执行,发现对象头为16个字节。

偏向锁

什么是偏向锁

JDK1.6之前锁为重量级锁(待会说,只要知道他和内核交互,消耗资源),1.6之后Java设计人员发现很多情况下并不存在多个线程竞争的关系,所以为了资源问题引入了无锁偏向锁轻量级锁重量级锁的概念。先说偏向锁,他是偏心,偏袒的意思,这个锁会偏向于第一个获取他的线程。

偏向锁演示

  • 创建并启动一个线程,run方法里面用了synchronized关键字,功能是打印this的Java对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class test {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
synchronized (this){
System.out.println(ClassLayout.parseInstance(this).toPrintable());
}
}
});
thread.start();
}
}

标红的地方为000,根据之前Mark Word在不同状态下的标志,得此为无锁状态。理论上一个线程使用synchronized关键字,应为偏向锁。

  • 实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟,所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。

  • 重新运行下代码,发现标红地方101,对比Mark Word在不同状态下的标志,得此状态为偏向锁。

偏向锁原理图解

  • 在线程的run方法中,刚执行到synchronized,会判断当前对象是否为偏向锁和锁标志,没有任何线程执行该对象,我们可以看到是否为偏向锁为0,锁标志位01,即无锁状态。

  • 线程会将自己的id赋值给markword,即将原来的hashcode值改为线程id,是否是偏向锁改为1,表示线程拥有对象锁,可以执行下面的业务逻辑。如果synchronized执行完,对象还是偏向锁状态;如果线程结束之后,会撤销偏向锁,将该对象还原成无锁状态。

  • 如果同一个线程中又对该对象进行加锁操作,我们只要对比对象的线程id是否与线程id相同,如果相同即为线程锁重入问题。

优势

加锁和解锁不需要额外的消耗,和执行非同步方法相比只有纳秒级的差距。

白话翻译

线程1锁定对象this,他发现对象为无锁状态,所以将线程id赋值给对象的Mark Word字段,表示对象为线程1专用,即使他退出了同步代码,其他线程也不能使用该对象。

同学A去自习教室C,他发现教室无人,所以在门口写了个名字,表示当前教室有人在使用,这样即使他出去吃了饭,其他同学也不能使用这个房间。

轻量锁

什么是轻量级锁

在多线程交替同步代码块的情况下,线程间没有竞争,使用轻量级锁可以避免重量级锁引入的性能消耗。

轻量级图解

  • 在刚才偏向锁的基础上,如果有另外一个线程也想错峰使用该资源,通过对比线程id是否相同,Java内存会立刻撤销偏向锁(需要等待全局安全点),进行锁升级的操作。

  • 撤销完轻量级锁,会在线程1的方法栈中新增一个锁记录,对象的Mark Word与锁记录交换。

优势

线程不竞争的时候,避免直接使用重量级锁,提高了程序的响应速度。

白话翻译

在刚才偏向锁的基础上,另外一个线程也想要获取资源,所以线程1需要撤销偏向锁,升级为轻量锁。

同学A在使用自习教室外面写了自己的名字,所以同学B来也想要使用自习教室,他需要提醒同学A,不能使用偏向锁,同学A将自习教室门口的名字擦掉,换成了一个书包,里面是自己的书籍。这样在同学A不使用自习教室的时候,同学B也能使用自习教室,只需要将自己的书包也挂在外面即可。这样下次来使用的同学就能知道已经有人占用了该教室。

重量级锁

什么是重量级锁

当多线程之间发生竞争,Java内存会申请一个Monitor对象来实现。

重量级锁原理图解

在刚才的轻量级锁的基础上,线程2也想要申请资源,发现锁的标志位为00,即为轻量级锁,所以向内存申请一个Monitor,让对象的MarkWord指向Monitor地址,并将ower指针指向线程1的地址,线程2放在等待队列里面,等线程1指向完毕,释放锁资源。

Monitor源码分析

环境搭建

我们去官网http://openjdk.java.net/找下open源码,也可以通过其他途径下载。源码是C实现的,可以通过DEV C++工具打开,效果如下图:

构造函数

我们先看下\hotspot\src\share\vm\runtime\ObjectMonitor.hpp,以.hpp结尾的文件是导入的一些包和一些声明,之后可以被.cpp文件导入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码 ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;//线程重入次数
_object = NULL;//存储该monitor的对象
_owner = NULL;//标识拥有该monitor的线程
_WaitSet = NULL;//处于wait状态的线程,会加入到_waitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//多线程竞争锁时的单项列表
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁lock状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}

锁竞争的过程

我们先看下\hotspot\src\share\vm\interpreter\interpreterRuntime.cppIRT_ENTRY_NO_ASYNC即为锁竞争过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
//是否使用偏向锁,可加参数进行设置 if (UseBiasedLocking) { //如果可以使用偏向锁,即进入fast_enter
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {//如果不可以使用偏向锁,即进行slow_enter
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

slow_enter实际上调用的ObjectMonitor.cpp的enter 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
复制代码void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;

//通过CAS操作尝试将monitor的_owner设置为当前线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
//如果设置不成功,直接返回
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
//如果_owner等于当前线程,重入数_recursions加1,直接返回
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}

//如果当前线程第一次进入该monitor,设置重入数_recursions为1,_owner为当前线程,返回
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}

//如果未抢到锁,则进行自旋优化,如果还未获取锁,则放入到list里面
// We've encountered genuine contention.
assert (Self->_Stalled == 0, "invariant") ;
Self->_Stalled = intptr_t(this) ;

// Try one round of spinning *before* enqueueing Self
// and before going through the awkward and expensive state
// transitions. The following spin is strictly optional ...
// Note that if we acquire the monitor from an initial spin
// we forgo posting JVMTI events and firing DTRACE probes.
if (Knob_SpinEarly && TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_recursions == 0 , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
Self->_Stalled = 0 ;
return ;
}

assert (_owner != Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (Self->is_Java_thread() , "invariant") ;
JavaThread * jt = (JavaThread *) Self ;
assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
assert (jt->thread_state() != _thread_blocked , "invariant") ;
assert (this->object() != NULL , "invariant") ;
assert (_count >= 0, "invariant") ;

// Prevent deflation at STW-time. See deflate_idle_monitors() and is_busy().
// Ensure the object-monitor relationship remains stable while there's contention.
Atomic::inc_ptr(&_count);

EventJavaMonitorEnter event;

{ // Change java thread status to indicate blocked on monitor enter.
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);

DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_enter()) {
JvmtiExport::post_monitor_contended_enter(jt, this);
}

OSThreadContendState osts(Self->osthread());
ThreadBlockInVM tbivm(jt);

Self->set_current_pending_monitor(this);

// TODO-FIXME: change the following for(;;) loop to straight-line code.
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()

EnterI (THREAD) ;

if (!ExitSuspendEquivalent(jt)) break ;

//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;

jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}

Atomic::dec_ptr(&_count);
assert (_count >= 0, "invariant") ;
Self->_Stalled = 0 ;

// Must either set _recursions = 0 or ASSERT _recursions == 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;

// The thread -- now the owner -- is back in vm mode.
// Report the glorious news via TI,DTrace and jvmstat.
// The probe effect is non-trivial. All the reportage occurs
// while we hold the monitor, increasing the length of the critical
// section. Amdahl's parallel speedup law comes vividly into play.
//
// Another option might be to aggregate the events (thread local or
// per-monitor aggregation) and defer reporting until a more opportune
// time -- such as next time some thread encounters contention but has
// yet to acquire the lock. While spinning that thread could
// spinning we could increment JVMStat counters, etc.

DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_entered()) {
JvmtiExport::post_monitor_contended_entered(jt, this);
}

if (event.should_commit()) {
event.set_klass(((oop)this->object())->klass());
event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
event.commit();
}

if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
}
}

白话翻译

同学A在使用自习教室的时候,同学B在同一时刻也想使用自习教室,那就发生了竞争关系。所以同学B在A运行过程中,加入等待队列。如果此时同学C也要使用该教室,也会加入等待队列。等同学A使用结束,同学B和C将竞争自习教室。

自旋优化

自旋优化比较简单,如果将其他线程加入等待队列,那之后唤醒并运行线程需要消耗资源,所以设计人员让其空转一会,看看线程能不能一会结束了,这样就不要在加入等待队列。

白话来说,如果同学A在使用自习教室,同学B可以回宿舍,等A使用结束再来,但是B回宿舍再来的过程需要1个小时,而A只要10分钟就结束了。所以B可以先不回宿舍,而是在门口等个10分钟,以防止来回时间的浪费。

结语

唉呀妈呀,终于结束了,累死了。终于将synchronized写完了,如果有不正确的地方,还需要各位指正。如果觉得写得还行,麻烦帮我点赞,评论哈。

求个关注

参考资料

Java中System.out.println()为何会影响内存可见性

别再问什么是Java内存模型了,看这里!

JVM—汇编指令集

Java中Synchronized的使用

synchronized同步方法(08)同步不具有继承性

Thread–synchronized不能被继承?!?!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%