当前位置: 首页 > article >正文

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

文章目录

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


image-20250122103431384

一、前言

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

二、并发最佳实践

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

  • 最佳实践:

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

    // 使用 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. 使用合适的同步策略
  • 原因:在并发编程中,不同的任务需要不同的同步策略。错误的同步策略可能导致性能问题、死锁或其他并发错误。

  • 最佳实践:

    • 对于只读操作,避免加锁,因为它们不会修改共享数据。
    • 对于必须同步的任务,使用 synchronizedReentrantLock 或其他并发工具类来确保线程安全。
    • 对于短时间的锁操作,可以考虑使用 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)虽然简单易用,但在高并发场景下可能会引发性能问题。无锁并发工具(如 AtomicIntegerAtomicReference 等)能够提供更高效的并发处理。

  • 最佳实践:

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

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

三、小结

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


http://www.kler.cn/a/517692.html

相关文章:

  • Next.js 实战 (十):中间件的魅力,打造更快更安全的应用
  • 【Day28 LeetCode】动态规划DP
  • 失业ing
  • 刷题总结 回溯算法
  • 衡量算法性能的量级标准:算法复杂度
  • 【QT】-explicit关键字
  • 一文讲清JVM中的内存泄漏问题
  • Go语言中的值类型和引用类型特点
  • STM32项目分享:智能宠物喂食系统(升级版)
  • 软件过程模型
  • python动态全局缓存配置
  • 【论文+源码】 SeqDiffuSeq带有序列到序列生成的编码器变压器的文本扩散模型
  • OpenCV相机标定与3D重建(65)对图像点进行去畸变处理函数undistortPoints()的使用
  • 洛谷P1469 找筷子
  • Scala语言的移动应用开发
  • 使用select函数创建多线程TCP服务端
  • Skia使用Dawn后端在Windows窗口中绘图
  • 反向代理模块1
  • 第五天 Labview数据记录(5.1 INI配置文件读写)
  • python+playwright自动化测试(九):expect断言和expect_xxx()元素及事件捕获
  • 隐马尔科夫模型HMM
  • HDLC,pap,chap网络
  • C语言初阶--折半查找算法
  • Titans 架构下MAC变体的探究
  • polars as pl
  • 消息队列:春招面试的重要知识模块