synchronized原理

synchronized关键字是jdk提供用于实现内部锁。

synchronized实现机制

synchronized代码块通过加锁的方式,只有获取到锁的线程才可以进去该代码块。使多个线程排队运行该代码块,保证代码块的原子性(对其他线程来说。只能看到别的线程还未执行或执行完毕的状态)。

monitor
  1. Monitor对象是jvm实现的,c++中的对象。
  2. 每个对象内部都有一个唯一的monitor,monitor的获取是互斥的。
  3. synchronized通过monitor来实现互斥。
  4. monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
javap反汇编分析

通过javap将下列代码反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SynchronizedDemo {
private int i=1;

public void test1(){
synchronized (this){
i++;
}
}
public synchronized void test2(){
i++;
}

}

test1用synchronized代码块,通过monitorenter获取锁,执行完后用monitorexit释放锁。
image

test2是synchronized方法,jvm通过ACC_SYNCHRONIZED标志,内部实现。
image

无论是monitorenter/monitorexit还是ACC_SYNCHRONIZED,最终都是通过获取锁对象的monitor实现互斥。进入synchronized代码块前会通过monitorenter来获取monitor对象。获取失败的线程进入同步队列中等待。获取monitor对象的线程执行完后通过monitorexit指令来释放锁。释放锁会通知同步队列中阻塞的线程出队列再次申请锁。
image

Synchronized 1.6的优化

锁的强度分四种级别,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。他们会随着竞争的激烈而逐渐升级。

锁的相关信息存在对象的对象头的Mark Word:
Mark Word是hotSpot虚拟机的对象的对象头中的一部分,用于存储对象滋生运行时数据,如hashcode,GC分代年龄、锁状态标志,线程持有锁,偏向线程id,偏向时间戳等信息。

Mark Word结构
image

适应自旋锁

线程的阻塞和唤醒需要cpu从用户态切换成内核态,给cpu造成很大的负担。而往往一个线程从阻塞到唤醒只经历很短的一段时间,所以引入自旋锁。通过无意义的循环进行等待锁释放,而不会立刻进入阻塞,这就是自旋。因为自旋是会消耗cpu的,所以要有个自旋次数限制,达到自旋次数还未获取锁就进入阻塞。适应自旋锁会根据自旋获取锁的成功率来调整自旋次数,如果获取锁成功率高会调高自旋次数,否则反之。

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是JIT编译器借助逃逸分析技术分析锁对象是否只能给一个线程访问而没发布到其他线程,锁消除就是在JIT生成动态字节码时消除moniterenter(申请锁)和moniterexit(释放锁)两个字节码指令。

注意:锁消除并不意味着可以随意加锁,JIT只会对执行频率足够多的地方进行优化。

锁粗化

使用锁时,我们会尽力将锁的范围缩小,只在操作共享变量时同步以减小锁竞争的范围。但是如果一系列的连续加锁解锁操作,频繁的获取释放锁可能会导致不必要的性能损耗,所以就是将多个连续的加锁、解锁操作(锁对象相同)连接在一起,扩展成一个范围更大的锁,这就是锁粗化。

偏向锁

大部分锁并没有被争用,且在其生命周期内也许至多被一个线程持有。所以一个内部锁第一次被获取时,会将Mark Word的偏向线程设为获取锁的线程,是否偏向锁标志设为1。线程只要判断偏向线程是否是当前线程,是则说明当前线程获取锁了。否则会通过cas设置Mark work。失败的话,则说明存在争用,撤销偏向锁,锁升级为轻量锁

偏向锁获取和撤销流程
  1. 获取对象头的Mark Word;
  2. 判断Mark Word偏向锁标志位是否为1,锁标志位为 01。否,则cas竞争锁。是的话,该锁是偏向锁进入(3);
  3. 判断Mark Work中的线程ID是否设置,没设置则进入步骤(4);如果指向当前线程,则执行同步代码块;如果指向其它线程,进入步骤(5);
  4. 通过CAS原子指令设置Mark Word的线程ID为当前线程ID,如果执行CAS成功,则执行同步代码块,否则进入步骤(5);
  5. 如果执行CAS失败,表示当前存在多个线程竞争锁,当达到全局安全点(safepoint),获得偏向锁的线程被挂起,撤销偏向锁,并升级为轻量级,升级完成后被阻塞在安全点的线程继续执行同步代码块;(偏向锁会一直被持有Mark Word中的ThreadId一直指向获取锁的线程,直到其他线程来竞争锁)

image

轻量级锁

引入轻量级锁的主要目的是在没有多线程竞争的前提下,通过cas减少重量锁的使用。轻量锁依据是大部分锁再同步周期内不存在竞争。

轻量锁获取和撤销流程
  1. jvm会在进入同步块前会在当前线程的栈帧创建用于存储锁记录的空间。
  2. 轻量锁进入同步代码块前会将mark work复制到锁记录中,通过cas将对象头中的Mark Word替换为指向锁记录的指针。cas成功则获取锁,将锁标志位改成00表示轻量锁。失败则尝试自旋获取锁。
  3. 在执行完代码块释放锁时会通过cas将Mark Word替换回对象头。cas成功则释放锁。如果失败则说明同步周期内存在竞争,锁升级为重量锁。

因为自旋会消耗cpu,所以轻量锁一旦升级为重量锁就不会恢复。
image

重量锁

重量锁就是通过Monitor实现的互斥锁。monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。