synchronized

Java 中对于锁的一种实现方法,可用于修饰方法、代码块。synchronized 理论上可解决所有的三大并发问题:原子性可见性有序性

synchronized 提供的是互斥锁,也是可重入锁,一个线程可以多次获取一个锁。

class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object()
  void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}  

加锁对象:

  • 当修饰静态方法的时候,synchronized 锁定的是当前类的 Class 对象
  • 当修饰非静态方法的时候,锁定的是当前的实例对象 this

不需要synchronized的原子操作:

  • 对基本类型赋值
  • 对引用类型赋值

JDK 1.6 synchronized 锁升级

synchronized 是基于操作系统底层实现的,在使用过程中频繁地切换用户态和内核态,增加了性能开销。

为提升性能,在 JDK 1.6 中引入了偏向锁、轻量级锁、重量级锁,减少锁竞争时带来的上下文开销。同时借助 JDK 1.6 中提供的对象头,实现了锁升级的功能。

在 JDK 1.6 的 JVM 中,对象实例在堆内存中被分为三部分:对象头、实例数据、对齐填充。其中,对象头包括以下几类信息:

  • Mark Word
  • 指向类的指针
  • 数组长度(可选)

Mark Word中记录了对象与锁的相关信息,JDK1.6需要依靠Mark Word中记录的锁标志位和是否偏向锁标志位实现锁升级。

偏向锁

偏向锁适用场景:一个线程多次获取同一个锁。 当加锁时,首先锁会变为偏向锁,在锁对象的对象头中记录抢到锁的线程 ID。

当该线程再次申请偏向锁时,检查线程 ID 通过,就不会进行加锁和解锁的操作。如果线程不一致,检查原线程是否存活:

  • 原线程不存活:取消原偏向锁,创建一个新偏向锁指向新线程
  • 原线程存活:检查原线程栈帧中的信息
    • 原线程还需持有锁:暂停原线程(Stop The World),撤销偏向锁,升级为轻量级锁
    • 原线程不需要持有锁:取消原偏向锁,创建一个新偏向锁指向新线程

轻量级锁

轻量级锁的适用场景:线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争。

当偏向锁升级为轻量级的锁的时候

  1. 将原对象头的 Mark Word 信息复制一份到栈帧中
  2. Mark Word 指向栈帧中的 Mark Word 信息地址。 这样做的主要目的是为了保留现场。

在线程1复制对象头 Mark Word 的同时(CAS之前,也就是步骤2之前),线程2也准备获取锁,也复制了对象头 Mark Word。在线程2进行 CAS 时,发现线程1已经把对象头换了,线程2的 CAS 失败,线程2会尝试使用自旋锁来等待线程1释放锁。

当自旋锁循环达到一定次数后,轻量级锁会升级会重量级锁。 使用自旋锁的目的:避免线程马上被阻塞,影响性能。

轻量级锁解锁时,会使用 CAS 将之前复制在栈桢中的 Displaced Mard Word 替换回 Mark Word 中。如果替换成功,则说明整个过程都成功执行,期间没有其他线程访问同步代码块。

重量级锁

在重量级锁下,所有获取锁失败的线程都会进入 Monitor,进入阻塞队列。

锁粗化

把多个锁请求合并成一个,降低锁请求、同步、释放带来的性能释放。

案例一:

public void doSomethingMethod() {
    synchronized(lock) {
        //do some thing
    }
    //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
    synchronized(lock) {
        //do other thing
    }
}
 
// 锁粗化:
public void doSomethingMethod() {
    //进行锁粗化:整合成一次锁请求、同步、释放
    synchronized(lock) { 
       //do some thing 
       //做其它不需要同步但能很快执行完的工作
        //do other thing
    }
}

案例二:

for(int i = 0; i < size; i++) {
    synchronized(lock) {
    }
}
 
// 锁粗化
synchronized(lock){
    for(int i = 0; i < size; i++) {
    }
}

锁消除

编译器级别的锁优化。虚拟机即时编译器在运行时,对一些在代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数据支持,

需要将 Java 设置为 server 模式,并开启逃逸分析。