一、synchronized 的特性
synchronized 是 Java 语言内置的关键字,用于实现线程同步,保障多线程环境下对共享资源的安全访问。它是 Java 最基础、最常用的线程安全机制之一。synchronized 是一个可重入、非公平、互斥的锁。它通过偏向锁优化单线程访问,通过轻量级锁/自旋优化多线程交替访问,最后在竞争激烈时升级为重量级锁。同时,它天然具备原子性、可见性和有序性,是保障线程安全的“三驾马车”。
(一)synchronized 的核心行为特性
-
互斥性 (Mutual Exclusion):这是
synchronized最根本的特性。它保证了同一时刻,只有一个线程能够获取到锁并执行临界区代码。当一个线程获取到锁后,其他试图获取同一把锁的线程会被阻塞,直到持有锁的线程释放锁。这确保了对共享资源的串行访问,解决了线程安全问题。 -
可重入性 (Reentrant):
synchronized是可重入锁。这意味着同一个线程在已经持有锁的情况下,可以再次成功获取该锁而不会造成死锁。JVM 会记录锁的持有线程 ID 和一个计数器。当线程再次进入时,计数器递增;每次退出同步块时,计数器递减。只有当计数器归零时,锁才会真正被释放,其他线程才能获取。避免了“自己把自己锁死”的情况,也支持在 synchronized 方法中调用其他 synchronized 方法。 -
内存可见性 (Visibility):
synchronized能够保证在释放锁之前,线程对共享变量的修改会刷新回主内存;而在获取锁之后,线程会从主内存重新读取最新的变量副本。这保证了多个线程之间共享变量的可见性,即一个线程修改了数据,其他线程能立即看到。
(二)锁机制与优化特性
- 锁升级机制 (Lock Escalation):
synchronized的锁状态会随着竞争激烈程度动态升级,且升级过程不可逆
-
无锁状态
-
偏向锁 (Biased Locking): 初始状态。如果锁对象只有一个线程访问,JVM 会将锁“偏向”该线程,线程再次进入时不需要进行任何同步操作(CAS),直接通过检查对象头中的线程 ID 即可。
-
轻量级锁 (Lightweight Locking): 当有多个线程交替访问(无竞争或竞争不激烈)时,JVM 使用 CAS 操作来尝试获取锁,避免了操作系统层面的线程阻塞和唤醒,提高了性能。
-
重量级锁 (Heavyweight Locking): 当竞争激烈(如线程自旋超过一定次数仍未获取到锁),锁会膨胀为重量级锁。此时依赖操作系统的互斥量(Mutex)来实现,线程会进入阻塞状态,需要操作系统内核进行调度,开销最大。
-
自旋锁策略 (Spin Lock):在轻量级锁状态下,如果线程获取锁失败,它不会立即挂起(阻塞),而是会执行一段忙循环(自旋),看持有锁的线程是否很快就会释放。这是一种“以时间换空间”的策略,避免了线程切换的开销。
-
非公平锁:
synchronized是非公平锁。这意味着当锁被释放时,等待时间最长的线程并不一定能优先获取到锁。新来的线程和队列中等待的线程有同等的概率竞争到锁,这可能导致某些线程长时间无法获取锁(饥饿),但整体吞吐量通常比公平锁更高。
二、synchronized的使用
synchronized 的本质是加锁,但锁的对象会根据使用位置的不同而变化。
(一)修饰实例方法 (对象锁):当你用 synchronized 修饰一个普通方法时,锁的是当前实例对象(this)。适用场景: 保护该实例对象的成员变量,防止多个线程同时修改同一个对象的状态。
public class Counter {
private int count = 0;
// 锁住的是调用这个方法的实例对象 (this)
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
(二)修饰静态方法 (类锁):当你用 synchronized 修饰一个静态方法时,锁的是该类的 Class 对象(类名.class)。适用场景为保护静态变量(类变量),因为静态变量属于类,不属于某个具体实例。
public class Counter {
private static int totalCount = 0;
// 锁住的是 Counter.class 对象
public static synchronized void incrementTotal() {
totalCount++;
}
}
(三)修饰代码块(同步代码块):这是粒度最细、最灵活的用法。你可以指定任意对象作为锁。适用场景: 只需要同步方法中的一部分代码(临界区),以减少锁的持有时间,提高并发性能。
public class Counter {
private int count = 0;
private final Object lock = new Object(); // 专用锁对象
public void increment() {
// 只锁住必要的代码块,而不是整个方法
synchronized (lock) {
count++;
}
// 这里可以执行不需要同步的耗时操作,不阻塞其他线程
}
}
三、synchronized的锁机制
Java 中 synchronized 的锁机制是一个非常精妙的设计,它并不是一成不变的“重量级”锁,而是随着竞争激烈程度动态升级的。
(一)、核心组件
synchronized 的实现依赖于 JVM 对 Java 对象的管理和操作系统底层的互斥机制。
- 对象头 (Object Header) 每个 Java 对象在内存中都有一个对象头,其中的 Mark Word 是核心。它在不同的锁状态下,存储的内容是不一样的(比如存储哈希码、线程 ID、指向锁记录的指针等)。
- Monitor (监视器/管程) 这是
synchronized实现同步的基础。每个 Java 对象都天生关联一个 Monitor。
- Owner: 指向持有该锁的线程。
- Entry Set: 存放等待获取锁的线程队列。
- Wait Set: 存放调用了
wait()方法的线程队列。
(二)锁升级机制
这是 synchronized 性能优化的关键。为了减少线程阻塞和唤醒的开销,JVM 会根据竞争情况,从低级别的锁向高级别的锁自动升级。这个过程是不可逆的(只能升级,不能降级)。
-
1. 无锁状态 (No Lock)
-
偏向锁 (Biased Locking) —— [优化单线程]
-
场景: 大多数情况下,锁不仅没有竞争,而且总是由同一个线程多次获取。
-
机制: JVM 会把该锁“偏向”于当前线程。它会在对象头的 Mark Word 中记录该线程的 ID。
-
优点: 下次同一个线程再进入同步块时,不需要进行任何同步操作(不需要 CAS 操作),直接执行即可。加锁解锁几乎没有额外开销。
-
触发升级: 当有第二个线程尝试获取锁时,偏向锁就会失效,升级为轻量级锁。
-
轻量级锁 (Lightweight Locking) —— [优化多线程交替]
-
场景: 线程间存在竞争,但不是特别激烈,且持有锁的时间很短。
-
机制:线程在自己的栈帧中建立一个锁记录 (Lock Record)。通过 CAS 操作,尝试将对象头的 Mark Word 指向这个锁记录。如果成功,获取锁;如果失败(说明有竞争),线程不会立即阻塞,而是进入自旋(Spin)状态,循环尝试获取锁。
-
优点: 避免了线程阻塞和唤醒的开销(用户态与内核态切换)。
-
缺点: 如果自旋次数过多(默认 10 次)或者竞争加剧,会消耗 CPU 资源。
-
重量级锁 (Heavyweight Locking) —— [终极手段]
-
场景: 多线程竞争激烈,或者锁被持有时间很长。
-
机制: 当自旋超过一定阈值,或者等待线程过多时,锁会膨胀为重量级锁。
-
原理: 此时依赖操作系统的互斥量 (Mutex) 来实现。对象头的 Mark Word 指向一个重量级的 Monitor 对象。
-
表现: 所有未获取到锁的线程都会被阻塞(挂起),进入内核调度状态,不再消耗 CPU。只有当持有锁的线程释放锁后,操作系统才会唤醒阻塞队列中的一个线程。
-
缺点: 线程阻塞和唤醒涉及上下文切换,开销很大。
914

被折叠的 条评论
为什么被折叠?



