最近了不起在整理关于JUC的学习路线,在这里也一起分享给大家。

在 Java 的多线程世界里,线程安全就像一座必须坚守的城池,而 synchronized 关键字便是守护这座城池的坚实盾牌。
从简单的单例模式实现到复杂的分布式系统交互,synchronized 始终在默默发挥着关键作用,确保多个线程在共享资源时能够有序协作,避免数据错乱的 “灾难”。
想要真正驾驭多线程编程,吃透 synchronized 的底层逻辑与实用技巧,无疑是绕不开的重要一课。
接下来,就让我们一同揭开 synchronized 的神秘面纱,从原理到实践,全方位掌握这一核心关键字。
synchronized 基于监视器锁(Monitor Lock)实现,每个 Java 对象都内置了一个监视器锁。当线程进入 synchronized 代码块或方法时,会自动尝试获取这个锁。
若锁可用,线程便获取锁并继续执行;若锁被其他线程持有,当前线程则会被阻塞,进入等待状态,直至锁被释放。
从字节码层面来看,synchronized 的实现运用了 monitorenter 和 monitorexit 指令。
synchronized 可以修饰实例方法、静态方法和代码块,根据修饰目标的不同,获取的锁也有所区别,分为对象锁(实例锁)和类锁(静态锁)。
实例方法同步:当 synchronized 修饰实例方法时,线程获取的是当前对象的锁。示例代码如下:
public synchronized void method() {
// 获取当前对象的锁
}
代码块同步:在代码块中使用 synchronized (this),线程获取的同样是当前对象的锁。示例代码如下:
public void method() {
synchronized(this) {
// 获取当前对象的锁
}
}
静态方法同步:synchronized 修饰静态方法时,线程获取的是类的 Class 对象的锁。示例代码如下:
public static synchronized void staticMethod() {
// 获取类的Class对象的锁
}
Class 对象同步:在代码块中使用 synchronized (MyClass.class),线程获取的是类的 Class 对象的锁。示例代码如下:
public void method() {
synchronized(MyClass.class) {
// 获取类的Class对象的锁
}
}
需要重点注意的是,对象锁和类锁是完全独立的,一个线程获取了对象锁,并不会阻碍其他线程获取类锁。
为了提升性能,JVM 对 synchronized 进行了优化,引入了锁升级机制。
锁会依据竞争情况,从偏向锁逐步升级到轻量级锁,再到重量级锁,且这个过程是单向升级,不可降级。
偏向锁:适用于只有一个线程访问的场景,几乎没有性能开销,它会在对象头中记录线程 ID,当有其他线程竞争时便会升级。
轻量级锁:适用于线程交替执行的情况,使用 CAS 操作避免线程阻塞,会在栈帧中创建锁记录,当竞争激烈时升级为重量级锁。
重量级锁:适用于高竞争的情况,它使用操作系统的互斥量,线程会被阻塞和唤醒,性能开销最大。
死锁是多线程编程中的常见问题,当两个或多个线程相互等待对方释放锁时,就会发生死锁。
(一)死锁的四个必要条件
互斥条件:资源不能被多个线程同时使用。
持有和等待:线程持有资源的同时等待其他资源。
不可剥夺:资源不能被强制从线程中剥夺。
循环等待:存在线程资源的循环等待链。
(二)死锁示例
以下是一个经典的死锁场景代码:
public class DeadlockExample {
privatestatic Object lock1 = new Object();
privatestatic Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: 获取lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1: 获取lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: 获取lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2: 获取lock1");
}
}
});
t1.start();
t2.start();
}
}
(三)死锁预防策略
锁排序:所有线程按相同顺序获取锁。
锁超时:使用 tryLock () 设置获取锁的超时时间。
死锁检测:定期检测死锁并采取恢复措施。
避免嵌套锁:尽量避免在持有锁的情况下获取其他锁。
虽然 synchronized 使用简单,但在高并发场景下可能成为性能瓶颈,掌握以下优化技巧有助于提升程序性能。
(一)优化策略 减小锁粒度:将大锁拆分为多个小锁,减少锁竞争。示例如下:
// 粗粒度锁
synchronized(this) {
updateField1();
updateField2();
}
// 细粒度锁
synchronized(lock1) { updateField1(); }
synchronized(lock2) { updateField2(); }
锁分离:读写分离,使用 ReadWriteLock。示例如下:
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
锁消除:JVM 会自动消除不必要的锁。示例如下:
// 局部变量,JVM会消除同步
StringBuffer sb = new StringBuffer();
sb.append("hello");
sb.append("world");
(二)性能建议
尽量缩短同步代码块的执行时间。
避免在循环中使用 synchronized。
考虑使用并发集合类替代同步集合。
在高并发场景下考虑使用 Lock 接口。
当我们梳理完 synchronized 的工作原理、锁的类型差异、升级机制、死锁问题及性能优化策略后,不难发现这个看似简单的关键字背后,蕴藏着 Java 多线程编程的深刻智慧。
它不仅是保障线程安全的基础工具,更是开发者理解 JVM 底层运行机制的重要窗口。
在实际开发中,没有放之四海而皆准的同步方案,唯有根据业务场景灵活运用 synchronized 的各项特性,才能在安全性与性能之间找到最佳平衡点。
希望通过本文的解读,你能对 synchronized 形成系统认知,并将其转化为解决实际问题的能力。若你在实践中总结出更多独到经验,欢迎在评论区分享,让我们一同在多线程编程的道路上不断精进。
福利分享时间
很多小伙伴也关注很久了
我们能与志同道合的朋友畅聊职业发展
分享最新面试机会和题库
为了更方便的交流
我们特别创建了一个专业的交流群
旨在帮助大家互相学习、共同进步
输入加群口令 mmm
即可轻松成为我们的一员