引言

本篇博文为 JUC 技术的常见概念及相关细节梳理,意在重学 Java 查漏补缺。
博文随时会进行更新,补充新的内容并修正错漏,该系列博文旨在帮助自己巩固扎实 Java 技能。
毕竟万丈高楼,基础为重,借此督促自己时常温习回顾。

JUC

在 Java 5.0 提供了 java.util.concurrent (JUC)增加了在并发编程中常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架

  • 提供可调的、灵活的线程池
  • 提供用于多线程上下文中的 Collection 实现

volatile 关键字与内存可见性

内存可见性

内存可见性(Memory Visibility)

  • 当某个线程正在使用对象状态,而另一个线程在同时修改该状态,需要确保当一个线程修改了对象的状态后,其他线程能够看到发生的状态变化

内存可见性错误(内存可见性问题)

  • 当读操作与写操作在不同的线程中执行时,无法确保执行读操作的线程能实时地看到其他线程写入的值

  • 当多个线程操作共享数据时,彼此不可见

  • 可以通过同步来保证对象被安全地发布

    • 同步锁可解决该问题(效率低)
  • 轻量级解决方案:volatile 变量

volatile 关键字

volatile 变量是 Java 提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程

  • 当多个线程进行操作共享数据时,可以保证内存中的数据可见
    • 可以将 volatile 看作一个轻量级的锁;与锁的区别:
      • 不具备『互斥性』:不能阻止另一个线程同时访问共享数据
      • 不能保证变量的『原子性』

原子变量与 CAS 算法

原子变量:JDK1.5 后 java.util.concurrent.atomic 包下提供了常用的原子变量:

  • volatile 保证内存可见性
  • CAS(Compare-And-Swap) 算法保证数据的原子性
    • CAS 算法是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问
    • CAS 包含了三个操作数
      • 内存值 V:需要读写的内存值
      • 预估值 A:进行比较的值
      • 更新值 B:拟写入的新值
      • 当且仅当 V == A 时,V = B,否则将不做任何操作
        当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作

原子变量

支持在单个变量上解除锁的线程安全编程

  • AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 这些类的实例各自提供对相应类型单个变量的访问和更新,每个类也为该类型提供适当的实用工具方法
  • AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 这些类进一步扩展了原子操作,对这些类型的数组提供了支持
  • 核心方法:boolean compareAndSet(expectedValue, updareValue)

java.util.concurrent.atomic 提供了原子操作的常用类:

  • AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference
  • AtomicIntegerArray、AtomicLongArray
  • AtomicMarkableReference
  • AtomicReferenceArray
  • AtomicStampedReference

ConcurrentHashMap 锁分段机制

Java 5.0 在 java.util.concurrent 中提供了多种并发容器类来改进同步容器的性能

concurrentHashMap 同步容器类是 Java 5 增加的一个线程安全的哈希表,对于多线程操作,介于 HashMap 与 Hashtable 之间,内部采用 “锁分段”机制代替 Hashtable 的独占锁,进而提高性能

用于多线程上下文中的 Collection 实现:

  • 当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap
    ConcurrentSkipListMap 通常优于同步的 TreeMap
  • 当期望的读和遍历远多于列表的更新时,CopyOnWriteArrayList 优于同步的 ArrayList

ConutDownLatch 闭锁

CountDownLatch 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待

闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动
  • 等待直到某个操作所有参与者都准备就绪再继续执行

实现 Callable 接口

创建执行线程的方式有 4 种

相较于实现 Runnable 接口(重写 run 方法)的方式,call 方法可以有返回值,并且可以抛出异常

Callable 方式创建的执行线程需要 FutureTask 实现类的支持,可用于接收运算结果

  • FutureTask 是 Future 接口的实现类
  • FutureTask 可用于闭锁

Lock 同步锁

用于解决多线程安全问题的方式:

  • synchronized:隐式锁
    • 同步代码块
    • 同步方法
  • JDK 1.5 后:显式锁(可以对锁进行灵活控制)
    • 同步锁 Lock
      • 需要通过 lock() 方法上锁,必须通过 unlock() 方法释放锁

注:

  • 通常通过 Lock 接口的实现类创建 lock 对象:Lock lock = new ReentrantLock();

    • ReentrantLock 提供了与 synchronized 相同的互斥性和内存可见性,相较于 synchronized 处理锁的灵活性更高
  • 为了确保释放锁操作,通常 unlock() 方法置于 finally 代码块中

生产者消费者案例 - 虚假唤醒

等待唤醒机制,解决生产者消费者存在的问题

为了避免虚假唤醒问题,wait() 方法应该总是用在循环中

Condition 线程通信

Condition 接口描述了可能会与锁有关联的条件变量

  • 这些变量用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能
  • 单个 Lock 可能与多个 Condition 对象关联
  • 在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是 await、signal 和 signalAll
  • Condition 实例实质上被绑定到一个锁上,要为特定 Lock 实例获得 Condition 实例使用 newCondition() 方法

线程锁

线程锁的关键:

  • 非静态方法的锁默认为 this,静态方法的锁为对应的 Class 实例
  • 某一时刻内,无论有多少个方法只能有一个线程持有锁

线程池

线程池:提供了一个线程队列,队列中保存着所有等待状态的线程,避免了创建于销毁额外的开销,提高了响应速度

线程池的体系结构:

  • java.util.concurrent.Executor:负责线程的使用与调度的根接口
    • ExecutorService 子接口:线程池的主要接口
      • ThreadPoolExecutor:线程池的实现类
      • ScheduledExecutorService 子接口:负责线程的调度
        • ScheduledThreadPoolExecutor:继承 ThreadPoolExecutor,实现 ScheduledExecutorService

工具类:Executors

  • ExecutorService newFixedThreadPool():创建固定大小的线程池
  • ExecutorService new CachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量
  • ExecutorService newSingleThreadExecutor():创建单个线程池,线程池中只有一个线程
  • ScheduledExecutorService newScheduledThreadPool():创建固定大小的线程池,可以延迟或定时的执行任务