synchronized锁机制

  • synchronized是Java中一个关键字,主要用来保证多线程环境下对共享资源的互斥访问。它可以用来修饰方法或代码块,从而保证线程安全。synchronized锁机制由JVM实现,具有可重入性和自动释放锁的特性。synchronized保证了原子性、可见性和有序性。实现方便。

1. 基本作用

  • 当一个方法或者代码块被synchronized修饰时,他会成为一个临界区,同一时刻只有一个线程可以访问,其他线程必须等待该线程退出临界区后才能进入。

2. 使用方式

  • 修饰实例方法时,锁对象是当前实例(this

    1
    2
    3
    public synchronized void instanceMethod() {
    // 临界区代码
    }
  • 修饰静态方法时,锁对象是类的Class对象

    1
    2
    3
    public static synchronized void staticMethod() {
    // 临界区代码
    }
  • 修饰代码块时,只有该代码块被锁定。这样做可以选择性的锁定对象的一部分,而不是整个方法

    1
    2
    3
    4
    5
    public void method() {
    synchronized(this) {
    // 临界区代码
    }
    }

3. 特性

  • 可重入性:同一个线程可以多次获取同一个锁。
  • 锁的可见性:synchronized保证了进入临界区的线程对共享变量的修改对其他线程是可见的。这是因为synchronized在释放锁时会将本地内存中的变量刷新到主内存。

4. 锁优化

  • synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。 到了Java1.6synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的Java1.71.8中,均对该关键字的实现机理做了优化。
  • JVM对synchronized的优化
    • 锁粗化: 将多个连续的锁操作合并为一个,减少锁的获取和释放次数
    • 锁消除: 通过逃逸分析,消除不必要的锁
    • 偏向锁: 在无竞争的情况下,偏向第一个获取锁的线程,减少锁的开销
    • 轻量级锁: 在竞争不激烈时,使用CAS操作代替重量级锁
    • 重量级锁: 在竞争激烈时,升级为重量级锁,线程进入阻塞状态
  • 偏向锁的“工作理论”是这样的:
    • 当一个线程第一次获得某个对象的锁时,JVM会在对象头(object header)里写上这个线程的ID,相当于说:“这个对象偏爱你,以后你来加锁我就不检查竞争了,直接给你用。”
    • 于是同一个线程再次进入同步块,JVM几乎不需要任何CAS(compare-and-swap,硬件原子操作),也不需要加锁的传统流程,只要看看对象头里写的是不是自己。如果是——完事。
    • 这种机制有几个关键点:
      • 没有竞争是偏向锁的前提,一旦有第二个线程来抢对象,偏向锁会“撤销”。撤销需要STW(stop-the-world),虽然很短,但确实存在。
      • 当发现对象不再是单线程使用,锁就会升级为轻量级锁,开始使用CAS
      • 再竞争激烈时,则升级到重量级锁,用OS层面的互斥。

5. 底层原理

5.1 synchronized同步语句块的情况

1
2
3
4
5
6
7
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
  • 通过JDK自带的javap命令查看SynchronizedDemo类的相关字节码信息:首先切换到类的对应目录执行javac SynchronizedDemo.java命令生成编译后的.class文件,然后执行javap -c -s -v -l SynchronizedDemo.class

  • 通过上图可以看到synchronized关键字在字节码中的体现:

    • monitorenter指令指向同步代码块开始位置
    • monitorexit指令指向同步代码块结束位置
    • 上面的字节码文件中包含一个monitorenter指令和两个monitorexit指令,这是为了保证锁在同步代码块正常执行以及出现异常时都能正确释放锁
  • 执行monitorenter时,线程试图获取对象的锁也就是获取对象监视器monitor的持有权,若锁计数器为0则表示可以获取锁,获取后计数器设为1;若锁计数器大于0则表示锁已被其他线程持有,当前线程将被阻塞直到锁可用

  • 执行monitorexit时,线程释放对象的锁,计数器为0,其他等待该锁的线程可以尝试获取锁

5.2 synchronized修饰方法的情况

1
2
3
4
5
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}

  • synchronized修饰的方法并使用的是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
  • 如果是实例方法,JVM会尝试获取实例对象的锁。如果是静态方法,JVM会尝试获取当前class的锁。

5.3 总结

  • synchronized同步语句块的实现使用的是monitorentermonitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
  • synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取而代之的是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。
  • 不过,两者的本质都是对对象监视器monitor的获取。