长城知识网

Java并发编程有哪些核心常识需掌握?

Java 并发编程核心常识

为什么需要并发编程?

  1. 充分利用多核 CPU:现代计算机大多是多核的,并发编程可以让程序的不同部分同时运行,从而提高 CPU 的利用率,加快程序执行速度。
  2. 提升程序响应性:对于有用户界面的程序或需要处理大量 I/O 操作(如网络请求、文件读写)的程序,使用多线程可以将耗时操作放到后台线程执行,避免主线程被阻塞,保持界面的流畅响应。
  3. 简化建模:某些现实世界的问题本身就是并发的(如多个客户端同时访问服务器),用并发模型来编写代码更符合直觉,更容易实现。

并发编程的三大核心问题

并发编程并非万能药,它会引入一系列复杂问题,其中最核心的三个是:

  1. 原子性:一个或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。

    Java并发编程有哪些核心常识需掌握?-图1
    (图片来源网络,侵删)
    • 问题:最常见的例子是 i++ 操作,它实际上包含三个步骤:读取 i 的值、i 加 1、写回 i 的值,在多线程环境下,如果两个线程同时读取到 i 的旧值,然后各自加 1,再写回,最终结果只增加了 1,而不是预期的 2。
    • 解决方案:使用 synchronized 关键字、java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)。
  2. 可见性:当一个线程修改了一个共享变量的值,其他线程能够立即得知这个修改。

    • 问题:由于每个线程都有自己的工作内存(CPU 缓存),变量可能从主内存拷贝到线程的工作内存中进行修改,如果修改后的值没有及时写回主内存,其他线程就无法看到最新的值。
    • 解决方案
      • volatile 关键字:保证了变量的可见性,禁止指令重排序。
      • synchronized 关键字:在解锁时,会将工作内存中的变量写回主内存;在加锁时,会清空工作内存,从主内存中重新加载变量。
      • final 关键字:被 final 修饰的字段,在构造函数完成之后,对其他所有线程都是可见的。
  3. 有序性:即程序执行的顺序按照代码的先后顺序执行。

    • 问题:为了优化性能,编译器和处理器可能会对指令进行重排序,在单线程环境下,重排序不会影响最终结果;但在多线程环境下,重排序可能会破坏代码的逻辑。
    • 解决方案
      • volatile 关键字:可以禁止指令重排序。
      • synchronized 关键字:一个变量在同一个时刻只允许一条线程对其进行锁定,这使得持有同一个锁的两个同步块只能串行地进入,从而保证了有序性。

synchronized 关键字可以同时解决原子性、可见性和有序性问题。volatile 关键字可以解决可见性和有序性问题,但不能保证原子性。

Java 并发基础工具

synchronized 关键字

这是 Java 最基础、最常用的同步机制。

Java并发编程有哪些核心常识需掌握?-图2
(图片来源网络,侵删)
  • 作用:确保在同一时刻,只有一个线程可以执行被 synchronized 修饰的代码块或方法。
  • 三种用法
    1. 实例方法:锁是当前实例对象(this)。
    2. 静态方法:锁是当前类的 Class 对象。
    3. 代码块:锁可以是任意对象,灵活性最高。
  • 特点
    • 可重入:一个线程可以多次获取它已经持有的锁,这避免了死锁(一个线程在调用一个 synchronized 方法时,可以调用同一个对象的另一个 synchronized 方法)。
    • 非公平锁:默认情况下,synchronized 是非公平的,新来的线程可能直接抢占锁,而不是等待,这可能导致某些线程长时间等待(“饥饿”)。

volatile 关键字

  • 作用:保证变量的可见性和禁止指令重排序。
  • 适用场景:当一个线程写一个变量,而其他线程读这个变量,并且不依赖当前值进行更复杂的原子操作时,最典型的场景是状态标记(如 boolean flag)。
  • 不适用场景:不能用于 i++ 这样的复合操作,因为它不保证原子性。

java.util.concurrent.atomic

  • 作用:提供了一系列原子变量类,如 AtomicInteger, AtomicLong, AtomicReference 等。
  • 原理:这些类内部使用 CAS (Compare-And-Swap) 操作,这是一种现代 CPU 都支持的硬件指令,可以实现无锁的原子操作。
  • 优点:相比于 synchronized,CAS 通常能带来更高的性能,因为它避免了线程的阻塞和上下文切换。
  • 缺点
    • ABA 问题:如果一个值从 A 变成 B,又变回 A,CAS 操作会认为它没有被修改过,可以通过在变量上附加一个“版本号”来解决(AtomicStampedReference)。
    • 自旋开销:CAS 失败,线程会不断地尝试(自旋),在竞争激烈的情况下,这会消耗大量 CPU 资源。

高级并发工具

java.util.concurrent.locks

提供了比 synchronized 更强大、更灵活的锁机制。

  • ReentrantLock (可重入锁)

    • 功能与 synchronized 类似,但提供了更多功能。
    • 公平性:可以设置为公平锁(FIFO),线程会按照请求的顺序获取锁。
    • 锁超时:可以尝试获取锁,并在指定时间内失败,而不是无限等待。
    • 可中断:可以响应中断,在等待锁的过程中可以被其他线程中断。
    • 多个条件变量:一个 ReentrantLock 可以绑定多个 Condition,实现更精确的线程唤醒控制。
  • ReadWriteLock (读写锁)

    • 将锁分为读锁和写锁。
    • 规则:多个读锁可以同时持有,但写锁是独占的。
    • 适用场景:读多写少的场景,可以大大提高并发性能。

线程池 (java.util.concurrent.ExecutorService)

创建和管理线程本身是昂贵的,线程池可以复用已创建的线程,避免频繁创建和销毁线程的开销。

Java并发编程有哪些核心常识需掌握?-图3
(图片来源网络,侵删)
  • 核心类ThreadPoolExecutor
  • 创建方式:推荐使用 Executors 工厂类,但要注意其局限性(如 FixedThreadPoolCachedThreadPool 可能导致资源耗尽,生产环境更推荐手动配置 ThreadPoolExecutor)。
  • 关键参数
    • corePoolSize:核心线程数。
    • maximumPoolSize:最大线程数。
    • workQueue:工作队列,用于存放等待执行的任务。
    • keepAliveTime:线程空闲时间,超过此时间将被回收。
  • 拒绝策略:当工作队列已满且线程数达到最大值时,新提交的任务会被拒绝。RejectedExecutionHandler 提供了四种默认策略(如 AbortPolicy 直接抛出异常)。

并发集合 (java.util.concurrent 包下的集合)

  • ConcurrentHashMap:线程安全的 HashMap,在 Java 8 中,它通过 CAS + synchronized 实现,分段锁的概念被弱化,并发性能更高。
  • CopyOnWriteArrayList / CopyOnWriteArraySet:写时复制集合,任何修改操作(add, set)都会复制整个底层数组,读操作不加锁,性能极高。
    • 适用场景:读远多于写,且集合大小不大的场景。
  • BlockingQueue:阻塞队列,是生产者-消费者模式的最佳实践,当队列满时,生产者会阻塞;当队列空时,消费者会阻塞。
    • 常用实现ArrayBlockingQueue (有界), LinkedBlockingQueue (可界), SynchronousQueue (不存储元素)。

CountDownLatch, CyclicBarrier, Semaphore

  • CountDownLatch (倒计时门闩):允许一个或多个线程等待其他一组线程完成操作,是一次性的。
    • 场景:主线程等待多个子任务全部完成后继续执行。
  • CyclicBarrier (循环栅栏):让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行,是可循环使用的。
    • 场景:多个线程分阶段执行任务,每个阶段都需要所有线程都到达后才能进入下一阶段。
  • Semaphore (信号量):控制同时访问特定资源的线程数量。
    • 场景:资源有限的场景,如数据库连接池、停车场车位管理。

线程的生命周期

  1. NEW (新建):线程被创建,但尚未启动。
  2. RUNNABLE (运行):Java 将 READY (就绪) 和 RUNNING (运行) 状态统称为 RUNNABLE,线程已启动,正在等待 CPU 时间片或正在执行。
  3. BLOCKED (阻塞):线程因为等待获取锁而进入阻塞状态。
  4. WAITING (等待):线程等待另一个线程执行特定操作(如 wait(), join(), LockSupport.park()),这种状态需要被显式唤醒。
  5. TIMED_WAITING (超时等待):和 WAITING 类似,但它可以在指定时间后自动唤醒(如 sleep(time), wait(time))。
  6. TERMINATED (终止):线程执行完毕或因异常退出。

最佳实践和常见陷阱

  1. 避免过度同步:同步会带来性能开销,只对真正需要共享和修改的数据进行同步。
  2. 优先使用 java.util.concurrent:这些工具经过精心设计和测试,比自己用 synchronized 手动实现更可靠、更高效。
  3. 注意线程安全:不要将可变对象(特别是集合类)不加保护地共享。ArrayList, HashMap 不是线程安全的。
  4. 避免死锁:死锁发生的四个必要条件(互斥、持有并等待、不可剥夺、循环等待),破坏其中一个即可避免,常见的预防方法:按固定顺序获取锁使用 tryLock 设置超时
  5. 谨慎使用 Executors 创建线程池:了解 FixedThreadPoolCachedThreadPool 的潜在风险,优先使用 ThreadPoolExecutor 并合理配置参数。
  6. 使用 ThreadLocal 谨慎ThreadLocal 用于创建线程隔离的变量,可以避免多线程问题,但它可能导致内存泄漏,因为每个线程都有自己的副本,如果线程不被销毁(如线程池中的线程),这些副本会一直存在,使用后记得调用 remove() 方法。
  7. 优先使用不可变对象:不可变对象天生是线程安全的,因为它们的状态创建后就不能被修改。
分享:
扫描分享到社交APP
上一篇
下一篇