1. 深入理解synchronized的本质
在Java多线程编程中,synchronized关键字就像是一个交通警察,负责协调多个线程对共享资源的访问。它的核心作用可以用三个关键词概括:互斥、可见性和可重入性。
1.1 互斥性:线程世界的单行道
想象一下高速公路上的ETC通道,同一时间只允许一辆车通过。synchronized的互斥性也是如此,它确保同一时刻只有一个线程能够执行被保护的代码块或方法。
java复制public class TicketCounter {
private int tickets = 100;
public synchronized void sellTicket() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + tickets-- + "张票");
}
}
}
在这个售票示例中,如果没有synchronized修饰,多个售票员可能同时看到"还剩1张票"的状态,导致超卖。互斥锁保证了票数判断和减少操作的原子性。
注意:synchronized的锁获取和释放是由JVM自动管理的,即使方法抛出异常也会释放锁,这是它与Lock接口的重要区别之一。
1.2 内存可见性:不只是锁那么简单
synchronized不仅提供互斥,还建立了"happens-before"关系,确保:
- 线程解锁前,所有变量的修改都会刷新到主内存
- 线程加锁时,会清空工作内存,从主内存重新读取变量
java复制public class VisibilityDemo {
private boolean flag = false;
public synchronized void setFlag() {
flag = true;
}
public synchronized boolean getFlag() {
return flag;
}
}
在这个例子中,setFlag和getFlag都加了synchronized,保证了flag修改对其他线程的可见性。如果没有同步,由于CPU缓存的存在,一个线程的修改可能对其他线程不可见。
1.3 可重入性:自己家的门自己可以反复进
可重入性是指同一个线程可以多次获取已经持有的锁。这个特性避免了线程自己阻塞自己的死锁情况。
java复制public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("进入methodA");
methodB(); // 可重入的关键点
System.out.println("离开methodA");
}
public synchronized void methodB() {
System.out.println("进入methodB");
// 一些操作
System.out.println("离开methodB");
}
}
如果没有可重入性,methodA调用methodB时就会发生死锁。Java中的synchronized是可重入锁,同一个线程可以多次获取同一个锁。
2. synchronized的四种使用姿势
2.1 实例方法同步:对象级别的锁
java复制public class InstanceSync {
public synchronized void doSomething() {
// 临界区代码
}
}
这种写法等价于:
java复制public class InstanceSync {
public void doSomething() {
synchronized(this) {
// 临界区代码
}
}
}
关键点:锁的是当前对象实例,不同实例之间互不影响。适合保护非静态成员变量的访问。
2.2 静态方法同步:类级别的锁
java复制public class StaticSync {
public static synchronized void doSomething() {
// 临界区代码
}
}
等价于:
java复制public class StaticSync {
public static void doSomething() {
synchronized(StaticSync.class) {
// 临界区代码
}
}
}
关键点:锁的是Class对象,所有实例共享同一把锁。适合保护静态变量的访问。
2.3 同步代码块:灵活控制锁范围
java复制public class BlockSync {
private final Object lock = new Object();
public void method() {
// 非同步代码
synchronized(lock) {
// 临界区代码
}
// 非同步代码
}
}
优势:
- 减小锁粒度,提高并发性能
- 可以使用任意对象作为锁
- 避免方法级同步的粗粒度问题
2.4 双检锁单例模式:经典应用案例
java复制public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这个案例展示了:
- 代码块同步的使用
- volatile与synchronized的配合
- 减少同步范围的优化思路
3. 锁的底层实现与优化
3.1 对象头与Mark Word
每个Java对象在内存中都由三部分组成:
- 对象头
- 实例数据
- 对齐填充
其中对象头包含:
- Mark Word:存储哈希码、GC分代年龄、锁状态等
- 类型指针:指向类元数据的指针
在32位JVM中,Mark Word结构如下:
| 锁状态 | 25bit | 4bit | 1bit(偏向锁) | 2bit(锁标志) |
|---|---|---|---|---|
| 无锁 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID | Epoch | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向互斥量的指针 | 10 | ||
| GC标记 | 空 | 11 |
3.2 锁升级过程
为了提高性能,JVM实现了锁的升级机制:
- 无锁:初始状态
- 偏向锁:第一个获取锁的线程会记录自己的ID
- 轻量级锁:当有竞争时升级为CAS自旋锁
- 重量级锁:自旋失败后升级为操作系统级别的互斥锁
锁升级是单向的,不能降级。这是JVM为了减少锁操作开销的重要优化。
3.3 锁消除与锁粗化
JVM还会进行两种重要的锁优化:
-
锁消除:通过逃逸分析,发现某些锁不可能被共享访问时,会直接消除锁
java复制public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }在这个例子中,StringBuffer是方法局部变量,不可能被其他线程访问,JVM会消除其同步操作。
-
锁粗化:将连续的加锁-解锁操作合并为一次范围更大的锁
java复制public void method() { synchronized(lock) { // 操作1 } synchronized(lock) { // 操作2 } // 可能被优化为 synchronized(lock) { // 操作1 // 操作2 } }
4. 线程安全类对比与选择
4.1 非线程安全集合
| 集合类 | 特点 | 适用场景 |
|---|---|---|
| ArrayList | 动态数组实现 | 单线程环境下的随机访问 |
| HashMap | 数组+链表+红黑树 | 单线程环境下的键值存储 |
| StringBuilder | 非同步的字符串构建器 | 单线程字符串拼接 |
4.2 线程安全集合
| 集合类 | 实现原理 | 特点 |
|---|---|---|
| Vector | 方法级synchronized | 性能差,不推荐使用 |
| Hashtable | 方法级synchronized | 键值都不允许null |
| ConcurrentHashMap | 分段锁+CAS | 高并发场景首选 |
| StringBuffer | 方法级synchronized | 线程安全的字符串构建器 |
4.3 最佳实践建议
-
优先使用java.util.concurrent包:它提供了更高效的并发工具
- ConcurrentHashMap替代Hashtable
- CopyOnWriteArrayList替代Vector
- AtomicInteger等原子类替代同步计数器
-
避免过度同步:只在必要的时候使用synchronized
- 同步块比同步方法更灵活
- 使用私有final对象作为锁
-
注意死锁风险:避免嵌套获取多个锁
java复制// 危险代码 synchronized(lockA) { synchronized(lockB) { // 操作 } }
5. 常见问题与性能调优
5.1 锁竞争问题排查
当系统出现性能问题时,可以检查:
-
线程转储:使用jstack获取线程快照
code复制jstack <pid> > thread_dump.txt查找BLOCKED状态的线程和它们等待的锁
-
JVisualVM:图形化工具查看线程状态和锁情况
-
Arthas:阿里巴巴开源的Java诊断工具
code复制thread -b # 查找死锁 monitor -c 5 com.example.Demo method # 监控方法调用
5.2 性能优化技巧
-
减小锁粒度:将一个大锁拆分为多个小锁
java复制// 优化前 synchronized(this) { // 很多操作 } // 优化后 synchronized(lock1) { // 操作1 } synchronized(lock2) { // 操作2 } -
降低锁持有时间:只在必要的时候持有锁
java复制// 优化前 synchronized(lock) { result = compute(); // 耗时计算 update(result); } // 优化后 result = compute(); // 不在同步块内计算 synchronized(lock) { update(result); } -
使用读写锁:当读多写少时
java复制ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); // 读操作 rwLock.readLock().lock(); try { // 读取数据 } finally { rwLock.readLock().unlock(); } // 写操作 rwLock.writeLock().lock(); try { // 修改数据 } finally { rwLock.writeLock().unlock(); }
5.3 避免的陷阱
-
字符串常量作为锁:由于字符串驻留可能导致意外锁竞争
java复制// 危险代码 synchronized("LOCK") { // 不同地方使用相同字符串常量会导致共享同一把锁 } -
锁对象被改变:锁对象应该是final的
java复制private Object lock = new Object(); public void method() { synchronized(lock) { lock = new Object(); // 改变了锁对象 } } -
过度同步集合:包装已经线程安全的集合
java复制List<String> list = Collections.synchronizedList(new Vector<>()); // 双重同步
在实际项目中,我经常看到开发者过度使用synchronized,导致性能问题。正确的做法是先分析并发需求,再选择合适的同步策略。对于高并发场景,考虑使用并发容器、原子变量或不可变对象来减少同步需求。