(二十三)Java-synchronized
synchronized关键字作为Java实现线程同步的基础工具。自JDK 1.0引入以来,经过多次优化(特别是Java 6的锁升级机制),使它已成为高效且易用的同步解决方案。本文将深入剖析synchronized的实现原理、使用方式及最佳实践。
一、基本用法
1.同步实例方法
锁对象:当前实例对象(this)
作用范围:整个方法体
public synchronized void increment() { // 临界区代码 } |
2.同步静态方法
锁对象:类的Class对象(ClassName.class)
作用范围:整个静态方法
public static synchronized void staticIncrement() { // 临界区代码 } |
3.同步代码块
显式指定锁对象
细粒度控制同步范围
public void doSomething() { // 非同步代码 synchronized(lockObject) { // 临界区代码 } // 非同步代码 } |
二、底层实现原理
1.对象头结构
Mark Word存储内容随状态变化:
|---------------------------------------------------- | | 锁状态 | 存储内容 | |---------------------------------------------------- | | 无锁 | 对象哈希码、分代年龄等 | | 偏向锁 | 线程ID、Epoch、分代年龄等 | | 轻量级锁 | 指向栈中锁记录的指针 | | 重量级锁 | 指向Monitor的指针 | |---------------------------------------------------- | |
2.Monitor机制
每个Java对象关联一个Monitor,包含:
_owner:持有锁的线程;
_EntryList:阻塞中的线程队列;
_WaitSet:调用wait()的线程队列;
_recursions:重入次数计数器。
3.字节码层面
通过monitorenter和monitorexit指令实现:
void test() { synchronized(obj) { // code } } // 对应字节码: 0: aload_1 1: dup 2: astore_2 3: monitorenter // 进入同步 4: aload_2 5: monitorexit // 正常退出 6: goto 14 9: astore_3 10: aload_2 11: monitorexit // 异常退出 12: aload_3 13: athrow 14: return |
三、锁升级过程(Java 6+优化)
1.偏向锁(Biased Locking)
适用场景:单线程访问。
优化原理:CAS设置线程ID。
撤销条件:出现第二个线程访问。
2.轻量级锁(Lightweight Locking)
适用场景:低竞争、短临界区。
实现方式:栈帧中创建Lock Record。
膨胀条件:自旋失败(默认10次)。
3.重量级锁(Heavyweight Locking)
实现方式:操作系统mutex。
特点:线程进入内核态阻塞。
优化:自适应自旋(JDK 6+)。
锁升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
四、内存语义
可见性保证:遵循happens-before原则
内存屏障:
monitorenter:LoadLoad + LoadStore
monitorexit:StoreStore + LoadStore
五、高级特性
1.可重入性
实现机制:_recursions计数器
最大递归深度:Integer.MAX_VALUE
public synchronized void a() { b(); } public synchronized void b() { // 可重入获取锁 } |
2.锁消除
JIT编译器通过逃逸分析消除不必要的锁:
public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); // 锁被消除 } |
3.锁粗化
JVM优化连续同步操作:
// 优化前 for(int i=0; i<100; i++) { synchronized(lock) { // 操作 } } // 优化后 synchronized(lock) { for(int i=0; i<100; i++) { // 操作 } } |
六、最佳实践
1.选择正确的锁对象
避免使用String常量等公共对象。
推荐使用私有final对象:
private final Object lock = new Object(); |
2.控制锁粒度
分段锁(如ConcurrentHashMap);
分离读写锁。
3.避免死锁
遵循顺序加锁原则:
// 线程1 synchronized(A) { synchronized(B) { ... } } // 线程2 synchronized(A) { // 保持相同顺序 synchronized(B) { ... } } |
4.性能优化
缩短同步块执行时间;
使用ThreadLocal减少竞争;
考虑ReadWriteLock替代方案。
七、常见问题
Q1: synchronized vs ReentrantLock?
特性对比:
公平性:synchronized非公平,ReentrantLock可选
条件变量:synchronized通过Object的wait/notify,ReentrantLock支持多个Condition
可中断:ReentrantLock支持lockInterruptibly()
Q2: 是否保证可见性?
完全保证,通过内存屏障实现
Q3: 静态同步与非静态同步是否互斥?
不互斥,因为锁对象不同(Class对象 vs 实例对象)
八、结论
synchronized作为Java内置锁,经过多年优化已具备优异的性能表现。开发者应当:理解不同锁状态的转换条件;根据场景选择合适的同步粒度;结合JVM优化特性编写高效代码;在复杂场景中结合java.util.concurrent工具类使用。
随着Java版本的更新(如JDK 15引入的偏向锁延迟启用),需要持续关注底层实现的演进。正确使用synchronized能在保证线程安全的同时,获得良好的性能表现。