八股文-Java并发编程

Java并发编程的关键是实现线程安全,也就是管理对共享可变状态的访问,可以从以下思路进行描述:线程安全的定义,问题来源,以及解决方案。Java的解决方案涉及到JSR133(内存模型)和JSR166(并发包,即JUC包),其中JSR166又是在JVM提供的synchronized和volatile关键字之外补充。

线程安全的定义,线程安全的关键点在于正确性的定义,以类为例:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。也就是线程安全的类封装了任何必要的同步。

线程安全的问题主要涉及到几个方面:原子访问、Word Tearing、缓存一致性、顺序一致性(指令重排)。为了解决这些问题,JSR133定义了Java内存模型。并在此基础上,提供了语法级别的synchronized、volatile关键字,以及JSR166定义的JUC包。

Java内存模型

在这里需要注意的一点是,Java内存模型不是内存分布,也就是不是JVM内存堆栈,GC的那些概念。
内存模型按JLS的定义:内存模型描述了对于一段程序和对应的执行路径,这个执行路径是否合法。(非正式表达)内存模型通过检查执行路径里面的读操作,在指定规则集合下面,判断读操作见到的写入是否合法。

基础概念

Data race(数据竞争)

数据竞争的条件:
同一个进程中的两个或多个线程并发操作同一变量,
至少其中一个操作为写操作,
并且线程没有采用任何互斥锁(或者没有被同步排序)。

Shared variables(共享变量)

共享变量/堆内存(Shared variables/Heap memory)能够在线程间共享的内存称作共享内存或堆内存。所有的实例字段,静态字段以及数组元素都存储在堆内存中。JLS使用变量这个词来表示字段和数组元素。方法中的局部变量永远不会在线程间共享且不会被内存模型影响。

Inter-thread Actions(线程间动作/操作)

线程间的动作(Inter-thread Actions)线程间的动作是由某一线程执行,能被另一线程探测或直接影响的动作(action)。线程间的动作包括共享变量的读写以及同步动作(synchronization action),如lock或unlock某个监视器(monitor),读写某个volatile变量或启动一个线程。也包括与外部世界交互的动作(外部动作,external action),以及导致某个线程进入无限循环的动作(线程分散动作,thread divergence action)。
这里不包括线程内动作,比如,加和两个局部变量并赋值到第三个局部变量。
为方便起见,JLS和JSR将线程间的动作简称为动作(action)。

Program Order(程序顺序)

在所有由线程t执行的的线程间动作中,t的程序顺序是一个反应动作按 t的线程内语义 执行的全序关系。

Intra-thread semantics(线程内语义)

线程内语义是单线程程序的标准语义,基于某个线程内读动作能看到的值,可以完整的预测这个线程的行为。
线程内语义决定着某个线程孤立的执行过程;内存模型决定的堆内存中读操作的值。

Synchronization Actions(同步动作)

同步动作包括锁、解锁、读写volatile变量,用于启动线程的动作以及用于探测线程是否结束的动作。任何动作,只要是synchronizes-with 边缘(edge)的起始或结束点,都是同步动作。

Synchronization Order(同步顺序)

同步顺序是一次执行过程中的所有同步动作上的全序关系。对于线程t,其同步动作的同步顺序与t的程序顺序一致。

Happens-Before and Synchronizes-With Edges
Sequential Consistency(顺序一致性)

在顺序一致性里,所有动作以全序(执行顺序)的顺序发生,与程序顺序一致;而且,如果符合以下条件,每个对变量 v 的读操作 r 都将看到写操作 w 写入 v 的值:

  • 执行顺序上 w 在 r 之前
  • 执行顺序上不存在这样一个 w’, w 在 w’之前且 w’在 r 之前

模型

Intra-thread Consistency(线程内一致性)

对应程序顺序(PO),确保程序按线程内语义执行。

Synchronization Order Consistency

对应同步顺序(Synchronization Order),限制线程间同步动作的全序关系。

Happens-before Consistency

对应Happens-before顺序(Happens-before Order)。

Commit semantics

对应Executions and Causality Requirements,解决out-of-thin-air (OoTA)问题。

实现

指令重排

对于编译器的编写者来说,Java内存模型(JMM)主要是由禁止指令重排的规则所组成的,包括字段存取和监视器(锁)控制指令。

指令重排涉及三类数据的存取:非volatile字段(Normal Load / Normal store),volatile字段(Volatile load / Volatile store)和monitor(MonitorEnter / MonitorExit)。在JMM中,Normal Load指令与Normal store指令的规则是一致的,类似的还有Volatile load指令与MonitorEnter指令,以及Volatile store指令与MonitorExit指令。

| Can Reorder | 2nd operation |||
| ---- |:----😐:----😐:----😐:----😐
| 1st operation | Normal Load
Normal Store
| Volatile Load
MonitorEnter
| Volatile Store
MonitorExit
|
| Normal Load
Normal Store
| | | NO |
| Volatile Load
MonitorEnter
| NO | NO | NO |
| Volatile Store
MonitorExit
| | NO | NO |

内存屏障

多核处理器通常需要内存屏障来实现"as-if-sequential"一致性。

内存屏障可以分为:LoadLoad,StoreStore,LoadStore,StoreLoad,分别对应Store和Load不同组合操作之间所用的内存屏障。

最简单保守的策略是为任一给定的load,store,lock或unlock生成代码时,都假设该类型的存取需要“最重量级”的屏障。实际实现中,绝大部分内存屏障可以简化为空操作。

以volatile store为例,OpenJDK的处理方式是在操作后加入StoreLoad屏障。具体到x86上,使用locked addl来实现。(未采用cpuid和mfence,cpuid为串行化执行指令,mfence内存屏障指令在部分场景下操作比较重)

JUC

JUC包主要包括Lock/锁, Atomic/原子操作, Executors/线程池, Concurrent Collections/并发集合, 和Synchronizers/同步器等内容。

Executors/线程池

Executors类提供了常用线程池的工厂方法和部分工具方法。

CachedThreadPool线程池无限大,队列采用SynchronousQueue(空间为0,若对象未被取走的话直接阻塞),默认初始0线程,空闲线程60s回收。
FixedThreadPool 和 SingleThreadExecutor 队列采用LinkedBlockingQueue(空间上限为Integer.MAX_VALUE)。
ScheduledThreadPool 和 SingleThreadScheduledExecutor 队列采用DelayedWorkQueue(内部以数组维护,锁为this的ReentrantLock)

ThreadPoolExecutor 中 ctl(AtomicInteger) 用于记录运行状态和线程数(前3位用于状态,后29位用于计线程数,因此线程数上限为2^29-1,大约5亿)。

线程池状态:

  • RUNNING
  • SHUTDOWN(不接受新任务,但是会处理队列任务)
  • STOP(不接受新任务,不处理队列任务,并中断进行中的任务)
  • TIDYING(所有任务停止,workerCount=0时转入此状态,并调用terminated函数)
  • TERMINATED(terminated函数执行完毕)

线程池和一般连接池的区别:若线程数小于CorePoolSize时,新任务达到时不管现有线程是否idle,立刻创建新线程;若线程数大于CorePoolSize,小于MaximumPoolSize,仅队列满时创建新线程。(默认情况下,核心线程即使被预先创建,在任务到达前不会start)

队列策略:

  • 若线程数小于corePoolSize,添加新线程执行
  • 若线程数不小于corePoolSize,任务入队
  • 若入队失败,创建新线程。除非线程数超过maximumPoolSize,此时直接拒绝

Worker为线程池的执行单元,绑定Thread,继承自AQS(可以进行加锁解锁)。Task执行过程中异常的话,

Concurrent Collections/并发集合

JUC中Queue分为阻塞和非阻塞两种,非阻塞队列为ConcurrentLinkedQueue,阻塞队列为BlockingQueue的实现类LinkedBlockingQueue, ArrayBlockingQueue, SynchronousQueue, PriorityBlockingQueue, DelayQueue 以及 BlockingDeque的实现类LinkedBlockingDeque。
Java7中增加了接口TransferQueue和其实现类LinkedTransferQueue。

除去Queue,JUC包还提供了ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteArraySet, ConcurrentSkipListMap, ConcurrentSkipListSet等类。

Synchronizers/同步器

Synchronizers 方面提供了 Semaphore, CountDownLatch, CyclicBarrier, Exchanger等类。JDK 7引入Phaser类。

Lock/锁

Locks包提供了Lock, ReadWriteLock和Condition三个接口,Condition的用法类似于Object.wait()里面的监视器,并提供了ReentrantLock, ReentrantReadWriteLock和StampedLock(JDK 8)三个实现类。底层基于CAS, LockSupport和AbstractQueuedSynchronizer。

Atomic/原子操作

Atomic包提供了CAS类封装,JDK 8新增了Striped64和LongAdder, DoubleAdder 以及 LongAccumulator, DoubleAccumulator。

Atomic*类底层为cmpxchg指令(多核处理器lock指令配合),类本身不负责ABA的问题,解决方案:AtomicLong单向更新 或 采用AtomicStampedReference类。

AtomicStampedReference将reference和stamp封装为对象Pair,CAS更新Pair的引用,Pair字段设置为volatile。

Striped64 维护一个base字段和延迟初始化的Cell数组记录数据。Cell数组长度为2的幂(第一次初始化长度为2,最大值为不小于CPU核心数的幂值),索引采用线程唯一的哈希值。Cell对象通过Contended注解(字段前后添加128字节,一般CPU缓存行为64字节)减少数组内元素的缓存竞争(伪共享)。
初始无竞争时通过base字段计数。自旋锁cellsBusy用于初始化或者调整Cell数组大小。

LongAdder add函数流程: CAS更新base字段;失败更新CAS Cell数组中hash对应的Cell值;若Cell数组不存在 或者 CAS仍旧失败的话,转入Strip64中longAccumulate函数。

Strip64 longAccumulate函数:

  1. 若Probe为默认值0,强制初始化,probe绑定在Thread对象中;进入循环
    2.1. 若Cell数组非空
    2.1.1. 若Cell[h]==null 并且 自旋锁CAS成功 时,新建Cell对象; 否则记录冲突,并重新hash(重新hash的值会更新在Thread字段中)
    2.1.2. 若之前进入函数之前CAS失败,重新hash
    2.1.3. CAS对应Cell的value,失败的话,重新循环
    2.1.4. 若Cells大于CPU核心数,或者在本次循环中已变动,记录冲突,并重新hash
    2.1.5. 若前次循环存在冲突,清除冲突并重新hash
    2.1.6. CAS自旋锁,成功后扩展Cells数组
    2.2. 若cellsBusy为0 并且 Cell数组为null,CAS自旋锁,成功后,初始化Cell数组;
    2.3. 若Cells为空且自旋锁抢失败的话,回退至base计数,仍旧失败的话,重新循环
    (简化说法:base字段作为无竞争 和 初始化竞争失败 时的计数方式;失败后根据线程的随机Hash CAS更新非null Cell的值,再次失败的话,初始化null的Cell,并配合扩展Cell数组和重新hash解决冲突)