你有没有遇到过这样的情况?两个线程同时修改同一个计数器,预期结果是20000,实际却只得到18000?或者多线程操作共享列表时,突然抛出ConcurrentModificationException?这些问题的根源,都是缺乏线程同步导致的线程不安全。

为什么需要多线程同步?
先看一个真实的“踩坑案例”:
// 计数器类(非线程安全)
public class Counter {
private int count = 0;
// 非原子操作:读count→加1→写回count
public void increment() { count++; }
public int getCount() { return count; }
}
// 测试代码
public class Test {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(counter.getCount()); // 结果常小于20000
}
}
问题出在哪儿?count++
不是原子操作——当线程1刚读完count=10,线程2也读count=10,两者都加1后写回,最终count=11而不是12。这就是典型的“线程间操作覆盖”问题,同步的核心目标就是解决这类冲突。
同步要解决的三个核心问题
线程安全的本质是保证原子性、可见性、有序性,三者缺一不可:
特性 | 定义 | 常见场景 |
---|---|---|
原子性 | 操作要么全执行、要么全不执行,中间不会被其他线程打断 | i++ 、多步数据库操作 |
可见性 | 一个线程修改共享变量后,其他线程能立即看到最新值 | 未加volatile的状态标记 |
有序性 | 程序执行顺序与代码逻辑一致(避免JVM指令重排) | 双重检查锁定的单例模式 |
比如“可见性”问题:线程A修改了flag=true
,但因为线程A的工作内存没同步到主内存,线程B读flag
时还是false,导致逻辑错误。
常用同步工具:选对工具比会用更重要
不同同步工具的设计目标不同,选对场景才能既安全又高效,以下是实战中最常用的5类工具:
synchronized:Java原生的“懒人同步”
synchronized
是Java最基础的同步关键字,隐式加锁/释放锁(JVM自动处理),适合简单场景:
– 修饰方法:public synchronized void method()
(锁对象是this
)
– 修饰代码块:synchronized (lockObject) { ... }
(锁对象可以是任意对象)
经典场景:单例模式的懒汉式(保证只有一个实例):
public class Singleton {
private static Singleton instance;
private Singleton() {} // 私有构造
// 同步getInstance方法,避免多线程创建多个实例
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
缺点:无法中断等待、无法设置超时、没有公平性(线程获取锁的顺序随机),适合“不需要额外控制”的简单同步。
Lock接口:更灵活的“显式同步”
java.util.concurrent.locks.Lock
是显式同步框架,代表实现是ReentrantLock
(可重入锁),解决了synchronized
的局限性:
– 可中断:lock.lockInterruptibly()
(等待时可响应中断)
– 超时机制:lock.tryLock(1, TimeUnit.SECONDS)
(超时放弃,避免死锁)
– 公平性:new ReentrantLock(true)
(按线程等待顺序分配锁)
实战对比:用ReentrantLock
实现线程安全的计数器:
public class SafeCounter {
private int count = 0;
// 可重入锁,默认非公平
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 显式加锁
try {
count++; // 临界区操作
} finally {
lock.unlock(); // 必须在finally释放锁,避免死锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
适用场景:需要中断、超时、公平性的复杂同步(比如秒杀系统的库存扣减)。
信号量(Semaphore):控制并发访问数量
Semaphore
本质是“许可计数器”,用来限制同时访问资源的线程数,典型场景是限流:
– 初始化:Semaphore semaphore = new Semaphore(5)
(允许5个线程同时访问)
– 获取许可:semaphore.acquire()
(没有许可则等待)
– 释放许可:semaphore.release()
(归还许可,让其他线程可用)
经典场景:数据库连接池(限制最大连接数为10):
public class DBConnectionPool {
private final Semaphore semaphore = new Semaphore(10);
private final List<Connection> connections = new ArrayList<>();
public DBConnectionPool() {
// 初始化10个数据库连接
for (int i = 0; i < 10; i++) {
connections.add(createConnection());
}
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire(); // 获取许可(最多10个线程同时获取)
return connections.remove(0);
}
public void releaseConnection(Connection conn) {
connections.add(conn);
semaphore.release(); // 归还许可
}
}
适用场景:接口限流、资源池管理(比如连接池、线程池)。
CountDownLatch:等待多个线程“完成任务”
CountDownLatch
是“倒计时门闩”,主线程等待多个子线程完成任务后再继续,不可重复使用:
– 初始化:CountDownLatch latch = new CountDownLatch(3)
(需要等待3个线程完成)
– 子线程完成:latch.countDown()
(计数器减1)
– 主线程等待:latch.await()
(直到计数器为0才继续)
经典场景:主线程等待所有子线程加载资源完成:
public class ResourceLoader {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
// 启动3个子线程加载资源
new Thread(() -> { loadImage(); latch.countDown(); }).start();
new Thread(() -> { loadConfig(); latch.countDown(); }).start();
new Thread(() -> { loadData(); latch.countDown(); }).start();
latch.await(); // 主线程等待,直到所有资源加载完成
System.out.println("所有资源加载完成,启动应用");
}
}
适用场景:任务拆分(比如大数据计算中的“分治”)。
CyclicBarrier:线程间“互相等待”
CyclicBarrier
是“循环屏障”,多个线程到达某个“ checkpoint”后,一起继续执行,可重复使用(比如循环执行多轮任务):
– 初始化:CyclicBarrier barrier = new CyclicBarrier(4, () -> { ... })
(4个线程到达后执行回调)
– 线程等待:barrier.await()
(到达屏障后等待其他线程)
经典场景:多线程并行计算,然后合并结果:
public class ParallelCalculator {
public static void main(String[] args) {
// 4个线程到达后,执行“合并结果”的回调
CyclicBarrier barrier = new CyclicBarrier(4, () -> {
System.out.println("所有线程计算完成,合并结果");
});
// 启动4个线程执行计算
for (int i = 0; i < 4; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "开始计算");
Thread.sleep(1000); // 模拟计算
System.out.println(Thread.currentThread().getName() + "到达屏障");
barrier.await(); // 等待其他线程
System.out.println(Thread.currentThread().getName() + "继续执行");
} catch (Exception e) { e.printStackTrace(); }
}).start();
}
}
}
适用场景:多线程协作(比如分布式计算中的“Map-Reduce”)。
同步避坑:这些错误90%的人都犯过
同步不是“加锁就完事”,过度同步或错误同步会导致性能下降甚至死锁,以下是必避的4个坑:
1. 避免“过度同步”:同步块越小越好
同步块过大(比如同步整个方法)会导致线程阻塞时间变长,性能下降。正确的做法是只同步“临界区”(需要保护的代码):
// 错误写法:同步整个方法(包含非临界区)
public synchronized void updateUser() {
log.info("开始更新用户"); // 非临界区(不需要同步)
userDao.update(user); // 临界区(需要同步)
log.info("更新完成"); // 非临界区
}
// 正确写法:同步临界区代码块
public void updateUser() {
log.info("开始更新用户");
synchronized (this) {
userDao.update(user); // 只同步需要保护的部分
}
log.info("更新完成");
}
2. 避免“死锁”:按顺序加锁+超时机制
死锁的4个必要条件:互斥、持有并等待、不可剥夺、循环等待,解决死锁的核心是破坏“循环等待”:
– 按顺序加锁:所有线程都按相同的顺序获取锁(比如先锁A再锁B),避免循环等待。
– 超时机制:用ReentrantLock.tryLock(1, TimeUnit.SECONDS)
设置超时,避免无限等待。
死锁案例(错误写法):
// 线程1:先锁A再锁B
synchronized (lockA) {
synchronized (lockB) { ... }
}
// 线程2:先锁B再锁A(循环等待,导致死锁)
synchronized (lockB) {
synchronized (lockA) { ... }
}
修复后:线程2也按“先A后B”的顺序加锁,彻底避免循环等待。
3. 别用volatile代替同步:它不保证原子性
volatile
能保证可见性和有序性,但不保证原子性!比如:
public class VolatileTest {
private volatile int count = 0;
// 错误:count++不是原子操作(读-改-写)
public void increment() { count++; }
}
volatile
适合状态标记(比如stop
信号),但不适合需要原子操作的场景(比如计数器)。
4. 拒绝过时工具:用并发集合代替同步集合
Vector
、Hashtable
等老式同步集合的方法都是synchronized
修饰的,性能极低(比如Hashtable是全局锁)。推荐用并发集合:
– ConcurrentHashMap
:分段锁,支持高并发读写(性能是Hashtable的数倍)
– CopyOnWriteArrayList
:读多写少场景的首选(写时复制,读无需锁)
– BlockingQueue
:实现生产者-消费者模式(比如ArrayBlockingQueue
)
最后:同步的本质是“ trade-off”
同步的核心是用性能换安全,没有“银弹”工具——简单场景用synchronized
,复杂场景用Lock
,限流用Semaphore
,等待多任务用CountDownLatch
。关键是理解每个工具的设计目标,结合场景选择。
比如电商系统的“库存扣减”场景:
– 用ReentrantLock
实现原子扣减(保证原子性)
– 用Semaphore
限制并发请求数(避免超卖)
– 用volatile
标记库存状态(保证可见性)
同步不是“技术炫技”,而是解决实际问题的手段——能简单解决的问题,就别用复杂工具。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/334