基本
synchronized,俗称对象锁。java 中互斥和同步都可以采用 synchronized 关键字来完成。
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行完或是使用wait/notify。
synchronized(对象) // 线程1, 线程2(blocked)
{
//临界区
}
方法上的synchronized
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
synchronized原理
对象头
对象头 主要存放 MarkWord,MarkWord里存储了对象的hashcode 以及锁信息等。除了MarkWord 对象头里还存放类的元信息--Class对象的指针(内存地址)。如果数组的话,还会再存储下数组的长度。
实例数据 实例数据部分是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。包括从父类继承来的,都会记录下来。
对齐填充 HotSpot要求对象的起止地址(姑且认为是对象大小)必须是8的整数倍,对象头部分正好是8的倍数,因此当实例数据部分不是8的倍数的话就需要填充了
字节码
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
//对应的字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object; lock的引用
3: dup //复制lock
4: astore_1 // lock引用 -> slot 1 (解锁时候用)
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // Field counter:I
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // Field counter:I
14: aload_1 //从slot 1 获取lock引用
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24 //跳转到24行
19: astore_2 // e -> slot 2 异常对象存入slot2
20: aload_1 // 从slot 1 获取lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // 获取slot 2 (e)
23: athrow // throw e
24: return //返回
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 15: 0
line 16: 6
line 17: 14
line 18: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
Synchronized 锁的管理就是依托于Monitor,当线程owner Monitor的时候则拥有进行同步操作的权利,线程进入同步块调用 monitorenter指令,退出同步块则调用 monitorexit,释放对Monitor的持有。
Monitor:对象的监视器,只要发生同步操作,线程就为当前对象创建一个Monitor对象与之关联,Monitor只能被一个线程持有,此时当前对象就处于锁定状态,其它线程只能阻塞等待。
Java虚拟机(HotSpot)中,Monitor是通过ObjectMonitor实现的(c++),里面有三个重要的属性
ObjectMonitor() {
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_owner = NULL;
}
synchronized 锁升级的流程
偏向锁
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现。 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
偏向锁的获取和撤销逻辑
1.首先获取锁对象的Markword,判断是否处于可偏向的状态( biased_lock:1&&ThreadId 为空)
2.如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID 写入到 MarkWord a) 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码 块 b) 如果 cas 失败,说明有其他线程已经获得了偏向锁, 这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行。
3.如果是已偏向状态,需要检查Markword中存储的ThreadID 是否等于当前线程的ThreadID)。如果相等不需要获取锁直接执行同步代码块。如果不相等,进行CAS替换,成功执行同步代码块,不成功说明当前锁偏向其他线程,需要撤销偏向锁升级到轻量级锁。
撤销
轻量锁
轻量级锁的加锁
升级为轻量级锁的过程:
- 线程在自己的栈桢中创建锁记录 LockRecord。
- 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
- 将锁记录中的 Owner 指针指向锁对象。
- 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。
自旋锁
轻量级锁在加锁过程中,用到了自旋锁所谓自旋,就是指当有另外一个线程来竞争锁时,这个线 程会在原地循环等待,而不是把该线程给阻塞,直到那个 获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于 在执行一个啥也没有的 for 循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样线程原地等待很短的时间就能够获得锁了。 自旋锁的使用,其实也是有一定的概率背景,在大部分同 步代码块执行的时间都是很短的。所以通过看似无异议的 循环反而能提升锁的性能。 但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而 会消耗 CPU 资源。默认情况下自旋的次数是 10 次, 可以通过 preBlockSpin 来修改。
轻量级锁的解锁
轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过 CAS 操作把线程栈帧中的LockRecord 替换回到锁对象的 MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁。
重量锁
操作系统的管程,查看上面的字节码。加了同步代码块以后,在字节码中会看到一个monitorenter和monitorexit。每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到 synchronized修饰的对象对应的monitor。