大家好,缓缓来迟的第 5 篇 并发内容,其实准备了好久了,因为想写点不一样的内容,结果导致托了一个月才准备好。在开始正文之前,继续来我们的几个灵魂问题:
今天是第五篇- synchronized 的原理讲解。
首先,在多线程编程中,我们一般处理的就是两个关键问题:
其中线程通信指的是线程之间共享程序的公共状态,通过写 - 读这个公共状态来隐式通信。
线程同步则是不同线程间的操作发生的相对顺序。
而 Java 的并发采用的是 共享内存模型,Java 线程通信总是 隐式 进行的。当然,各种面试问题虽然没有直面的问 内存模型的问题,但是我们知道了这个,对我们了解整个并发模型的帮助是很大的。这里我们不再叙述。
说到 synchronized,大家的第一反应就是 锁 , 它可以修饰类的实例方法,静态方法,和代码块,我们将分别介绍下。
public class CountTest {
private int count;
public synchronized void addCount(){
count ++;
}
public synchronized int getCount() {
return count;
}
}
这是一个简单的计数器类,其中 addCount 方法和 getCount 方法 分别加了 synchronized 做修饰。我们来看它的 class 文件
public synchronized void addCount();
descriptor: ()V
flags: ACC_PUBLIC, 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
public synchronized int getCount();
descriptor: ()I
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field count:I
4: ireturn
LineNumberTable:
line 10: 0
这里我主要节选了被 synchronized 修饰的两个方法,我们可以看出,每个方法都额外的添加了一个 flags:
ACC_SYNCHRONIZED。这个 flags 在 未修饰的方法中是没有的,如下:
public void addCount() {
count++;
}
--- class 文件 ----
public void addCount();
descriptor: ()V
flags: ACC_PUBLIC
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
ACC_SYNCHRONIZED:方法是否为 synchronized
也就是说,jvm 是通过看这个 标志来加入同步功能的,
public class CountTest {
private static int count;
public static synchronized void addCount(){
count ++;
}
public static synchronized int getCount() {
return count;
}
}
public static synchronized void addCount();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field count:I
3: iconst_1
4: iadd
5: putstatic #2 // Field count:I
8: return
LineNumberTable:
line 6: 0
line 7: 8
public static synchronized int getCount();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field count:I
3: ireturn
LineNumberTable:
line 10: 0
}
可以看出,synchronized 修饰静态方法,只是在 flags 加了个 ACC_STATIC 表面是静态的,并没有其他区别。
在 Java 8 的 jvm描述中,有以下内容:
The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor. Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8). A
synchronized
method is distinguished in the run-time constant pool'smethod_info
structure (§4.6) by theACC_SYNCHRONIZED
flag, which is checked by the method invocation instructions. When invoking a method for whichACC_SYNCHRONIZED
is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of thesynchronized
method and thesynchronized
method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of thesynchronized
method.其中主要表述了,jvm 是通过监视器 monitor 来做同步的,方法级别的同步是隐式执行的,synchronized 方法是在运行时间常量池中的区分 method_info 结构,ACC_SYNCHRONIZED标志 由方法调用指令进行检查。调用方法时 如果 ACC_SYNCHRONIZED 被设置,则执行的线程进入监视器,调用方法本身,然后再退出监视器,在执行线程有监视器期间,没有线程可以进入,如果在调用 synchronized 方法期间抛出异常并且synchronized方法未处理异常,则在从方法重新抛出异常之前,将自动退出该方法的监视器synchronized。 这里的监视器也就是 我们计算机结构中的 - 管程。
synchronized 修饰方法,不管是静态方法还是非静态方法,锁的都是对象,只不过对于实例方法,修饰的是当前实例对象 this,对于静态方法则是修饰的是当前 class 对象,即 CountTest.class。注意:静态和非静态修饰的是不同的对象,不同的两个线程,可以一个执行 静态方法,另一个执行实例方法。
public class CountTest {
private int count;
public synchronized void addCount() {
synchronized (this) {
count++;
}
}
public synchronized int getCount() {
synchronized (this) {
return count;
}
}
}
public synchronized void addCount();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
############################ monitor enter
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
############################ monitor exit
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: 4
line 8: 14
line 9: 24
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class CountTest, class java/lang/Object ] 锁对象
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
在修饰代码块的时候,其实就是加了一个监控锁,我们在class 文件中已经标出,即:
3: monitorenter
{其他操作}
15: monitorexit
而锁的对象就是
locals = [ class CountTest, class java/lang/Object ]
也就是我们的 this,所以可以看出,synchronized 修饰 代码块的时候 和修饰方法的差别还是蛮大的。我们下面看下 jvm 关于具体监视器的描述。
### monitorenter A monitorenter instruction may be used with one or more monitorexit instructions (§monitorexit) to implement a
synchronized
statement in the Java programming language (§3.14). The monitorenter and monitorexit instructions are not used in the implementation ofsynchronized
methods, although they can be used to provide equivalent locking semantics. Monitor entry on invocation of asynchronized
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.The synchronization constructs of the Java programming language require support for operations on monitors besides entry and exit. These include waiting on a monitor (
Object.wait
) and notifying other threads waiting on a monitor (Object.notifyAll
andObject.notify
). These operations are supported in the standard packagejava.lang
supplied with the Java Virtual Machine. No explicit support for these operations appears in the instruction set of the Java Virtual Machine.一个monitorenter指令可以与一个或多个被用来 monitorexit指令(§ monitorexit)来实现 synchronizedJava编程语言。该monitorenter和 monitorexit指令不执行使用 synchronized方法,但它们可以被用来提供相当于锁定语义。监视方法进入和返回时的监视器退出由Java虚拟机的方法调用和返回指令隐式处理,就像使用了monitorenter和monitorexit一样。 除了进入和退出之外,Java编程语言的同步构造还需要支持监视器上的操作。这些包括等待监视器(Object.wait)并通知在监视器上等待的其他线程(Object.notifyAll 和Object.notify)。java.lang Java虚拟机随附的标准软件包支持这些操作。Java虚拟机的指令集中不显示对这些操作的明确支持。
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:
每个对象都有一个 monitor 关联,当某个monitor 被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。步骤如下:
### monitorexit 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的类型必须是
reference
引用。 执行 monitorexit 的线程必须是与 objectref 引用的实例关联的监视器的所有者 。 与objectref关联的监视器的条目计数减一。如果因此条目计数的值为零,则线程退出监视器并且不再是其所有者。其他线程将继续抢占获取监视器。
这个 monitor 对象 指针是一个 ObjectMonitor 对象,所有线程加入它的 entrylist 里面,去 CAS 抢锁,成功后 更改计数 state,执行完毕后再 state 减 1,只是所有线程并不阻塞。
synchronized 中的锁是存在对象头中的,Hotspot 虚拟机把对象头分为了 Mark Word(标记字段)、Klass Pointer(类型指针)。 Mark Word 用于存储自身的运行时数据,而 Klass Pointer 则是对象指向它的l类元数据的指针。
如果对象是数组类型,则用 3 个字宽 存储对象头,如果是非数组类型,则用 2 个字宽存储对象头,在 32 为 jvm 中,用 1 字宽代表 4 字节,即 32 bit。
Mark Word 默认存储对象的 HashCode、分代年龄和锁 标记位,其中我们关注的 synchronized 中的锁 就是在这个标记位。
32位 jvm 对象头内存结构
25 bit | 4 bit | 1 bit 是否偏向锁 | 2 bit 锁标记位 |
---|---|---|---|
对象的 hashcode | 对象分代年龄 | 0 | 01 |
64 位 jvm 对象头内存结构
25 bit | 31 bit | 1 bit | 4 bit | 1 bit是否偏向锁 | 2 bit 锁标记位 |
---|---|---|---|---|---|
unsed | 对象的 hashcode | cms_free | 对象分代年龄 | 0 | 01 |
其中的锁标记位如下:
锁状态 | 1 bit是否偏向锁 | 2 bit 锁标记位 |
---|---|---|
轻量级锁 | 0 | 00 |
重量级锁 | 0 | 10 |
GC 标记 | 0 | 11 |
偏向锁 | 1 | 01 |
看完了上面的关于 monitor 的介绍,是不是对 synchronized 有了更深入的理解呢,我在这里要提几句注意的地方:
虽然 synchronized 可以锁一个 Class ,但是一旦锁上,那么这个类的其他操作也将进行抢占锁处理,比如我们常常说到的用户转账操作,锁住了 用户.class,就像电影院中的包场,只要与用户有关的操作统统串行。所以我们就要处理细粒度的锁了。
synchronized 之所以是可重入的,官方 doc 已经给了我们解释,锁方法的时候是 jvm 做了处理,锁方法块的时候则是监视器做的处理,其中监视器就是 更改 state 来判断重入的。重入我们将在后面的 显示锁中,给大家明确提出。
synchronized 不要锁字符串对象、Integer对象等,因为jvm 在处理这些对象的时候,是有缓存机制的,指不定你锁的你家的锁,隔壁老王拿的你家钥匙。老老实实 new 一个自己的 object 对象多好。
最后,我对 synchronized的理解是这样,不改变CPU 时间片切换,当其他线程要访问某资源时,发现锁还未释放,所以只能在外面等待。
下篇我们将继续锁的研究。