《Effective Java》学习笔记——第7部分并发
文章目录
- 一、前言
- 二、并发最佳实践
- 1. 优先使用现有的并发库
- 2. 避免共享可变数据
- 3. 最小化锁的持有时间
- 4. 使用合适的同步策略
- 5. 使用 volatile 变量来避免缓存问题
- 6.避免死锁
- 7. 使用 ExecutorService管理线程
- 8. 优先使用无锁并发工具
- 三、小结
一、前言
《Effective Java》第7部分“并发”介绍了如何编写高效、安全的多线程程序。随着多核处理器的普及,Java 的并发编程变得更加重要。本部分的内容涵盖了并发编程中的一些常见问题,并提供了一些最佳实践和技巧,以帮助开发编写出更加可靠且高效的并发程序。
二、并发最佳实践
1. 优先使用现有的并发库
-
原因:Java 提供了强大的并发工具库(如
java.util.concurrent
包),这些库经过高度优化,可以减少开发人员的工作量并避免常见的并发错误。手动实现并发机制可能会导致复杂且容易出错的代码。 -
最佳实践:
- 使用
Executor
框架来管理线程池,避免手动创建线程。 - 使用
CountDownLatch
、CyclicBarrier
、Semaphore
等并发工具类来协调线程之间的同步。 - 使用
ConcurrentHashMap
、CopyOnWriteArrayList
等线程安全的集合类来替代传统的集合类。
- 使用
-
示例:
// 使用 Executor 服务来管理线程 ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> { // 执行并发任务 }); executor.shutdown();
2. 避免共享可变数据
-
原因:多个线程同时访问和修改共享数据时,可能会导致数据的不一致性、竞态条件和死锁等问题。为了确保线程安全,必须谨慎处理共享数据。
-
最佳实践:
- 尽量避免在多个线程之间共享可变的数据。如果需要共享数据,考虑使用同步机制(如
synchronized
)或java.util.concurrent
包中的并发工具类。 - 尽量使用不可变对象(
immutable
)和局部变量,这些变量的状态无法被其他线程修改,减少了并发问题。
- 尽量避免在多个线程之间共享可变的数据。如果需要共享数据,考虑使用同步机制(如
-
示例:
// 使用 synchronized 来保护共享数据 private final Object lock = new Object(); private int sharedData; public void increment() { synchronized (lock) { sharedData++; } }
3. 最小化锁的持有时间
-
原因:锁是并发编程中的一个重要机制,但它也会影响程序的性能。长时间持有锁会导致其他线程无法访问资源,降低程序的并发性和性能。
-
最佳实践:
- 将锁的持有时间限制在最小范围内,尽量避免在锁定区域内执行耗时操作。
- 尽可能使用更细粒度的锁(如锁定某一方法而不是整个类)。
-
示例:
// 锁的持有时间较长(不推荐) public void process() { synchronized (lock) { // 执行一些耗时操作 performTimeConsumingTask(); } } // 推荐:减少锁的持有时间 public void process() { performTimeConsumingTask(); // 不在同步块中执行耗时操作 synchronized (lock) { // 执行与共享数据相关的操作 updateSharedData(); } }
4. 使用合适的同步策略
-
原因:在并发编程中,不同的任务需要不同的同步策略。错误的同步策略可能导致性能问题、死锁或其他并发错误。
-
最佳实践:
- 对于只读操作,避免加锁,因为它们不会修改共享数据。
- 对于必须同步的任务,使用
synchronized
、ReentrantLock
或其他并发工具类来确保线程安全。 - 对于短时间的锁操作,可以考虑使用
ReentrantLock
,它比synchronized
更加灵活。
-
示例:
// 使用 synchronized 进行同步(简单场景) public synchronized void increment() { sharedCounter++; } // 使用 ReentrantLock 进行同步(更灵活的场景) private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); try { sharedCounter++; } finally { lock.unlock(); } }
5. 使用 volatile 变量来避免缓存问题
-
原因:在多线程环境下,线程之间的共享数据可能会被保存在各自的 CPU 缓存中,导致不同线程读取到不同的数据副本。
volatile
关键字可以确保数据的一致性。 -
最佳实践:
- 对于简单的共享变量,使用
volatile
关键字来确保它们在多个线程之间的可见性。 - 需要注意,
volatile
不能保证复合操作的原子性(如i++
),因此需要配合其他同步机制。
- 对于简单的共享变量,使用
-
示例:
private volatile boolean flag = false; public void toggleFlag() { flag = !flag; // 保证线程间对 flag 的一致性 }
6.避免死锁
-
原因:死锁发生在两个或多个线程相互等待对方释放资源时,导致程序无法继续执行。死锁是并发编程中常见且棘手的问题。
-
最佳实践:
- 使用合适的锁顺序来避免死锁。如果多个线程需要获取多个锁,确保它们按照相同的顺序获取锁。
- 使用
tryLock()
等方法来避免死锁,尝试在有限时间内获取锁,避免长期阻塞。
-
示例:
// 死锁的示例 synchronized (lock1) { synchronized (lock2) { // 操作 } } synchronized (lock2) { synchronized (lock1) { // 操作 } } // 推荐:避免死锁 synchronized (lock1) { synchronized (lock2) { // 操作 } }
7. 使用 ExecutorService管理线程
-
原因:直接使用
Thread
来创建和管理线程是一种低效且容易出错的方法。ExecutorService
提供了一个更高效、更灵活的线程池管理机制。 -
最佳实践:
- 使用
ExecutorService
来管理线程池,自动调度和重用线程,而不是每次都创建新的线程。 - 使用
submit()
提交任务,使用Future
获取任务结果。 - 优先使用
Executors
工厂方法创建线程池,而不是手动配置线程池。
- 使用
-
示例:
ExecutorService executor = Executors.newFixedThreadPool(10); Future<Integer> future = executor.submit(() -> { // 执行任务 return 42; }); try { Integer result = future.get(); // 获取任务执行结果 } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } finally { executor.shutdown(); }
8. 优先使用无锁并发工具
-
原因:传统的锁机制(如
synchronized
)虽然简单易用,但在高并发场景下可能会引发性能问题。无锁并发工具(如AtomicInteger
、AtomicReference
等)能够提供更高效的并发处理。 -
最佳实践:
- 使用原子类(如
AtomicInteger
、AtomicLong
)来代替传统的同步方法,这些类通过底层硬件支持来实现线程安全,避免了锁的开销。
- 使用原子类(如
-
示例:
private AtomicInteger counter = new AtomicInteger(0); public void increment() { counter.incrementAndGet(); // 原子操作 }
三、小结
《Effective Java》第7部分“并发”提供了有关如何编写高效且线程安全的并发程序的最佳实践。正确地使用并发工具和库,避免共享可变数据、死锁以及无效的同步等问题,能够显著提高程序的性能和可靠性。通过理解并应用这些最佳实践,开发者可以避免常见的并发错误,并编写出更健壮的多线程程序。