《Effective Java》学习笔记——第7部分并发

文章目录

      • 一、前言
      • 二、并发最佳实践
        • [1. 优先使用现有的并发库](#1. 优先使用现有的并发库)
        • [2. 避免共享可变数据](#2. 避免共享可变数据)
        • [3. 最小化锁的持有时间](#3. 最小化锁的持有时间)
        • [4. 使用合适的同步策略](#4. 使用合适的同步策略)
        • [5. 使用 volatile 变量来避免缓存问题](#5. 使用 volatile 变量来避免缓存问题)
        • 6.避免死锁
        • [7. 使用 ExecutorService管理线程](#7. 使用 ExecutorService管理线程)
        • [8. 优先使用无锁并发工具](#8. 优先使用无锁并发工具)
      • 三、小结

一、前言

《Effective Java》第7部分"并发"介绍了如何编写高效、安全的多线程程序。随着多核处理器的普及,Java 的并发编程变得更加重要。本部分的内容涵盖了并发编程中的一些常见问题,并提供了一些最佳实践和技巧,以帮助开发编写出更加可靠且高效的并发程序。

二、并发最佳实践

1. 优先使用现有的并发库
  • 原因 :Java 提供了强大的并发工具库(如 java.util.concurrent 包),这些库经过高度优化,可以减少开发人员的工作量并避免常见的并发错误。手动实现并发机制可能会导致复杂且容易出错的代码。

  • 最佳实践:

    • 使用 Executor 框架来管理线程池,避免手动创建线程。
    • 使用 CountDownLatchCyclicBarrierSemaphore 等并发工具类来协调线程之间的同步。
    • 使用 ConcurrentHashMapCopyOnWriteArrayList 等线程安全的集合类来替代传统的集合类。
  • 示例:

    java 复制代码
    // 使用 Executor 服务来管理线程
    ExecutorService executor = Executors.newFixedThreadPool(10);
    executor.submit(() -> {
        // 执行并发任务
    });
    executor.shutdown();
2. 避免共享可变数据
  • 原因:多个线程同时访问和修改共享数据时,可能会导致数据的不一致性、竞态条件和死锁等问题。为了确保线程安全,必须谨慎处理共享数据。

  • 最佳实践:

    • 尽量避免在多个线程之间共享可变的数据。如果需要共享数据,考虑使用同步机制(如 synchronized)或 java.util.concurrent 包中的并发工具类。
    • 尽量使用不可变对象(immutable)和局部变量,这些变量的状态无法被其他线程修改,减少了并发问题。
  • 示例:

    java 复制代码
    // 使用 synchronized 来保护共享数据
    private final Object lock = new Object();
    private int sharedData;
    
    public void increment() {
        synchronized (lock) {
            sharedData++;
        }
    }
3. 最小化锁的持有时间
  • 原因:锁是并发编程中的一个重要机制,但它也会影响程序的性能。长时间持有锁会导致其他线程无法访问资源,降低程序的并发性和性能。

  • 最佳实践:

    • 将锁的持有时间限制在最小范围内,尽量避免在锁定区域内执行耗时操作。
    • 尽可能使用更细粒度的锁(如锁定某一方法而不是整个类)。
  • 示例:

    java 复制代码
    // 锁的持有时间较长(不推荐)
    public void process() {
        synchronized (lock) {
            // 执行一些耗时操作
            performTimeConsumingTask();
        }
    }
    
    // 推荐:减少锁的持有时间
    public void process() {
        performTimeConsumingTask();  // 不在同步块中执行耗时操作
        synchronized (lock) {
            // 执行与共享数据相关的操作
            updateSharedData();
        }
    }
4. 使用合适的同步策略
  • 原因:在并发编程中,不同的任务需要不同的同步策略。错误的同步策略可能导致性能问题、死锁或其他并发错误。

  • 最佳实践:

    • 对于只读操作,避免加锁,因为它们不会修改共享数据。
    • 对于必须同步的任务,使用 synchronizedReentrantLock 或其他并发工具类来确保线程安全。
    • 对于短时间的锁操作,可以考虑使用 ReentrantLock,它比 synchronized 更加灵活。
  • 示例:

    java 复制代码
    // 使用 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++),因此需要配合其他同步机制。
  • 示例:

    java 复制代码
    private volatile boolean flag = false;
    
    public void toggleFlag() {
        flag = !flag;  // 保证线程间对 flag 的一致性
    }
6.避免死锁
  • 原因:死锁发生在两个或多个线程相互等待对方释放资源时,导致程序无法继续执行。死锁是并发编程中常见且棘手的问题。

  • 最佳实践:

    • 使用合适的锁顺序来避免死锁。如果多个线程需要获取多个锁,确保它们按照相同的顺序获取锁。
    • 使用 tryLock() 等方法来避免死锁,尝试在有限时间内获取锁,避免长期阻塞。
  • 示例:

    java 复制代码
    // 死锁的示例
    synchronized (lock1) {
        synchronized (lock2) {
            // 操作
        }
    }
    
    synchronized (lock2) {
        synchronized (lock1) {
            // 操作
        }
    }
    
    // 推荐:避免死锁
    synchronized (lock1) {
        synchronized (lock2) {
            // 操作
        }
    }
7. 使用 ExecutorService管理线程
  • 原因 :直接使用 Thread 来创建和管理线程是一种低效且容易出错的方法。ExecutorService 提供了一个更高效、更灵活的线程池管理机制。

  • 最佳实践:

    • 使用 ExecutorService 来管理线程池,自动调度和重用线程,而不是每次都创建新的线程。
    • 使用 submit() 提交任务,使用 Future 获取任务结果。
    • 优先使用 Executors 工厂方法创建线程池,而不是手动配置线程池。
  • 示例:

    java 复制代码
    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)虽然简单易用,但在高并发场景下可能会引发性能问题。无锁并发工具(如 AtomicIntegerAtomicReference 等)能够提供更高效的并发处理。

  • 最佳实践:

    • 使用原子类(如 AtomicIntegerAtomicLong)来代替传统的同步方法,这些类通过底层硬件支持来实现线程安全,避免了锁的开销。
  • 示例:

    java 复制代码
    private AtomicInteger counter = new AtomicInteger(0);
    
    public void increment() {
        counter.incrementAndGet();  // 原子操作
    }

三、小结

《Effective Java》第7部分"并发"提供了有关如何编写高效且线程安全的并发程序的最佳实践。正确地使用并发工具和库,避免共享可变数据、死锁以及无效的同步等问题,能够显著提高程序的性能和可靠性。通过理解并应用这些最佳实践,开发者可以避免常见的并发错误,并编写出更健壮的多线程程序。

相关推荐
ChinaRainbowSea3 小时前
四.4 Redis 五大数据类型/结构的详细说明/详细使用( zset 有序集合数据类型详解和使用)
java·javascript·数据库·redis·后端·nosql
苏-言4 小时前
SSM框架探秘:Spring 整合 Mybatis 框架
java·spring·mybatis
Future_yzx4 小时前
算法基础学习——快排与归并(附带java模版)
学习·算法·排序算法
qq_447663055 小时前
java-----多线程
java·开发语言
a辰龙a5 小时前
【Java报错解决】警告: 源发行版 11 需要目标发行版 11
java·开发语言
听海边涛声5 小时前
JDK长期支持版本(LTS)
java·开发语言
IpdataCloud5 小时前
Java 获取本机 IP 地址的方法
java·开发语言·tcp/ip
MyMyMing5 小时前
Java的输入和输出
java·开发语言
忆~遂愿5 小时前
3大关键点教你用Java和Spring Boot快速构建微服务架构:从零开发到高效服务注册与发现的逆袭之路
java·人工智能·spring boot·深度学习·机器学习·spring cloud·eureka
云夏之末5 小时前
【Java报错已解决】java.lang.UnsatisfiedLinkError
java·开发语言