引言
本篇博文为 JUC 技术的常见概念及相关细节梳理,意在重学 Java 查漏补缺。
博文随时会进行更新,补充新的内容并修正错漏,该系列博文旨在帮助自己巩固扎实 Java 技能。
毕竟万丈高楼,基础为重,借此督促自己时常温习回顾。
JUC
在 Java 5.0 提供了 java.util.concurrent (JUC)增加了在并发编程中常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架
- 提供可调的、灵活的线程池
- 提供用于多线程上下文中的 Collection 实现
volatile 关键字与内存可见性
内存可见性
内存可见性(Memory Visibility)
- 当某个线程正在使用对象状态,而另一个线程在同时修改该状态,需要确保当一个线程修改了对象的状态后,其他线程能够看到发生的状态变化
内存可见性错误(内存可见性问题)
当读操作与写操作在不同的线程中执行时,无法确保执行读操作的线程能实时地看到其他线程写入的值
当多个线程操作共享数据时,彼此不可见
可以通过同步来保证对象被安全地发布
- 同步锁可解决该问题(效率低)
轻量级解决方案:volatile 变量
volatile 关键字
volatile 变量是 Java 提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程
- 当多个线程进行操作共享数据时,可以保证内存中的数据可见
- 可以将 volatile 看作一个轻量级的锁;与锁的区别:
- 不具备『互斥性』:不能阻止另一个线程同时访问共享数据
- 不能保证变量的『原子性』
- 可以将 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 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
- ExecutorService 子接口:线程池的主要接口
工具类:Executors
- ExecutorService newFixedThreadPool():创建固定大小的线程池
- ExecutorService new CachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量
- ExecutorService newSingleThreadExecutor():创建单个线程池,线程池中只有一个线程
- ScheduledExecutorService newScheduledThreadPool():创建固定大小的线程池,可以延迟或定时的执行任务