1. 同步的语义
上面的内容摘自 JSR 133 FAQ:
Synchronization has several aspects. The most well-understood is mutual exclusion — only one thread can hold a monitor at once, so synchronizing on a monitor means that once one thread enters a synchronized block protected by a monitor, no other thread can enter a block protected by that monitor until the first thread exits the synchronized block.
同步有几个方面。最容易了解的是 互斥 —— 只有一个线程能够立刻持有一个监视器,因而在监视器上进行同步意味着一旦一个线程进入由一个监视器爱护的同步块,则其余线程都不能进入该监视器爱护的块,直到第一个线程退出同步块。
But there is more to synchronization than mutual exclusion. Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory. We will then be able to see all of the writes made visible by the previous release.
然而同步不仅仅是互斥。同步确保以可预感的形式,使线程在同步块之前或期间对内存的写入对于在同一监视器上同步的其余线程可见。 退出同步块后,咱们 开释 该监视器,其有将缓存刷新到主内存的成果,以便该线程进行的写入对于其余线程可见。在咱们进入一个同步块之前,咱们须要 获取 该监视器,该监视器具备使本地处理器缓存有效的作用,以便能够从主内存中从新加载变量。而后,咱们将可能看到以前开释中所有可见的写入。
Discussing this in terms of caches, it may sound as if these issues only affect multiprocessor machines. However, the reordering effects can be easily seen on a single processor. It is not possible, for example, for the compiler to move your code before an acquire or after a release. When we say that acquires and releases act on caches, we are using shorthand for a number of possible effects.
从高速缓存的角度进行探讨,听起来仿佛这些问题仅影响多处理器计算机。然而,重排序成果能够在单个处理器上轻松看到。例如,编译器不可能在获取之前或开释之后挪动代码。当咱们说获取和开释作用于缓存时,咱们应用简写来示意多种可能的影响。
The new memory model semantics create a partial ordering on memory operations (read field, write field, lock, unlock) and other thread operations (start and join), where some actions are said to happen before other operations. When one action happens before another, the first is guaranteed to be ordered before and visible to the second. The rules of this ordering are as follows:
新的内存模型语义在内存操作(读字段,写字段,锁定,解锁)和其余线程操作(start 和 join)上创立了局部排序,其中某些操作据说 happen before 其余操作。当一个动作在另一个动作之前产生时,第一个动作被确保排序在第二个动作之前并且对于第二个动作可见。此排序规定如下:
- Each action in a thread happens before every action in that thread that comes later in the program’s order.
线程中的每个动作先于该线程中的在程序程序上后呈现的每个动作产生。
- An unlock on a monitor happens before every subsequent lock on that same monitor.
监视器上的一个解锁产生在 同一个 监视器上的每个后续锁定之前。
- A write to a volatile field happens before every subsequent read of that same volatile.
对 volatile 字段的每个写操作产生在每次后续读取 同一个 volatile 之前。
- A call to start() on a thread happens before any actions in the started thread.
一个对线程的 start() 的调用产生在被启动线程中的任何操作之前。
- All actions in a thread happen before any other thread successfully returns from a join() on that thread.
线程中的所有操作产生在其余线程胜利从该线程上的 join() 返回之前。
This means that any memory operations which were visible to a thread before exiting a synchronized block are visible to any thread after it enters a synchronized block protected by the same monitor, since all the memory operations happen before the release, and the release happens before the acquire.
这意味着线程在退出同步块之前对一个线程可见的任何内存操作,在进入受同一监视器爱护的同步块之后对于任何线程都是可见的,因为所有内存操作都产生在开释之前,而开释产生在获取之前。
能够看到同步的语义蕴含两点:一个是互斥,一个是保障可见性。
2 synchronized 的根本应用
依据 Java 语言标准可知:
Java 外面的每个对象都关联着一个 monitor,一个线程能够 lock 或者 unlock 这个 monitor。
- 对于一个类办法,该办法所在类的 Class 对象关联的 monitor 被应用。
- 对于一个实例办法,与 this(某个调用该办法的实例对象)关联的 monitor 被应用。
- 对于一个同步块,即 synchronized(obj){….}, 与 obj 关联的 monitor 被应用。
举个栗子:
package synchronizedTest;
class Test {
int count;
// 实例同步办法
synchronized void bump() {count++;}
static int classCount;
// 类同步办法
static synchronized void classBump() {classCount++;}
}
下面的代码等价于:
package synchronizedTest;
class BumpTest {
int count;
void bump() {
// 同步块
synchronized (this) {count++;}
}
static int classCount;
static void classBump() {
try {
// 同步块
synchronized (Class.forName("BumpTest")) {classCount++;}
} catch (ClassNotFoundException e) {}}
}
3. 从 JVM 字节码层面看同步块
反解析下下面两个类对应的字节码文件。
编译成 class 文件
javac synchronizedTest/Test.java
将 Class 文件反汇编下
javap -p -v synchronizedTest/Test > synchronizedTest/Test.disasm
相似地:
javac synchronizedTest/BumpTest.java
javap -p -v synchronizedTest/BumpTest > synchronizedTest/BumpTest.disasm
上面重点看下 Test.disasm 和 BumpTest.disasm
BumpTest.bump 办法节选
void bump();
descriptor: ()V
flags:
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field count:I
9: iconst_1
10: iadd
11: putfield #2 // Field count:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 6: 0
line 7: 24
3.1 monitor_enter 的阐明:lock 特定对象的 monitor。
The objectref must be of type reference.
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
objectref 必须是援用类型。
每个对象都与一个监视器关联。监视器只有在领有所有者的状况下才被锁定。执行 monitorenter 的线程尝试取得与 objectref 关联的监视器的所有权,如下所示:
如果与 objectref 关联的监视器的条目计数为零,则线程进入监视器并将其条目计数设置为 1。而后,该线程是监视器的所有者。
如果线程曾经领有与 objectref 关联的监视器,则它将从新进入监视器,从而条目计数加 1。
如果另一个线程曾经领有与 objectref 关联的监视器,则该线程将阻塞,直到该监视器的条目计数为零为止,而后再次尝试获取所有权。
3.2 monitor_exit 的阐明:unlock 特定对象的 monitor。
The objectref must be of type reference.
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
objectref 必须是援用类型。
执行 monitorexit 的线程必须是与 objectref 援用的实例相关联的监视器的所有者。
该线程缩小与 objectref 关联的监视器的条目计数。后果,如果条目计数的值为零,则线程退出监视器,并且不再是其所有者。其余被阻塞进入监视器的线程能够尝试进入监视器。
3.3 上面是对于异样表 (Exception table) 的阐明:
下面是 bump 办法对应的指令,异样表有两行(如下所示),每一行称为异样表条目:
4 16 19 any
19 22 19 any
每个异样表条目监控 [from, to) 的字节码,如果出现异常,则跳转到 target 指针对应的字节码执行,type 则代表该处理器所能捕捉的异样类型(any 代表任何异样)。
对应的下面两个异样条目标意思就是:
- 4 对应 from,16 对应 to, 19 对应 target,any 对应 type;也就是 [4,16) 指向的字节码指令抛任何异样(any)了,都会跳转到 19 执行。
- 19 对应 from,22 对应 to, 19 对应 target,any 对应 type;也就是 [19, 22) 指向的字节码抛出任何异样(any)了,都会跳转到 19 执行。
也就是
- 状况一:[4,16)执行没有任何异样,则 goto 到 24,返回。在这种状况下失常加锁
3: monitorenter
,开释锁15: monitorexit
- 状况二:[4,16)抛出任何异样,都跳转 19,都会执行到
21: monitorexit
;如果胜利了,则异样完结;如果在 [19, 22) 执行中抛出任何异样,就跳转到 19 再从新执行一遍。
通过下面的剖析,咱们能够发现,不论是否抛出异样,synchronized 同步块,都会开释之前获取的锁,也就是 monitorenter 与 monitorexit 始终是成对呈现的。
BumpTest.classBump 和 BumpTest.bump 是相似,你能够本人尝试剖析下。
4. 从 JVM 字节码层面看同步办法
Test.bump 节选
synchronized void bump();
descriptor: ()V
flags: ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return
LineNumberTable:
line 6: 0
line 7: 10
由 Java 虚拟机标准可知
Monitor entry on invocation of a synchronized method, and monitor exit on its return, are handled implicitly by the Java Virtual Machine’s method invocation and return instructions, as if monitorenter and monitorexit were used.
Java 虚拟机的办法调用和返回指令,隐式解决了调用同步办法时的 monitor entry 和返回时的 monitor exit,就像应用了 monitorenter 和 monitorexit 一样。
所以,同步办法和同步块的实现形式实质上并没有什么不同。
5. 从机器码看 synchronized
如果咱们把 BumpTest 改成如下代码:
package synchronizedTest;
class BumpTest {
int count;
void bump() {synchronized (this) {count++;}
}
static int classCount;
static void classBump() {
try {synchronized (Class.forName("BumpTest")) {classCount++;}
} catch (ClassNotFoundException e) {}}
public static void main(String[] args){for(int i=0; i< 100000; i++){classBump();
}
System.out.println(classCount);
}
}
而后从新编译成字节码文件,再失去本地机器指令文件(留神,执行第二条指令还须要 hsdis, 你能够参考我之前的文章装置)
javac synchronizedTest/BumpTest.java
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly synchronizedTest/BumpTest > synchronizedTest/BumpTest.native
而后咱们搜寻 'classBump'
,发现了上面的机器指令,能够看到在 Intel64 CPU 下,synchronized 最终还是用 lock cmpxchg 实现的。
从这里咱们能够发现,这里的 synchronized 和之前讲的 volatile,Unsafe 中的 CAS,刚好是用相似的原子指令(比方这里的 lock cmpxchg)实现的。
至于 BumpTest 和 Test 中其余同步办法或同步块,你能够试一下,后果是统一的。
6. 同步
然而,并不是所有的同步都是用上述的原子指令实现的(其实是轻量级锁),而是依据不同状况应用不同的锁,锁的类型分为重量级锁,轻量级锁,偏差锁。 上面次要简略地阐明这三种锁的实现。
HotSpot JVM 应用一个 two-word 对象头,第一个 word 是 Class pointer,第二个是 Mark word 用来保留同步,GC 和 hashCode 等相干信息。Mark word 应用形式见下图:
6.1 重量级锁 heavyweight monitor
重量级锁对应上述 Mark word 的 tag bits 为 10 的状况,即此时状态为 inflated。
重量级锁会应用操作系统级别的锁定原语 (OS-level locking primitives,比方 pthread mutex) 来实现。 这些操作将波及零碎调用,须要从操作系统的用户态切换至内核态,其开销十分之大。重量级锁能够在所有场景应用。
6.2 轻量级锁 lightweight lock
轻量级锁对应上述 Mark word 的 tag bits 为 00 的状况,即此时状态为 lightweight-locked。
轻量级锁是对重量级锁的优化。轻量级锁应用一个或两个 CPU 级别的原子指令(比方 lock cmpxchg),从而防止了应用操作系统级别的锁定原语。
可选的轻量级锁实现算法有 Metalock (CAS in both acquire and release), Thin Locks(CAS in acquire), 和 Relaxed-locks (CAS in acquire).
但轻量级锁适用范围无限,以 Thin Locks 为例子,它实用于这样的对象,这些对象不被争用,不须要对本人执行 wait,notify 或 notifyAll 操作,并且没有锁定到过多的嵌套深度。绝大多数对象都满足上述条件;那些不满足条件的对象的锁要用重量级锁来实现。
6.3 偏差锁 biased lock
偏差锁在 JDK6 引入,对应上述 Mark word 的 tag bits 为 01 的状况,即此时状态为 unlocked 或 biasable。
偏差锁是对轻量级锁的再优化,尝试在 acquire 和 release 中防止原子指令, 仅在第一次获取时执行一次原子指令,以将锁定线程 ID 装置到 mark word 中。
但偏差锁的适用范围绝对轻量级锁来说更加无限,偏差锁实用于单个过程重复获取并开释锁,而其余过程很少拜访该锁的状况,即大多数对象在其生命周期中最多只能被一个线程锁定的状况。
更多对于偏差锁的常识,请见 Quickly Reacquirable Locks 和 Biased Locking in HotSpot
6.4 锁的转换
在 HotSpot JVM 中,依照偏差锁,轻量级锁,重量级锁的程序来尝试获取对象的锁。残缺流程见下图:
偏差锁相干流程(对应上图中以 1 结尾的 ):
如果新调配的对象 O 是可偏差的但未被偏差(对应上图中 1 ),那么第一次锁定的时候应用 CAS 在 mark word 中插入线程 T1 的 ID(对应上图中 1 -1),而接下来的锁定仅仅将 mark word 外面的 线程 T1 的 ID 与 以后线程 T2 的比拟,此时可能呈现两种状况:
- 状况 1:如果线程 ID 一样,则表明对象 O 已偏差以后线程 T2,也就是以后线程 T2 曾经锁定对象 O,能够无需 CAS 即可 lock/unlock (对应图中 1 -2)
- 状况 2:如果线程 ID 不一样,则撤销对 T1 的偏差, 并须要查看对象 O 是否能够重偏差。如果能够重偏差,则将对象 O 重偏差到线程 T2(对应上图中 1 -3);否则将撤销偏差并回退到失常锁定流程(对应上图中以 2 结尾的),尔后对象 O 对应的类不能够再被偏差锁定。
轻量级锁和重量级锁流程(对应上图中以 2 结尾的 ):
如果新调配的对象 O 对应的类不可偏差,则先尝试通过 CAS 设置 mark word 来获取轻量级锁。如果胜利,则获取轻量级锁;如果失败,则先判断是否是递归锁定,如果是则表明曾经获取锁,如果不是则收缩为重量级锁。
轻量级锁定时,每次进入同步办法,都会在栈帧中生成一个新的 lock record(锁记录),该锁记录有两个字段 displaced hdr 和 owner,displaced hdr 用来保留锁对象的对象头 mark word,owner 用来保留指向锁对象的指针。另外,Lock record 出于内存对齐的要求,会确保 lock record 的存储地址最初两位为 00,这两位刚好用来作为轻量级锁的标识。
轻量级锁定:尝试将 lock record 的 displaced hdr 用来保留锁对象原来的 mark word;将 lock record 的 owner 指向锁对象;将锁对象原来的 mark word 替换为指向 lock record 的指针;这三步都会在同一个 CAS 原子地进行尝试。
如果 CAS 胜利,则表明获取轻量级锁胜利,也就是下图所示的状况
如果 CAS 失败,则分为递归锁定和须要收缩到重量级锁两种状况解决。
虚拟机首先测试对象的 mark word 是否指向以后线程的办法栈。
- 如果是,则表明是递归锁定,以后线程曾经领有对象的锁,能够平安地继续执行它。对于这种递归锁定的对象,将 lock record 初始化为 0 而不是对象的 mark word。(对应上图中的 2 -2)
- 如果不是,则表明存在两个不同的线程同时在同一个对象上同步,这时须要将轻量级锁收缩到重量级锁,也就是将指向 heavy monitor 的指针赋值给 对象的 mark word(对应上图中的 2 -3)
下面只是对于同步流程的局部总结,对于同步更全面的介绍,请参见 Sun 的 Eliminating Synchronization Related Atomic Operations with Biased Locking and Bulk Rebiasing 和 Synchronization in Java SE 6(HotSpot),以及 Synchronization(我翻译的 Synchronization 中英对照版,还有 Java 虚拟机是怎么实现 synchronized 的?,以及在 bytecodeInterpreter.cpp 搜寻Lock method if synchronized
。
7. 总结
这篇文章首先对 synchronized 的根本应用进行了温习;而后尝试从字节码和本地机器码的角度上看 synchronized 的实现;最初通过查看官网文档弄清 synchronized 的实现会别离尝试偏差锁(尝试防止原子指令,仅第一次的时候须要应用原子指令,以将锁定线程的 ID 装置到 header word 中),轻量级锁(在锁定和解锁中应用 一个或两个 CPU-level 的原子指令),重量级锁(操作系统级调用),这三种锁实现适用范围越来越大,但代价也越来越大。
其实 synchronized 如何实现对于个别人是无感的,这也是为什么每次 JDK 公布都可能会改善它的性能,咱们要做的基本上是依据 JDK 版本了解对应的实现,而后调整一下 相应的 JVM 参数。
8. 参考
1.Java 语言标准第八版第 17 章
2.Java 虚拟机标准第八版
3.https://www.cs.umd.edu/~pugh/…
4.https://time.geekbang.org/col…
5.https://blogs.oracle.com/dave…
6.https://wiki.openjdk.java.net…
7.https://stackoverflow.com/que…
8.https://www.zhihu.com/questio…