技术-202111-《并发编程的艺术》

创建时间: 2021/11/8 20:46
更新时间: 2022/8/22 19:31
作者: HelloXF
标签: 知识库

第一章 并发编程的挑战

多线程弊端:

比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题,线程创建的开销。

上下文切换(分配时间片,读取执行状态)

单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现

这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切

换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

无锁并发编程。 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一

些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

· CAS算法 。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

· 使用最少线程 。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这

样会造成大量线程都处于等待状态。

·协程 :在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

减少上下文切换实战:

通过减少线上大量WAITING的线程,来减少上下文切换次数

第一步:用jstack命令dump线程信息,看看pid为3117的进程里的线程都在做什么。

sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17

第二步:统计所有线程分别处于什么状态,发现300多个线程处于WAITING(onobject-

monitor)状态。

$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c

第三步:打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么。发现这些线

程基本全是JBOSS的工作线程,在await。说明JBOSS线程池里线程接收到的任务太少,大量线

程都闲着

第四步:减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到100

第五步:重启JBOSS,再dump线程信息,然后统计WAITING(onobjectmonitor)的线程,发现减少了175个。WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从WAITTING到RUNNABLE都会进行一次上下文的切换。读者也可以使用vmstat命令测试一下。

死锁

线程间互相持有对方的锁

避免死锁的几个常见方法。

·避免一个线程同时获取多个锁。

·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

资源限制的挑战

硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接

数和socket连接数等。

在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。

解决方式

比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这

笔数据。

对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket

连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。

如何在资源限制的情况下,让程序执行得更快呢?方法就是,根据不同的资源限制调整

程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作

时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则

某些线程会被阻塞,等待数据库连接

第2章并发机制的底层原理

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节

码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和

CPU的指令。

volatile的应用

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

volatile的定义与实现原理

Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的

表2-1 CPU的术语定义

volatile的两条实现原则

Lock前缀指令会引起处理器缓存回写到内存

一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

synchronized的实现原理与应用

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现

为以下3种形式。

·对于普通同步方法,锁是当前实例对象。

·对于静态同步方法,锁是当前类的Class对象。

·对于同步方法块,锁是Synchonized括号里配置的对象。

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

对象头

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

锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE
1.6中,锁一共有4种状态,级别从低到高依次是: 无锁状态、偏向锁状态、轻量级锁状态和重量级锁
状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

在运行期间,MarkWord里存储的数据会随着锁标志位的变化而变化。MarkWord可能变

化为存储以下4种数据,如表2-4所示。

偏向锁

![](2_files/ScreenClip [1].png)

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,

持有偏向锁的线程才会释放锁。

轻量级锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录中,官方称为DisplacedMarkWord。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级解锁 时,会使用原子的CAS操作将DisplacedMarkWord替换回到对象头,如果成

功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是两个线程同时争夺锁,导致锁膨胀的流程图。

![](2_files/Image [1].png)

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

锁的优缺点对比

![](2_files/Image [2].png)

原子操作的实现原理

原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作atomicoperation)意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。

处理器如何实现原子操作

(1)通过总线锁保证原子性。

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

(2)使用缓存锁保证原子性

同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

如何实现原子操作

在Java中可以通过锁和循环CAS的方式来实现原子操作。

(2)CAS实现原子操作的三大问题

1)ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java
1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

(3) 使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

第3章Java内存模型

内存模型基础

Java线程之间的通信对程序员完全透明, 内存可见性
问题很容易困扰Java程序员,本章将揭开Java内存模型神秘的面纱。本章大致分4部分:Java内存模型的基础,主要介绍内存模型相关的基本概念;Java内存模型中的
顺序一致性
,主要介绍重排序与顺序一致性内存模型;同步原语,主要介绍3个同步原语(synchronized、volatile和final)的内存语义及
重排序规则 在处理器中的实现;Java内存模型的设计,主要介绍Java内存模型的设计原理,及其与处理器内存模型和顺序一致性内存模型的关系。

线程之间的通信机制有两种:共享内存和消息传递

在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-
读内存中的公共状态进行隐式通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。

![](2_files/Image [3].png)

从图3-1来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2)线程B到主内存中去读取线程A之前已更新过的共享变量。

![](2_files/Image [4].png)

如图3-2所示,本地内存A和本地内存B由主内存中共享变量x的副本。假设初始时,这3个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证

内存可见性

一个线程修改了共享变量的值,其他线程也能看到最新修改的值 。

指令序列的重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。

1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-
LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图3-3所示。

![](2_files/Image [5].png)

上述的 1属于编译器重排序,2和3属于处理器重排序
。这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的
内存屏障( Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

并发编程模型的分类

happens-before简介

一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-
before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间

就存在数据依赖性。

as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)

程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

程序顺序规则

顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。

数据竞争与顺序一致性

顺序一致性内存模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性。

1)一个线程中的所有操作必须按照程序的顺序来执行。

2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

![](2_files/Image [6].png)

从这里我们可以看到,JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。

volatile的内存语义

当声明共享变量为volatile后,对这个变量的读/写将会很特别。为了揭开volatile的神秘面纱,下面将介绍volatile的内存语义及volatile内存语义的实现。

volatile的特性

理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。

简而言之,volatile变量自身具有下列特性。

·可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

·原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写-读建立的happens-before关系

上面讲的是volatile变量自身的特性,对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们去关注。

从JSR-133开始(即从JDK5开始),volatile变量的写-读可以实现线程之间的通信。

从内存语义的角度来说,volatile的写-读与锁的释放-
获取有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。

class VolatileExample { int a = 0; volatile boolean flag = false; public void
writer() { a = 1; // 1 flag = true; // 2 }public void reader() {if (flag) { //
3 int i = a; // 4…… }}}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个

过程建立的happens-before关系可以分为3类:

1)根据程序次序规则,1 happens-before 2;3 happens-before 4。

2)根据volatile规则,2 happens-before 3。

3)根据happens-before的传递性规则,1 happens-before 4。

上述happens-before关系的图形化表现形式如下。

![](2_files/Image [7].png)

锁的内存语义

众所周知,锁可以让临界区互斥执行。这里将介绍锁的另一个同样重要,但常常被忽视的功能:锁的内存语义。

锁的释放-获取建立的happens-before关系

锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

锁的释放和获取的内存语义

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的

临界区代码必须从主内存中读取共享变量。图3-26是锁获取的状态示意图。

对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有

相同的内存语义;锁获取与volatile读有相同的内存语义。

下面对锁释放和锁获取的内存语义做个总结。

·线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。

·线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。·线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

![](2_files/Image [8].png)

锁内存语义的实现

本文将借助ReentrantLock的源代码,来分析锁内存语义的具体实现机制。

![](2_files/Image [9].png)

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(本文简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,马上我们会看到,这个volatile变量是ReentrantLock内存语义实现的关键。

AQS类图

![](2_files/Image [10].png)

ReentrantLock分为公平锁和非公平锁,我们首先分析公平锁。

使用公平锁时,加锁方法lock()调用轨迹如下。

1)ReentrantLock:lock()。

2)FairSync:lock()。

3)AbstractQueuedSynchronizer:acquire(int arg)。

4)ReentrantLock:tryAcquire(int acquires)。

在使用公平锁时,解锁方法unlock()调用轨迹如下。

1)ReentrantLock:unlock()。

2)AbstractQueuedSynchronizer:release(int arg)。

3)Sync:tryRelease(int releases)

非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。使用非公平锁时,加锁方法lock()调用轨迹如下。

1)ReentrantLock:lock()。

2)NonfairSync:lock()。

3)AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)

intel的手册对lock前缀的说明如下。

1)确保对内存的读-改-
写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium
4、Intel Xeon及P6处理器开始,Intel使用缓存锁定(Cache
Locking)来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。

2)禁止该指令,与之前和之后的读和写指令重排序。

3)把写缓冲区中的所有数据刷新到内存中。

上面的第2点和第3点所具有的内存屏障效果,足以同时实现volatile读和volatile写的内存语义。

经过上面的分析,现在我们终于能明白为什么JDK文档说CAS同时具有volatile读和volatile写的内存语义了。

现在对公平锁和非公平锁的内存语义做个总结。

·公平锁和非公平锁释放时,最后都要写一个volatile变量state。

·公平锁获取时,首先会去读volatile变量。

·非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

从本文对ReentrantLock的分析可以看出, 锁释放-获取的内存语义的实现 至少有下面两种

方式。

1)利用volatile变量的写-读所具有的内存语义。、

2)利用CAS所附带的volatile读和volatile写的内存语义。

concurrent包的实现

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如3-28所示。

![](2_files/Image [11].png)

final域的内存语义

与前面介绍的锁和volatile相比,对final域的读和写更像是普通的变量访问。

happens-before

happens-before是JMM最核心的概念。对应Java程序员来说,理解happens-before是理解JMM的关键。

《JSR-133:Java Memory Model and Thread Specification》定义了如下happens-before规则。

1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-
before于线程B中的任意操作。

6)join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-
before于线程A从ThreadB.join()操作成功返回。

·1 happens-before
4是由传递性规则产生的。这里的传递性是由volatile的内存屏障插入策略和volatile的编译器重排序规则共同来保证的。

下面我们来看start()规则。假设线程A在执行的过程中,通过执行ThreadB.start()来启动线程B;同时,假设线程A在执行ThreadB.start()之前修改了一些共享变量,线程B在开始执行后会读这些共享变量。

下面我们来看join()规则。假设线程A在执行的过程中,通过执行ThreadB.join()来等待线

程B终止;同时,假设线程B在终止之前修改了一些共享变量,线程A从ThreadB.join()返回后会

读这些共享变量。

双重检查锁定与延迟初始化

![](2_files/Image [12].png)

由于对getInstance()方法做了同步处理,synchronized将导致性能开销。如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。

下面是使用双重检查锁定来实现延迟初始化的示例代码。

![](2_files/Image [13].png)

如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美

基于volatile的解决方案

对于前面的基于双重检查锁定来实现延迟初始化的方案(指DoubleCheckedLocking示例代码),只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。请看下面的示例代码。

![](2_files/Image [14].png)

基于类初始化的解决方案

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案(这个方案被称之为Initialization
On Demand Holder idiom)。

![](2_files/Image [15].png)![](2_files/Image [16].png)

Java内存模型综述

处理器的内存模型

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

·放松程序中写-读操作的顺序,由此产生了Total Store Ordering内存模型(简称为TSO)。

·在上面的基础上,继续放松程序中写-写操作的顺序,由此产生了Partial Store Order内存模型(简称为PSO)。

·在前面两条的基础上,继续放松程序中读-写和读-读操作的顺序,由此产生了Relaxed Memory
Order内存模型(简称为RMO)和PowerPC内存模型。

JMM的内存可见性保证

按程序类型,Java程序的内存可见性保证可以分为下列3类。

  1. ·单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

  2. ·正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。

  3. ·未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0、null、false)

第4章Java并发编程基础

线程简介

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是 线程
,也叫轻量级进程(Light Weight
Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。下面使用JMX来查看一个普通的Java程序包含哪些线程,如代码清单4-1所示。

![](2_files/Image [17].png)

线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线

程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程

分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需

要多或者少分配一些处理器资源的线程属性。

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线

程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分

配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操

作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较

低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,

有些操作系统甚至会忽略对线程优先级的设定

线程的状态

Java线程在运行的生命周期中可能处于表4-1所示的6种不同的状态,在给定的一个时刻,

线程只能处于其中的一个状态。

![](2_files/Image [18].png)

下面我们使用jstack工具(可以选择打开终端,键入jstack或者到JDK安装目录的bin目录下

执行命令),尝试查看示例代码运行时的线程信息,更加深入地理解线程状态,示例如代码清

单4-3所示。

public class ThreadState {

public static void main(String[] args) {

new Thread(new TimeWaiting (), "TimeWaitingThread").start();

new Thread(new Waiting(), "WaitingThread").start();

//使用两个Blocked线程,一个获取锁成功,另一个被阻塞

new Thread(new Blocked(), "BlockedThread-1").start();

new Thread(new Blocked(), "BlockedThread-2").start();

}

//该线程不断地进行睡眠

static class TimeWaiting implements Runnable {

@Override

public void run() {

while (true) {

SleepUtils.second(100);

}}}

//该线程在Waiting.class实例上等待

static class Waiting implements Runnable {@Override

public void run() {

while (true) {

synchronized (Waiting.class) {

try {

Waiting.class.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}}}}}

//该线程在Blocked.class实例上加锁后,不会释放该锁

static class Blocked implements Runnable {

public void run() {

synchronized (Blocked.class) {

while (true) {

SleepUtils.second(100);

}}}}}

上述示例中使用的SleepUtils如代码清单4-4所示。

代码清单4-4 SleepUtils.java

public class SleepUtils {

public static final void second(long seconds) {

try {

TimeUnit.SECONDS.sleep(seconds);

} catch (InterruptedException e) {

}}}

运行该示例,打开终端或者命令提示符,键入“jps”,输出如下。

611

935 Jps

929 ThreadState

270

可以看到运行示例对应的进程ID是929,接着再键入“jstack 929”(这里的进程ID需要和读者自己键入jps得出的ID一致),

通过示例,我们了解到Java程序运行中线程状态的具体含义。线程在自身的生命周期中,

并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态

变迁如图4-1示。

![](2_files/Image [19].png)

由图4-1中可以看到,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。

注意 Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,
但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态
,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

启动和终止线程

在前面章节的示例中通过调用线程的start()方法进行启动,随着run()方法的执行完毕,线程也随之终止

构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。代码清单4-6所示的代码摘自java.lang.Thread中对线程进行初始化的部分。

![](2_files/Image [20].png)

在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。

启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义

是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用

start()方法的线程。

线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

volatile和synchronized关键字

对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

![](2_files/Image [21].png)

从图4-2中可以看到,任意线程对Object(Object由synchronized保护)的访问,首先要获得

Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object

的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新

尝试对监视器的获取。

等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法和描述如表4-2所示。

![](2_files/Image [22].png)

调用wait()、notify()以及notifyAll()时需要注意的细节,如下。

1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。

2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。

3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。

4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。

5)从wait()方法返回的前提是获得了调用对象的锁。

从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。

![](2_files/Image [23].png)

在图4-3中,WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。

Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long
millis)和join(long millis,int
nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。

![](2_files/Image [24].png)![](2_files/Image [25].png)

从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结

束通知)。

ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这

个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个

线程上的一个值。

4.4线程应用实例

等待超时模式

数据库连接池

线程池技术

线程池技术能够很好地解决这个问题,它预先创建了若干数量的线程,并且不能由用户直接对线程的创建进行控制,在这个前提下重复使用固定或较为固定数目的线程来完成任务的执行。这样做的好处是,一方面,消除了频繁创建和消亡线程的系统资源开销,另一方面,面对过量任务的提交能够平缓的劣化。

客户端可以通过execute(Job)方法将Job提交入线程池执行,而客户端自身不用等待Job的执行完成。除了execute(Job)方法以外,线程池接口提供了增大/减少工作者线程以及关闭线程池的方法。这里工作者线程代表着一个重复执行Job的线程,而每个由客户端提交的Job都将进入到一个工作队列中等待工作者线程的处理。
接下来是线程池接口的默认实现,示例如代码清单4-20所示。

public class DefaultThreadPool implements
ThreadPool {

//线程池最大限制数

private static final int MAX_WORKER_NUMBERS = 10;

//线程池默认的数量

private static final int DEFAULT_WORKER_NUMBERS = 5;

//线程池最小的数量

private static final int MIN_WORKER_NUMBERS = 1;

//这是一个工作列表,将会向里面插入工作

private final LinkedList jobs = new LinkedList();

//工作者列表

private final List workers = Collections.synchronizedList(new

ArrayList());

//工作者线程的数量

private int workerNum = DEFAULT_WORKER_NUMBERS;

//线程编号生成

private AtomicLong threadNum = new AtomicLong();

public DefaultThreadPool() {

initializeWokers(DEFAULT_WORKER_NUMBERS);

}

public DefaultThreadPool(int num) {

workerNum = num > MAX_WORKER_NUMBERS MAX_WORKER_NUMBERS : num < MIN_WORKER_

NUMBERS MIN_WORKER_NUMBERS : num;

initializeWokers(workerNum);

}

public void execute(Job job) {

if (job != null) {

//添加一个工作,然后进行通知

synchronized (jobs) {

jobs.addLast(job);

jobs.notify();

}}}

public void shutdown() {

for (Worker worker : workers) {

worker.shutdown();

}

}

public void addWorkers(int num) {

synchronized (jobs) {

//限制新增的Worker数量不能超过最大值

if (num + this.workerNum > MAX_WORKER_NUMBERS) {

num = MAX_WORKER_NUMBERS - this.workerNum;

}

initializeWokers(num);

this.workerNum += num;}

}

public void removeWorker(int num) {

synchronized (jobs) {

if (num >= this.workerNum) {

throw new IllegalArgumentException("beyond workNum");

}

//按照给定的数量停止Worker

int count = 0;

while (count < num) {

Worker worker = workers.get(count)

if (workers.remove(worker)) {

worker.shutdown();

count++;

}}

this.workerNum -= count;

}}

public int getJobSize() {

return jobs.size();

}

//初始化线程工作者

private void initializeWokers(int num) {

for (int i = 0; i < num; i++) {

Worker worker = new Worker();

workers.add(worker);

Thread thread = new Thread(worker, "ThreadPool-Worker-" + threadNum.

incrementAndGet());

thread.start();

}}

//工作者,负责消费任务

class Worker implements Runnable {

//是否工作

private volatile boolean running = true;

public void run() {

while (running) {

Job job = null;

synchronized (jobs) {

//如果工作者列表是空的,那么就wait

while (jobs.isEmpty()) {

try {

jobs.wait();

} catch (InterruptedException ex) {

//感知到外部对WorkerThread的中断操作,返回

Thread.currentThread().interrupt();

return;

}}

//取出一个Job

job = jobs.removeFirst();

}

if (job != null) {

try {

job.run();

} catch (Exception ex) {

//忽略Job执行中的Exception}

}}}

public void shutdown() {

running = false;

}}}

从线程池的实现可以看到,当客户端调用execute(Job)方法时,会不断地向任务列表jobs中添加Job,而每个工作者线程会不断地从jobs上取出一个Job进行执行,当jobs为空时,工作者线程进入等待状态。

添加一个Job后,对工作队列jobs调用了其notify()方法,而不是notifyAll()方法,因为能够确定有工作者线程被唤醒,这时使用notify()方法将会比notifyAll()方法获得更小的开销(避免将等待队列中的线程全部移动到阻塞队列中)。

可以看到,线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者线程,随着大量的任务被提交,更多的工作者线程会被唤醒

一个基于线程池技术的简单Web服务器

下面通过使用前一节中的线程池来构造一个简单的Web服务器,这个Web服务器用来处理HTTP请求,这个Web服务器使用main线程不断地接受客户端Socket的连接,将连接以及请求提交给线程池处理,这样使得Web服务器能够同时处理多个客户端请求,示例如代码清单4-21所示

代码清单4-21 SimpleHttpServer.java

public class SimpleHttpServer {

//处理HttpRequest的线程池

static ThreadPool threadPool = new DefaultThreadPool

(1);

// SimpleHttpServer的根路径

static String basePath;

static ServerSocket serverSocket;

//服务监听端口

static int port = 8080;

public static void setPort(int port) {

if (port > 0) {

SimpleHttpServer.port = port;

}}

public static void setBasePath(String basePath) {

if (basePath != null && new File(basePath).exists() && new File(basePath).

isDirectory()) {

SimpleHttpServer.basePath = basePath;

}}

//启动SimpleHttpServer

public static void start() throws Exception {

serverSocket = new ServerSocket(port);Socket socket = null;

while ((socket = serverSocket.accept()) != null) {

//接收一个客户端Socket,生成一个HttpRequestHandler,放入线程池执行

threadPool.execute(new HttpRequestHandler(socket));

}

serverSocket.close();

}

static class HttpRequestHandler implements Runnable {

private Socket socket;

public HttpRequestHandler(Socket socket) {

this.socket = socket;

}

@Override

public void run() {

String line = null;

BufferedReader br = null;

BufferedReader reader = null;

PrintWriter out = null;

InputStream in = null;

try {

reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

String header = reader.readLine();

//由相对路径计算出绝对路径

String filePath = basePath + header.split(" ")[1];

out = new PrintWriter(socket.getOutputStream());

//如果请求资源的后缀为jpg或者ico,则读取资源并输出

if (filePath.endsWith("jpg") || filePath.endsWith("ico")) {

in = new FileInputStream(filePath);

ByteArrayOutputStream baos = new ByteArrayOutputStream();

int i = 0;

while ((i = in.read()) != -1) {

baos.write(i);

}

byte[] array = baos.toByteArray();

out.println("HTTP/1.1 200 OK");

out.println("Server: Molly");

out.println("Content-Type: image/jpeg");

out.println("Content-Length: " + array.length);

out.println("");

socket.getOutputStream().write(array, 0, array.length);

} else {

br = new BufferedReader(new InputStreamReader(new

FileInputStream(filePath)));

out = new PrintWriter(socket.getOutputStream());

out.println("HTTP/1.1 200 OK");

out.println("Server: Molly");

out.println("Content-Type: text/html; charset=UTF-8");

out.println("");

while ((line = br.readLine()) != null) {

out.println(line);

}}

out.flush();

} catch (Exception ex) {

out.println("HTTP/1.1 500");

out.println("");

out.flush();

} finally {close(br, in, reader, out, socket);

}}}

//关闭流或者Socket

private static void close(Closeable... closeables) {

if (closeables != null) {

for (Closeable closeable : closeables) {

try {

closeable.close();

} catch (Exception ex) {

}}}}}

在图4-4中,SimpleHttpServer在建立了与客户端的连接之后,并不会处理客户端的请求,而是将其包装成HttpRequestHandler并交由线程池处理。在线程池中的Worker处理客户端请求的同时,SimpleHttpServer能够继续完成后续客户端连接的建立,不会阻塞后续客户端的请求。

![](2_files/Image [26].png)

随着线程池中线程数量的增加,SimpleHttpServer的吞吐量不断增大,响应时间不断变小,线程池的作用非常明显。

但是,线程池中线程数量并不是越多越好,具体的数量需要评估每个任务的处理时间,以及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加系统的无故开销,起到相反的作用。

第5章Java中的锁

Lock接口

一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java
SE
5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以

及超时获取锁等多种synchronized关键字所不具备的同步特性。

使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。
这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。

Lock的使用也很简单,代码清单5-1是Lock的使用的方式。

Lock lock = new ReentrantLock();

lock.lock();

try {

} finally {

lock.unlock();

}

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放

![](2_files/Image [27].png)![](2_files/Image [28].png)

这里先简单介绍一下Lock接口的API,随后的章节会详细介绍同步器 AbstractQueuedSynchronizer 以及常用Lock接口的实现
ReentrantLock 。Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug
Lea)期望它能够成为实现大部分同步需求的基础。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int
newState)和compareAndSetState(int expect,int
update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

重入锁

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

当一个线程调用Mutex的lock()方法获取锁之后,如果再次调用lock()方法,则该线程将会被自己所阻塞,原因是Mutex在实现tryAcquire(int
acquires)方法时没有考虑占有锁的线程再次获取锁的场景,而在调用tryAcquire(int
acquires)方法时返回了false,导致该线程被阻塞。简单地说,Mutex是一个不支持重进入的锁。
而synchronized关键字隐式的支持重进入
,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,而不像Mutex由于获取了锁,而在下一次获取锁时出现阻塞自己的情况。

ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

实现重进入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

1) 线程再次获取锁 。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。

2) 锁的最终释放
。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

ReentrantLock是通过 组合自定义同步器来实现锁的获取与释放 ,以非公平性(默认的)实现为例,获取同步状态的代码如代码清单5-12所示

![](2_files/Image [29].png)

公平与非公平获取锁的区别

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。

在测试中公平性锁与非公平性锁相比,总耗时是其94.3倍,总切换次数是其133倍。可以看出,公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

读写锁

之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升

一般情况下, 读写锁的性能都会比排它锁好,
因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock,它提供的特性如表5-8所示。

表5-8 ReentrantReadWriteLock的特性

![](2_files/Image [30].png)

读写锁的接口与示例

ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()

读写状态的设计

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0

时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。

Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、

wait(long
timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

![](2_files/Image [31].png)

Condition的使用方式比较简单,需要注意在调用方法前获取锁,使用方式如代码清单5-20

所示。

Lock lock = new ReentrantLock();

Condition condition = lock.newCondition();

public void conditionWait() throws InterruptedException {

lock.lock();

try {

condition.await();

} finally {

lock.unlock();

}

}

public void conditionSignal() throws InterruptedException {

lock.lock();

try {

condition.signal();

} finally {

lock.unlock();

}

}

如示例所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会

释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程

才从await()方法返回,并且在返回前已经获取了锁。

表5-13 Condition的(部分)方法以及描述

![](2_files/Image [32].png)

获取一个Condition必须通过Lock的newCondition()方法。

等待

调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释

放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相

关联的锁。

通知

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在

唤醒节点之前,会将节点移到同步队列中。

第6章Java并发容器和框架

ConcurrentHashMap的实现原理与使用

ConcurrentHashMap是线程安全且高效的HashMap。本节让我们一起研究一下该容器是如何在保证线程安全的同时又能保证高效的操作。

在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会。

(1)线程不安全的HashMap

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

(2)效率低下的HashTable

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

(3)ConcurrentHashMap的锁分段技术可有效提升并发访问率

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。
首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap的结构

通过ConcurrentHashMap的类图来分析ConcurrentHashMap的结构,如图6-1所示。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁,如图6-2所示

图6-1 ConcurrentHashMap的类图

![](2_files/Image [33].png)![](2_files/Image [34].png)

ConcurrentHashMap的初始化

ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel等几个参数来初始化segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的

HashEntry数组来实现的。

get操作

Segment的get操作实现非常简单和高效。先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素

get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile类型,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是因为根据Java内存模型的happen
before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

put操作

由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里

(1)是否需要扩容

在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。

(2)如何扩容

在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

size操作

最安全的做法是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效。因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以

ConcurrentHashMap的做法是 先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如

果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小
。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

ConcurrentLinkedQueue

如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。

ConcurrentLinkedQueue是一个 基于链接节点的无界线程安全队列 ,它采用 先进先出
的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了 “wait-
free”算法(即CAS算法)来实现
,该算法在Michael&Scott算法上进行了一些修改。

![](2_files/Image [35].png)

ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和

指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一

张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。

Java中的阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

Java里的阻塞队列

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

·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。

·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。

·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。

·DelayQueue:一个使用优先级队列实现的无界阻塞队列。

·SynchronousQueue:一个不存储元素的阻塞队列。

·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

阻塞队列的实现原理

如果队列是空的,消费者会一直等待,当生产者添加元素时,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,如何让生产者和消费者进行高效率的通信呢?让我们先来看看JDK是如何实现的。

使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

发现ArrayBlockingQueue使用了Condition来实现,代码如下。

当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过 LockSupport.park(this) 来实现。

park这个方法会阻塞当前线程,只有以下4种情况中的一种发生时,该方法才会返回。

·与park对应的unpark执行或已经执行时。“已经执行”是指unpark先执行,然后再执行park的情况。

·线程被中断时。

·等待完time参数指定的毫秒数时。

·异常现象发生时,这个异常现象没有任何原因。

Fork/Join框架

什么是Fork/Join框架

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

![](2_files/Image [36].png)

工作窃取算法

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。那么,为什么

需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,可以把这个任务分割为若干

互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个

队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。比如A线程负责处理A

队列里的任务。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有

任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列

里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被

窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿

任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

工作窃取算法的优点:充分利用线程进行并行计算,减少了线程间的竞争。工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并

且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。

Fork/Join框架的实现原理

ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。

第7章Java中的13个原子操作类

原子更新基本类型类

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

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

·AtomicInteger:原子更新整型。

·AtomicLong:原子更新长整型。

AtomicInteger的常用方法如下。

·int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的

value)相加,并返回结果。

·boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方

式将该值设置为输入的值。

·int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。

·void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他

线程在之后的一小段时间内还是可以读到旧的值

·int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值

那么getAndIncrement是如何实现原子操作的呢?让我们一起分析其实现原理,

getAndIncrement的源码如代码清单7-2所示

public final int getAndIncrement() {

for (;😉 {

int current = get();

int next = current + 1;

if (compareAndSet(current, next))

return current;

}

}

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

源码中for循环体的第一步先取得AtomicInteger里存储的数值,第二步对AtomicInteger的当

前数值进行加1操作,关键的第三步调用compareAndSet方法来进行原子更新操作,该方法先检

查当前数值是否等于current,等于意味着AtomicInteger的值没有被其他线程修改过,则将

AtomicInteger的当前数值更新成next的值,如果不等compareAndSet方法会返回false,程序会进

入for循环重新进行 compareAndSet 操作。

Atomic包提供了3种基本类型的原子更新,但是Java的基本类型里还有char、float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe实现的
,都是 CAS实现

原子更新数组

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

·AtomicIntegerArray:原子更新整型数组里的元素。

·AtomicLongArray:原子更新长整型数组里的元素。

·AtomicReferenceArray:原子更新引用类型数组里的元素。

·AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下。

·int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。

·boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子

方式将数组位置i的元素设置成update值。

以上几个类提供的方法几乎一样,所以本节仅以AtomicIntegerArray为例进行讲解,

需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组

复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

原子更新引用类型

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

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

·AtomicReference:原子更新引用类型。

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

·AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类

型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean

initialMark)。

原子更新字段类

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供

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

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

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

·AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起

来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的

ABA问题。

第8章Java中的并发工具类

等待多线程完成的CountDownLatch

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

最简单的做法是使用join()方法

![](2_files/Image [37].png)

join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存

活,如果join线程存活则让当前线程永远等待。其中,wait(0)表示永远等待下去,代码片段如

下。

直到join线程中止后,线程的this.notifyAll()方法会被调用,调用notifyAll()方法是在JVM里

实现的,所以在JDK里看不到,大家可以查看JVM源码。

同步屏障CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一

组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会

开门,所有被屏障拦截的线程才会继续运行。

控制并发线程数的Semaphore

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

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

线程间交换数据的Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交

换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过

exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也

执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产

出来的数据传递给对方。

Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换

两人的数据,并使用交叉规则得出2个交配结果。Exchanger也可以用于校对工作,比如我们需

要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行

录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否

录入一致

第9章Java中的线程池

合理地使用线程池能够带来3个好处。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

线程池的实现原理

从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。

1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。

2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

ThreadPoolExecutor执行execute()方法的示意图,如图9-2所示。

![](2_files/Image [38].png)![](2_files/Image [39].png)

ThreadPoolExecutor执行execute方法分下面4种情况。

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用

RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁

public void execute(Runnable command) {

if (command == null)

throw new NullPointerException();

//如果线程数小于基本线程数,则创建线程并执行当前任务

if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {

//如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。

if (runState == RUNNING && workQueue.offer(command)) {

if (runState != RUNNING || poolSize == 0)

ensureQueuedTaskHandled(command);

}

//如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,

//则创建一个线程执行任务。

else if (!addIfUnderMaximumPoolSize(command))

//抛出RejectedExecutionException异常

reject(command); // is shutdown or saturated

}

}

工作线程:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行。我们可以从Worker类的run()方法里看到这点。

![](2_files/Image [40].png)

线程池中的线程执行任务分两种情况,如下。

1)在execute()方法中创建一个线程时,会让这个线程执行当前任务。

2)这个线程执行完上图中1的任务后,会反复从BlockingQueue获取任务来执行。

线程池的使用

线程池的创建

我们可以通过ThreadPoolExecutor来创建一个线程池。

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,

milliseconds,runnableTaskQueue, handler);

创建一个线程池时需要输入几个参数,如下。

1) corePoolSize(线程池的基本大小) :当提交一个任务到线程池时,线程池会创建一个线

程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任

务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,

线程池会提前创建并启动所有基本线程。

2) runnableTaskQueue(任务队列) :用于保存等待执行的任务的阻塞队列。可以选择以下几

个阻塞队列。

·ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原

则对元素进行排序。

·LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通

常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

·SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用

移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工

厂方法Executors.newCachedThreadPool使用了这个队列。

·PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

3)maximumPoolSize(线程池最大数量) :线程池允许创建的最大线程数。如果队列满了,并

且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如

果使用了无界的任务队列这个参数就没什么效果。

4) ThreadFactory :用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设

置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线

程设置有意义的名字,代码如下。

new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

5) RejectedExecutionHandler(饱和策略) :当队列和线程池都满了,说明线程池处于饱和状

态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法

处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。

·AbortPolicy:直接抛出异常。

·CallerRunsPolicy:只用调用者所在线程来运行任务。

·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

·DiscardPolicy:不处理,丢弃掉。

当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录

日志或持久化存储不能处理的任务。

·keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,

如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

·TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟

(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

向线程池提交任务

可以使用两个方法向线程池提交任务, 分别为execute()和submit()方法。

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

通过以下代码可知execute()方法输入的任务是一个Runnable类的实例

threadsPool.execute(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

}

});

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个

future对象可以判断任务是否执行成功,并且可以通过 future的get()方法来获取返回值 ,get()方

法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线

程一段时间后立即返回,这时候有可能任务没有执行完。

Future

posted on 2023-04-09 13:02  HelloXF_jeff  阅读(19)  评论(0编辑  收藏  举报