傻子学编程之——Java并发编程的问题与挑战
傻子学编程之——Java并发编程的问题与挑战
Java并发编程能让程序跑得更快,但也像走钢丝一样充满风险。本文用最直白的语言和代码示例,带你直面并发编程的四大「致命陷阱」,并给出解决方案。
一、资源竞争:多个线程打架怎么办?
现象:多个线程同时修改共享变量导致数据不一致。
public class Counter { private int count = 0; public void increment() { count++; } // 非原子操作
}
// 多线程调用 increment() 后结果可能小于预期
原因:count++
包含读取→修改→写入三步,线程切换会导致中间状态丢失。
解决方案:
- 同步代码块:用
synchronized
包裹临界区
public synchronized void increment() { count++; }
- 原子变量:使用
AtomicBoolean
、AtomicInteger
等
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
- 无锁编程:CAS(Compare and Swap)机制
二、死锁:两个线程互相掐脖子
现象:程序卡死无响应,线程互相持有对方需要的锁。
// 线程1:先锁A,再请求B
synchronized(lockA) { synchronized(lockB) { ... }
}
// 线程2:先锁B,再请求A
synchronized(lockB) { synchronized(lockA) { ... }
}
原因:违反锁顺序一致性原则,满足死锁四条件(互斥、占有等待、不可抢占、循环等待)。
解决方案:
- 固定锁顺序:统一先锁A再锁B
- 超时释放:使用
ReentrantLock.tryLock()
设置超时时间
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { try { ... } finally { lock.unlock(); }
}
- 死锁检测工具:通过
jstack
分析线程栈
三、线程安全容器:ArrayList 为什么会丢数据?
现象:多线程操作集合时出现 IndexOutOfBoundsException
或数据丢失。
List<String> list = new ArrayList<>();
// 多线程调用 list.add("data")
System.out.println(list.size()); // 结果可能小于线程数
原因:集合内部数组扩容时发生竞态条件。
解决方案:改用并发容器
- 写时复制集合:适用于读多写少场景
List<String> safeList = new CopyOnWriteArrayList<>();
- 分段锁容器:
ConcurrentHashMap
(JDK8后使用CAS+红黑树)
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1); // 线程安全
四、上下文切换:为什么线程越多越慢?
现象:线程数超过 CPU 核心数后性能急剧下降。
ExecutorService executor = Executors.newFixedThreadPool(1000);
// 执行大量简单任务反而比单线程慢
原因:线程切换消耗 CPU 时间(保存/恢复线程状态、缓存失效)。
解决方案:
- 减少锁竞争:缩小同步块范围
- 使用线程池:控制线程数量(推荐公式:
线程数 = CPU核心数 * (1 + 等待时间/计算时间)
) - 协程(虚拟线程):JDK21+ 使用虚拟线程减少切换开销
Thread.startVirtualThread(() -> { System.out.println("轻量级线程!");
});
五、工具类:JUC包的「神器」们
Java并发包(java.util.concurrent
)提供了现成的解决方案:
- CountDownLatch:等待所有线程完成任务
CountDownLatch latch = new CountDownLatch(3);
latch.await(); // 主线程阻塞
// 子线程完成任务后调用 latch.countDown()
- Semaphore:控制并发访问数
Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问
semaphore.acquire(); // 获取许可证
semaphore.release();
- ThreadLocal:为每个线程维护独立副本
ThreadLocal<Integer> localCount = ThreadLocal.withInitial(() -> 0);
localCount.set(1); // 线程隔离操作
六、最佳实践:写给初学者的建议
- 避免过早优化:单线程能解决就不用多线程
- 优先使用并发容器:
ConcurrentHashMap
>Collections.synchronizedMap()
- 监控工具:用
jconsole
查看线程状态,用Arthas
分析死锁 - 测试:多线程问题可能潜伏很久,必须进行高并发压测
记住三条黄金法则:
- 能不用锁就不用锁
- 必须用锁时缩小锁范围
- 永远先查看官方文档再造轮子
参考资料:
- 《Java并发编程实战》(机械工业出版社)
- 并发容器原理(JDK1.7 vs JDK1.8)
- JUC工具类使用指南