synchronized 是 Java 中实现线程同步的基础机制,常用于保证共享变量在多线程环境下的访问安全。下面从特性、使用方式和锁机制三个方面进行梳理。
一、synchronized 的特性
synchronized 通过加锁保证多线程环境下的资源访问安全,核心特性可以归纳为以下几点:
- 原子性:保证被修饰的代码块和方法在同一时间只能被一个线程执行,线程在执行过程中不会被其他线程打断。
- 可见性:线程释放锁时会强制将工作内存中的共享变量刷新到主内存;另一个线程获取锁时会清空自己的工作内存,并重新从主内存读取数据,从而保证共享变量的可见性。
- 有序性:同一变量在同一时刻只允许一条线程对其执行加锁操作,同一把锁的两个线程只能串行执行,从而保证执行顺序。
- 可重入性:线程第一次加锁成功后,如果再次对同一把锁加锁,synchronized 会判断当前线程是否与第一次加锁的线程一致:若为同一个线程,则直接通过;若不是同一个线程,第二次加锁才会真正生效并产生阻塞效果。
理解以上四个特性,有助于后续正确使用 synchronized。其使用方式主要分为修饰静态方法和修饰实例方法两类。
二、synchronized 的使用
synchronized 的使用场景主要围绕“锁对象是什么”展开:修饰静态方法时锁的是 Class 对象,修饰实例方法时锁的是当前实例对象。
1. 修饰静态方法
使用 synchronized 修饰静态方法时,锁对象是当前类的 Class 对象。
public class Demo11 { // 静态变量,模拟共享资源 private static int count = 0; // synchronized 修饰静态方法时,锁对象为 Demo11.class public synchronized static void add() { count++; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { // 显式使用 Demo11.class 作为锁对象,与外层 add() 使用同一把锁 synchronized (Demo11.class) { for (int i = 0; i < 5000; i++) { add(); } } }); t1.start(); t1.join(); System.out.println("count = " + count); } }2. 修饰实例方法
synchronized 修饰实例方法时,锁对象是当前实例对象,通常写作 this。同步代码块和同步方法两种写法等价。
public class Demo { // 实例变量,模拟共享资源 private int count = 0; // 写法一:同步代码块,锁对象为 this public void addByBlock() { synchronized (this) { count++; } } // 写法二:直接修饰实例方法,等价于锁 this public synchronized void addByMethod() { count++; } }以上就是 synchronized 的两种典型使用方式。实际是否加锁、锁的竞争程度如何,会进一步进入 synchronized 的锁机制优化。
三、synchronized 的锁机制
synchronized 的底层并不是任何场景都直接使用重量级锁,而是会根据实际竞争情况进行锁升级、锁消除和锁粗化等优化。
1. 锁升级
锁升级是 synchronized 的适应性过程,升级之后不会降级。锁状态的整体演进路径为:
无锁(没有进入代码块)→ 偏向锁 → 轻量级锁(自旋)→ 重量级锁
- 无锁:线程还没有进入同步代码块,没有进行加锁。
- 偏向锁:并没有真正加锁,只是在对象头中进行偏向标记,开销较小。如果没有其他线程竞争,则一直保持标记状态直到释放;一旦出现其他线程竞争,则触发锁升级。
- 轻量级锁(自旋):达到一定条件后进入轻量级锁。遇到锁冲突时,线程不会立即阻塞,而是先重新尝试获取锁;如果重试几次后锁被释放,就可以避免进入阻塞。达到一定条件后会进一步升级为重量级锁。
- 重量级锁:当竞争线程数量达到一定阈值时,会触发锁升级为重量级锁。
锁升级条件主要是评估当前锁上的竞争线程数量,当参与竞争的线程数目达到一定要求后,就会触发升级。
2. 锁消除
锁消除是编译器针对 synchronized 的一种优化。如果编译器识别到某段逻辑中并不需要使用锁,但代码里写了加锁,编译器会尝试在确保安全的前提下直接把锁去掉,避免无意义的加锁开销。
3. 锁粗化
编译器会根据实际情况进行判断:如果一系列操作都对同一个变量进行反复加锁和解锁,编译器会自动将这些锁合并,从而减少频繁加锁、解锁带来的开销。
总结
synchronized 通过原子性、可见性、有序性和可重入性保证线程安全;使用时需要区分类锁和对象锁;底层则通过锁升级、锁消除和锁粗化等机制,在正确性和性能之间取得平衡。三者分别对应“它解决什么问题”“如何正确使用”以及“内部如何优化”三个关键点。