多线程编程就像组织一帮人同时抢着改同一份文件,稍不留神就乱套:数据改错、死锁卡壳、看不见最新改动,全是坑。不懂这些常见错误,程序分分钟翻车。
下面我将详细梳理 Java 多线程并发中常见的错误、其产生原因以及相应的解决方法。
这是最经典、最常见的并发问题。
i++
这类操作并非原子操作,它包含“读取-修改-写入”三个步骤。多个线程交叉执行这些步骤会导致更新丢失。increment()
,最终 count
的值可能只增加了 1,而不是 2。synchronized
关键字对临界区(访问共享资源的代码块)进行加锁。
public synchronized void increment() { count++; }java.util.concurrent.atomic
包下的类,如 AtomicInteger
。它们通过硬件级别的 CAS (Compare-And-Swap) 操作保证单个变量的原子性,性能通常优于同步锁。
public class Counter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }ThreadLocal
。Lock.tryLock(long timeout, TimeUnit unit)
方法尝试获取锁,如果获取失败,可以释放已持有的锁并进行回退或重试,从而避免无限期等待。synchronized
和 Lock
,而是使用 java.util.concurrent
包中的高级类(如 ConcurrentHashMap
, CountDownLatch
, CyclicBarrier
等),它们内部已经很好地处理了并发问题。volatile
关键字:volatile
变量保证了修改会立即被刷新到主内存,并且每次读取都从主内存重新加载。它保证了变量的可见性,但不保证复合操作的原子性(如 i++
)。
private volatile boolean flag = false;ReentrantLock(true)
),但会降低吞吐量。Executors
工厂类或直接创建 ThreadPoolExecutor
来管理线程生命周期,复用线程,避免频繁创建和销毁的开销。线程数 = CPU核数 + 1
线程数 = CPU核数 * (1 + 平均等待时间 / 平均计算时间)
,通常可以设置为 2 * CPU核数
java.util.concurrent
包提供了强大的工具,但错误使用它们同样会带来问题。ConcurrentHashMap
所有操作都是原子的:concurrentMap.get(key) + 1
这样的操作仍然不是原子的,需要使用 replace(key, oldValue, newValue)
或 compute
等方法。HashMap
和 ArrayList
:它们不是线程安全的!在并发环境下读写会导致数据损坏或 ConcurrentModificationException
。必须使用 ConcurrentHashMap
和 CopyOnWriteArrayList
,或在外层进行同步。ThreadLocal
的内存泄漏:如果使用线程池,ThreadLocal
变量用完后必须调用 remove()
方法清理,否则其关联的 value 可能无法被 GC 回收,造成内存泄漏。java.util.concurrent
包中的类(如 ExecutorService
, ConcurrentHashMap
, CountDownLatch
, CyclicBarrier
等),而不是自己用 synchronized
和 wait()/notify()
从头构建。new Thread()
。jstack
查看线程状态和死锁,使用 JMH 进行并发性能基准测试,使用 FindBugs/SpotBugs、IDEA 等工具的静态检查功能发现潜在的并发bug。并发编程非常复杂,唯一的“银弹”就是深入理解内存模型、锁机制和并发工具的原理,并严格遵守上述最佳实践。
总之,搞定多线程的关键就三点:共享数据要加锁或换原子类,用线程池管好人手,高级工具优先别造轮子。记牢这些,你的并发程序就能又稳又快!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。