Java并发编程的艺术

Java并发编程的艺术

第一章 并发编程的挑战

第二章 Java底层实现原理

volatile

缓存一致性协议

  • Lock前缀指令会引起处理器缓存回写到内存
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

优化

通过填充缓存行,保证(缓存)锁的粒度

LinkedTransferQueue的代码如下。- JDK7

1
2
3
4
5
6
7
8
9
10
11
12
/** 队列中的头部节点 */
private transient f?inal PaddedAtomicReference<QNode> head;
/** 队列中的尾部节点 */
private transient f?inal PaddedAtomicReference<QNode> tail;
static f?inal class PaddedAtomicReference <T> extends AtomicReference T> {
// 使用很多4个字节的引用追加到64个字节
Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; PaddedAtomicReference(T r) {
super(r); }
}
public class AtomicReference <V> implements java.io.Serializable {
private volatile V value;
// 省略其他代码 }

Synchronized

对象头

三种锁

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽 等于4字节,即32bit,如表2-2所示。

表2-2 Java对象头的长度

image-20230624092519572

image-20230624092652662

原子操作原理

处理机实现方案

–锁总线

总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该 处理器可以独占共享内存。

–锁缓存(内存地址)

通过缓存锁定来保证原子性 :保证对某个内存地址 的操作是原子性即可

“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中

Java实现方案

–CAS

问题

  • ABA问题
  • 空循环
  • 只能保证一个共享变量的原子操作,不满足多个共享变量原子操作

第三章 Java内存模型 - JMM

(理解:Java内存模型 是 为了保证Java线程之间的共享变量( 基于类而言的共享变量)的合理通信)

基础

—问题

通信和同步。同步是基于通信的。

通信:线程之间的通信机制有两种:共享内存消息传递。(命令式编程)

同步:是指程序中用于控制不同线程间操作发生相对顺序的机制。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对 程序员完全透明。

—模型抽象结构

从抽象的角度来看,JMM定义了线程和主内存之间的抽 象关系: 线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地 内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的 一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优 化。Java内存模型的抽象示意如图3-1所示。

image-20230624093058688

图3-1 Java内存模型的抽象结构示意图

—从源代码到指令序列的重排序

重排序分3种类型。

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上 去可能是在乱序执行。

上述的1属于编译器重排序,2和3属于处理器重排序。

  • 对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排 序(不是所有的编译器重排序都要禁止)。
  • 对于处理器重排序,JMM的处理器重排序规则会要 求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

JMM模型通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

表3-3 内存屏障类型表

image-20230624094722081

— Happen-Before

如果一 个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关 系。

与程序员密切相关的happens-before规则如下。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的 读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

重排序

满足要求和规定的才能重排序

—数据依赖性

前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

表3-4 数据依赖类型表

image-20230624100606192

这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作, 不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

—-as-if-serial

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

顺序一致性

总结

—处理器内存模型

根据对不同类型的读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分 为如下几种类型。

  • 放松程序中写-读操作的顺序,由此产生了Total Store Ordering内存模型(简称为TSO)。
  • 在上面的基础上,继续放松程序中写-写操作的顺序,由此产生了Partial Store Order内存模型(简称为PSO)。
  • 在前面两条的基础上,继续放松程序中读-写和读-读操作的顺序,由此产生了RelaxedMemory Order内存模型(简称为RMO)和PowerPC内存模型。

注意,这里处理器对读/写操作的放松,是以两个操作之间不存在数据依赖性为前提的(因为处理器要遵守as-if-serial语义,处理器不会对存在数据依赖性的两个内存操作做重排序)

第四章 并发编程基础-线程

第五章 Java的锁

Lock接口

队列同步器 AQS

重入锁

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

fair

unfair

读写锁

LockSupport工具

当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应 工作。

LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功 能,而LockSupport也成为构建同步组件的基础工具。

Condition接口-监控器

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以 实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等 待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

对比Object的监控器

表5-12 Object的监视器方法与Condition接口的对比

image-20230624102551568

第六章 并发容器和框架

容器

—ConCurrentHashMap

—阻塞队列

JDK 7提供了7个阻塞队列,如下。

  • ·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
  • ·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  • ·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
  • ·DelayQueue:一个使用优先级队列实现的无界阻塞队列。 ·
  • SynchronousQueue:一个不存储元素的阻塞队列。 ·
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • ·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

框架Fork/Join

第七章 原子操作类

基本类型

使用原子的方式更新基本类型,Atomic包提供了以下3个类。

·AtomicBoolean:原子更新布尔类型。

·AtomicInteger:原子更新整型。

·AtomicLong:原子更新长整型。

数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类。

  • ·AtomicIntegerArray:原子更新整型数组里的元素。
  • ·AtomicLongArray:原子更新长整型数组里的元素。
  • ·AtomicReferenceArray:原子更新引用类型数组里的元素。
  • ·AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,

其常用方法如下。

  • ·int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
  • ·boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需

要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。

  • ·AtomicReference:原子更新引用类型。
  • ·AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

字段

Atomic包提供

了以下3个类进行原子字段更新。

  • ·AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
  • ·AtomicLongFieldUpdater:原子更新长整型字段的更新器。

·AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起 来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的 ABA问题。

第八章 并发工具类

等待多线程完成的CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

假如有这样一个需求:我们需要解析一个Excel里多个sheet的数据,此时可以考虑使用多 线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完 成。在这个需求中,要实现主线程等待所有线程完成sheet的解析操作,最简单的做法是使用 join()方法,如代码清单8-1所示。

同步屏障CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会 开门,所有被屏障拦截的线程才会继续运行。

控制并发线程数的Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以

保证合理的使用公共资源。

线程间交换数据的Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交 换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过 exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也 执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产 出来的数据传递给对方。

第九章 线程池

第十章 Executor框架

在Java中,使用线程来异步执行任务。Java线程的创建与销毁需要一定的开销,如果我们 为每一个任务创建一个新线程来执行,这些线程的创建与销毁将消耗大量的计算资源。同时, 为每一个任务创建一个新线程来执行,这种策略可能会使处于高负荷状态的应用最终崩溃

Java的线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开 来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

ThreadPoolExecutor

Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类,主要由下列4个组

件构成。

·corePool:核心线程池的大小。

·maximumPool:最大线程池的大小。

·BlockingQueue:用来暂时保存任务的工作队列。

·RejectedExecutionHandler:当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和 时(达到了最大线程池大小且工作队列已满),execute()方法将要调用的Handler。

·通过Executor框架的工具类Executors,可以创建3种类型的ThreadPoolExecutor。 ·FixedThreadPool。
·SingleThreadExecutor。
·CachedThreadPool。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运 行任务,或者定期执行任务。ScheduledThreadPoolExecutor的功能与Timer类似,但 ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而 ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。

FutureTask

tureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给 Executor执行,也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行 的时机,FutureTask可以处于下面3种状态。

1)未启动。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一 个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态。

2)已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。 3)已完成。FutureTask.run()方法执行完后正常结束,或被取消(FutureTask.cancel(…)),或

执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态。

第十一章


Java并发编程的艺术
http://example.com/2023/06/24/书籍-笔记/Java并发编程的艺术/
作者
where
发布于
2023年6月24日
许可协议