前言
操作系统的运行环境
CPU运行模式
在计算机系统中,CPU通常执行两种不同性质的程序,一种是操作系统内核程序,另一种是用户程序。内核程序是用户程序的管理者,因此内核程序可以执行一些特权指令。
- 特权指令:指不允许用户直接使用的指令。
- 非特权指令:指允许用户使用的命令,它不能直接访问系统中的软硬件资源,仅限于访问用户的地址空间,主要是为了防止用户程序对系统造成破坏。
在具体的实现上,将CPU的运行模式实现为用户态和内核态,用户程序运行在用户态,操作系统内核运行在内核态。CPU变态的过程如下:
- 计算机开机时,计算机内核开始运行,CPU处于内核态。
- 内核态 → \rightarrow →用户态:操作系统执行一条特权指令,这个动作意味着操作系统将主动让出CPU使用权。
- 用户态 → \rightarrow →内核态:由异常和中断引发,硬件自动完成变态过程。
异常和中断
异常和中断是CPU从用户态到内核态的唯一途径,当发生异常和中断时,运行在用户态的CPU会立即进入到内核态,这是通过硬件实现的。
当CPU在执行用户程序的第 i i i条指令时检测到一个异常事件,或者在执行第 i i i条指令后发现一个中断请求信号,则CPU打断当前用户程序,然后转到相应的异常或中断处理程序去执行。若异常或中断处理程序能够解决相应的问题,则在异常或中断处理程序的最后,CPU通过执行异常或中断返回指令,回到被打断的用户程序的第i条指令或第 i + 1 i+1 i+1条指令继续执行;若异常或中断处理程序发现是不可恢复的致命错误,则终止用户程序。通常情况下,对异常和中断的具体处理过程由操作系统完成。从CPU检测到异常或中断事件,到调出相应的处理程序,整个过程称为异常和中断的响应。CPU对异常和中断响应的过程可分为以下几个步骤:
- 关中断:在保存断点和程序状态期间,不能被新的中断打断,因此要禁止响应新的中断,即关中断。
- 保存断点和程序状态:为了能在异常和中断处理后正确返回到被中断的程序继续执行,必须将程序的断点和状态送到栈或特定寄存器中。
- 识别异常和中断:异常大多采用软件识别方式,中断可以采用软件识别方式或硬件识别方式。
- 软件识别方式是指CPU设置一个异常状态寄存器,用于记录异常原因。操作系统使用一-个统一的异常或中断查询程序,按优先级顺序查询异常状态寄存器,以检测异常和中断类型,先查询到的先被处理,然后转到内核中相应的处理程序。
- 硬件识别方式又称向量中断,异常或中断处理程序的首地址称为中断向量,所有中断向量都存放在中断向量表中。每个异常或中断都被指定一个中断类型号。在中断向量表中,类型号和中断向量一一对应,因而可以根据类型号快速找到对应的处理程序。
系统调用
系统调用是操作系统提供给应用程序使用的接口,应用程序可以通过系统调用来请求获得操作系统内核的服务。相对于库函数而言,系统调用更为底层:
系统调用按功能大致可分为以下几类:
- 设备管理。完成设备的请求或释放,以及设备启动等功能。
- 文件管理。完成文件的读、写、创建及删除等功能。
- 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
- 进程通信。完成进程之间的消息传递或信号传递等功能。
- 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及始址等功能。
系统调用必须使用某些特权指令才能完成,所以系统调用的处理需要由操作系统内核程序负责完成,要运行在核心态。用户程序可以执行陷入(访管)指令(CPU状态会从用户态进入内核态)来发起系统调用,请求操作系统提供服务。处理完成后,操作系统内核程序又会把CPU的使用权还给用户程序(CPU状态会从核心态回到用户态)。
原语
操作系统结构可以从不同方向划分,但从操作系统的发展来看,从操作系统的内核架构划分的方式得到了长足的发展:
其中原语是一些可以被调用的公用小程序,它们各自完成一个规定的动作,它们的特点如下:
- 处于操作系统的最底层,最接近硬件的部分。
- 原语的运行具有原子性。
- 运行的时间比较短,而且调用频繁。
定义原语的直接方法就是关闭中断,让其所有动作不可分割的完成后再打开中断。
线程
概述
在Java中,当我们启动main()
函数时其实就是启动了一个JVM 进程,而 main()
函数所在的线程就是这个进程中的一个线程,也称主线程。Java线程如何实现并不受JVM规范的约束,它与具体的虚拟机实现相关。以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的。目前使用的两种主要线程库是:POSIX Pthreads、Windows API。Pthreads作为POSIX标准的扩展,可以提供用户级或内核级的库。Windows线程库是用于Windows操作系统的内核级线程库。Java线程API允许线程在Java程序中直接创建和管理。然而,由于JVM实例通常运行在宿主操作系统之上,Java 线程API通常采用宿主系统的线程库来实现,因此在Windows系统中Java线程通常采Windows API来实现,在类UNIX系统中采用Pthreads来实现。
JVM进程模型
下图是一个Java进程运行时的JVM(jdk8)内存模型,一个进程中可以有多个线程,其中堆和元空间是线程共享的,程序计数器、本地方法栈和堆是每个线程都有的。
线程状态
Java线程在生命周期内一定属于以下六种状态中的一个:
- NEW:线程被创建,但还没有调用
start()
方法。 - RUNNABLE:调用
start()
方法之后,该状态包含操作系统层面的就绪状态、运行状态和阻塞状态。 - BLOCKED:当一个线程试图获取一个内部的对象锁,但该锁被其它线程持有时,该线程就会进人该状态。当锁被释放并且线程调度器允许该线程持有它的时候,该线程将返回到原来的状态。
- WAITING:当线程等待某个条件出现时,它自己就进入该状态。
- TIMED_WAITING:当线程等待某个条件出现并设置了最大的等待时间时,它就进入了该状态。
- TERMINATED:当
run()
方法执行结束或因其它原因终止时就进入该状态。
线程控制
线程创建
Thread
在Java中Thread
类表示线程,创建一个线程就是实例化一个Thread
类对象。
Thread(Runnable target) //接收一个任务创建一个线程
Thread(Runnable target, String name) //接收一个任务和线程名字创建一个线程
void start() //启动线程
boolean isAlive() //判断线程是否还活着
static Thread currentThread() //获取当前线程
void setDaemon(boolean on) //将当前线程设置为守护线程
boolean isDaemon() //判断当前线程是否为守护线程
String getName() //获取线程的名字
void setName(String name) //设置线程的名字
线程池
线程池改变了手动创建并管理线程的现状,它将任务和线程解耦,通过复用已有线程来执行任务,极大提高了程序性能。
线程池的构造
所有线程池都是ThreadPoolExecutor
的实例,通过ThreadPoolExecutor
的构造方法,可以了解线程池的构造并自定义线程池。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
maximumPoolSize
和corePoolSize
:corePoolSize
表示核心线程的数量,maximumPoolSize
表示最大线程数量。ThreadPoolExecutor
可以根据和两个参数设置的边界自动调节线程池的大小。当在方法execute(Runnable)
中提交了一个新任务,并且运行的线程少于corePoolSize
,则创建一个新线程来处理请求,即使其它工作线程处于空闲状态。如果运行的线程数大于corePoolSize
但小于maximumPoolSize
,则只有在队列已满时才会创建新线程。keepAliveTime
:如果线程池当前有超过corePoolSize
的线程,并且多余的线程的空闲时间超过keepAliveTime
时将被终止。threadFactory
:如果没有特别指定,则使用Executors.defaultThreadFactory()
。它创建线程池中的所有线程,使其位于同一个ThreadGroup
中,具有相同的NORM_PRIORITY
优先级和非守护进程状态。workQueue
:用于保存未被执行任务的工作队列。handler
:当线程池中的workQueue
已满并且线程数量已到达maximumPoolSize
时,线程池就需要调用handler.rejectedExecution(Runnable r, ThreadPoolExecutor executor)
方法拒绝新提交的任务。线程池预定义了以下四个拒绝策略:AbortPolicy
:直接抛出异常。CallerRunsPolicy
:使用调用execute
方法的线程执行任务。DiscardOldestPolicy
:丢弃workQueue
中最老的任务,将新任务添加到workQueue
中。DiscardPolicy
:丢弃无法处理的任务。
线程池的工作流程
当调用execute
方法提交任务时,线程池会经过以下步骤:
- 如果运行的线程少于
corePoolSize
,线程池会创建一个新线程执行任务。 - 如果
corePoolSize
或更多线程正在运行,线程池会将任务添加到workQueue
中。 workQueue
已满,并且线程数量未到达maximumPoolSize
,继续创建线程执行任务。- 线程数已达
maximumPoolSize
,执行拒绝策略。
钩子方法
线程池提供了以下两个钩子方法,可以在每个任务执行之前和之后调用它们。如果钩子方法抛出异常,内部工作线程可能依次失败并突然终止。
protected void beforeExecute(Thread t, Runnable r)
protected void afterExecute(Runnable r, Throwable t)
队列的维护
方法getQueue
允许访问工作队列,以便进行监视和调试。remove
和purge
方法可用于在大量排队任务被取消时协助进行存储回收。
BlockingQueue<Runnable> getQueue()
void purge()//尝试从工作队列中删除已取消的所有Future任务。
boolean remove(Runnable task)
线程池的终结
程序中不再引用且没有剩余线程的池将自动关闭。但是核心线程一旦创建就不会自动死亡,如果希望即使忘记调用shutdown
也能回收未使用的线程池,那么必须安排未使用的线程最终死亡,方法是使用零个核心线程或设置核心线程的空闲时间。
void allowCoreThreadTimeOut(boolean value)//设置控制核心线程是否会超时并在保持活动时间内没有任务到达时终止的策略,如果新任务到达时需要替换核心线程。当为false时,核心线程永远不会因为缺少传入的任务而终止。当为true时,应用于非核心线程的保持活动策略也应用于核心线程。
预定义线程池
Executors工具类提供了创建预定义线程池的方法:
static ExecutorService newCachedThreadPool()
static ExecutorService newFixedThreadPool(int nThreads)
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
static ExecutorService newSingleThreadExecutor()
static ScheduledExecutorService newSingleThreadScheduledExecutor()
static ExecutorService newWorkStealingPool()
但最好还是使用构造函数创建线程池,因为这种方式会让线程池的使用者更好的了解线程池的构造并且会规避资源耗尽的风险。
线程中断
线程中断是指给某一线程发送一个请求中断信号,而停不停止运行完全取决于被请求线程。在Java中可以通过调用interrupt()
方法对某一线程发送中断请求,当此方法被调用时,被请求线程的中断标志将被设置为true
(这是每一个线程都具有的boolean
标志),在每个线程内都应该不时地检测这个标志,以判断线程是否被请求中断,并决定是否接受请求。
void interrupt() //向线程发送中断请求,并将中断标志设置为true
boolean isInterrupted() //判断是否被请求中断
static boolean interrupted() //判断是否被请求中断并清除标志位
线程阻塞
线程阻塞是指让当前线程进入TIMED_WAATING
或WAITING
状态,当线程进入阻塞状态时,无法响应外部中断请求,那么导致线程阻塞的方式必须解决这一问题,据此可以将阻塞方式分为以下两种:
- 抛出异常式:顾名思义抛出异常的方式在当前线程被请求中断时会抛出一个
InterruptedException
异常。在抛出异常后会将中断标志清空。此类方式如下: - 不抛出异常式:
LockSupport
就是不抛出异常方式的实现,它可以在线程内任意位置让线程阻塞,并且不需要先获取某个对象的锁,也不会抛出InteruptedException
异常。LockSupport
使用了一种名为Permit的许可概念来做到阻塞和唤醒线程,每个线程都只有一个许可,如果许可可用则park
方法就会立即返回并在过程中消耗它,否则就会阻塞,也可以使用unpark
方法获取许可。外部请求中断时park
方法会立即返回,并且不会清空中断标志。
并发编程
JMM
JMM(Java内存模型)类似于操作系统内存模型,它有以下两个作用:
- 抽象了主内存(所有线程创建的对象都必须放在其中)和工作内存(JMM抽象出来的一个概念,每个线程都有一个工作内存,并且线程只能访问自己的工作内存)之间的关系。当线程需要访问共享变量时,必须将共享变量加载到工作内存并保存为一个副本,且线程对共享变量的操作都只能对工作内存中的副本进行。当多个线程同时操作一个共享变量(临界资源)时,就会引发线程安全问题。
- 规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范。为了提升速度和性能,计算机在执行指令时会对指令进行重排序,即计算机在执行代码的时候并不一定是按照我们所写的代码的顺序依次执行。Java 源代码会经历编译器优化重排→指令并行重排→内存系统重排的过程,最终才变成操作系统可执行的指令序列。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致线程安全问题。
解决线程安全的过程就是实现以下三个性质的过程:
- 内存可见性:指某个线程修改了共享变量的值,新值对其它线程来说是立即可见的。
- 有序性:指Java源代码不会被重排序。
- 原子性:指某个线程对共享变量的操作一旦开始就不会被其它线程干扰。
其中,实现原子性一定可以保证线程安全,而实现内存可见性和有序性只在特定情况下才能保证线程安全。相应的,以后者实现的线程安全代价也自然低。
不可变对象
如果一个共享变量在初始化之后就不能被改变,那么这种对象就称为不可变对象,不可变对象一定是线程安全的。不可变对象的实现方式如下:
- 如果共享变量是一个基本类型,那么只要在声明时使用
final
修饰,就可以保证该共享变量是不可变的。 - 如果一个共享变量是对象类型,那么对象自身要保证其行为不会对其状态产生任何影响。例如
String
的实现。
ThreadLocal
如果一个变量是线程独有的、不可共享的,那么这个变量就一定是线程安全的,这种对象称之为非共享对象。通过ThreadLocal
就可以实现非共享对象。每一个Thread
对象内均含有一个ThreadLocalMap
类型的成员变量threadLocals
,ThreadLocalMap
定义在ThreadLocal
中,它存储了以ThreadLocal
为键、Object
为值的键值对,threadLocals
变量的作用是存储线程的非共享对象。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal
的使用流程是这样的:首先通过ThreadLocal
将共享变量包装:
private static ThreadLocal<SharedVariable>threadLocal=new ThreadLocal<SharedVariable>();
然后在每个线程中使用ThreadLocal
提供的get
和set
方法设置共享变量的值。在这个过程中ThreadLocal
充当当前线程中ThreadLocalMap
的访问入口。set
方法的执行过程如下:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//以自身为键,共享变量值为值存放到ThreadLocalMap中
map.set(this, value);
else
createMap(t, value);
}
不难看出ThreadLocal
通过在每个线程的ThreadLocalMap
中保存当前线程设置的共享变量值的方式来实现非共享对象。ThreadLocalMap
通过一个Entry
来保存存入的键和值,Entry
的定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
其中将键实现为弱引用,值实现为强引用,当键被GC自动回收而值不会被回收时就会造成内存泄漏问题。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set
、get
、remove
方法的时候,会清理掉 key
为 null
的记录。因此在使用完ThreadLocal
方法后最好手动调用remove
方法。
volatile
volatile
关键字可以保证共享变量的内存可见性,当一个共享变量被volatile
关键字修饰时,实际就是告诉JVM,这个共享变量是不稳定的,在使用时必须到主存中读取。volatile
关键字也可以实现顺序性,当读写一个被volatile
关键字修饰的共享变量时,volatile
关键字会禁止指令重排序。volatile
底层通过插入内存屏障的方式实现:
- 写屏障:写屏障保证在该屏障之前对共享变量的改动都会同步到主存当中,并且不会进行指令重排。
- 读屏障:读屏障保证在该屏障之后对共享状态的读取都是主存中的最新数据,并且不会进行指令重排。
若想只通过volatile
关键字保证线程安全,必须同时满足以下条件:
- 不能是一个组合的临界变量。
- 运算结果并不依赖共享变量的当前值,或者只能由一条线程修改共享变量的值。
volatile
关键字一个典型的应用就是单例模式,2处实例化对象的过程分为多步:分配内存空间、初始化、将对象地址赋值给INSTANCE
引用变量,如果没有使用volatile
关键字修饰INSTANCE
的话,那么赋值步骤可能会在初始化步骤之前,此时如果另一个线程在1处访问一个未初始化的对象就会产生异常。
public class Singleton {
volatile private static Singleton INSTANCE=null;
public static Singleton getInstance(){
if (INSTANCE==null){//1
synchronized (Singleton.class){
if (INSTANCE==null){
INSTANCE=new Singleton();//2
}
}
}
return INSTANCE;
}
}
原子类
原子类是一些通过CAS保证原子性操作特征的类。CAS是Compare And Swap比较并交换的缩写,在CAS中有三个值:
- Var:要更新的值
- Expected:预期的值
- New:新值
一次CAS操作的流程如下:判断V是否等于E,如果是则将V修改为N,否则就自旋重复这个过程。Java中的CAS由Unsafe
类中的本地方法实现。这些本地方法原子性由操作系统或CPU实现。如果从乐观锁和悲观锁的角度对Java中的锁进行分类,那么对象锁和AQS都是悲观锁,因为它们在访问临界资源时都会先加锁,只有获得锁的线程才能访问临济资源。而CAS是乐观锁,任何线程在访问临界资源的时都不需要加锁,并且只有在满足条件的时候才能修改临界资源,如果不满足条件就会一直尝试,直到满足条件为止。但是,CAS自身还存在着一些问题:
- ABA问题:线程一将共享状态A改成了B然后又改成了A,那么线程二将不会知道线程一的第一次修改。这就是ABA问题。
JUC包下的原子类如下所示:
- 原子整数:
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
- 原子引用:
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- AtomicMarkableReference :原子更新带有标记位的引用类型
- 原子数组:
- AtomicIntegerArray:整型数组原子类
- AtomicLongArray:长整型数组原子类
- AtomicReferenceArray:引用类型数组原子类
- 原子更新器:一个基于反射,支持对指定类的指定字段进行原子更新的类。使用原子更新器更新的字段必须是可访问的、必须被
volatile
修饰、不能被static
修饰。- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
- 原子累加器:
- DoubleAdder:
- LongAdder:
- DoubleAccumulator:
- LongAccumulator:
对象锁
Java中每个对象都有一个对象锁,每个对象的对象锁最多只能由同一个线程获得一次或多次。通过synchronized
关键字即可以使用对象锁。synchronized
的使用有以下三种形式:
synchronized
修饰实例方法:进入同步代码块的线程会获得当前对象的对象锁。
synchronized public void method(){
//synchronizedCode
}
synchronized
修饰静态方法:进入同步代码块的线程会获得方法所在类的Class
对象的对象锁。
synchronized static public void method(){
//synchronizedCode
}
synchronized
修饰代码块:进入同步代码块的线程会获得synchronized
关键字指定对象的对象锁。
public void method(){
//...
synchronized (/*AnyObject*/){
//synchronizedCode
}
//...
}
对象锁的实现
在JDK6之前,对象锁通过操作系统层面的管程实现,在JDK6之后,Java从JVM层面对对象锁进行了优化,通过锁记录实现。
管程实现方式
以管程实现的对象锁成为重量级锁,管程的逻辑结构如下图所示,当一个线程获得对象锁时,就会成为管程的Owner,其它尝试获取锁的线程会进入管程的EntryList。当Owner为空时会唤醒EntryList中的线程,被唤醒的线程通过调度重新获取对象锁。
锁记录实现方式
对象的对象头(以HotSpot为例)中有一片区域叫做Mark Word
,用于存储对象运行时的一些状态,它的结构如下:
JDK6以后对象锁分为以下几个状态(级别由低到高):
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
在获取对象锁的时候会伴随着一个锁升级的过程,这个过程是单向的,对象锁的每一个状态都会在Mark Word
相应的标志位内体现。在优化后获取对象锁的流程如下:
-
当只有一个线程获取对象锁时,JVM会把
Mark Word
中的lock
标志位设置为01
,把biased_lock
标志位设置为1
,表示进入偏向模式,同时使用CAS操作把获取到这个锁的线程ID记录到Mark Word
中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的代码块时,JVM都不用再进行任何同步操作。如果操作失败,则需要进行锁升级。 -
之后一旦发现其它线程尝试获取该对象锁,偏向模式会立即失败,失败后标志位恢复到可偏向(可重新获取偏向锁)或未锁定状态。如果是未锁定状态,JVM首先在当前线程内创建一个锁记录对象,用于存储对象
Mark Word
的拷贝:
- 然后JVM使用CAS操作将对象的
Mark Word
替换为指向锁记录的指针。如果CAS操作成功了,该线程就获得了该对象的轻量级锁。 - 如果CAS操作失败了,那就意味着至少存在一条线程与当前线程竞争该对象的对象锁,JVM首先会检查对象的
Mark Word
是否指向当前线程的锁记录,如果是,说明当前线程已经拥有了这个对象的锁;否则就说明这个对象的对象锁已经被其它线程抢占了,那么当前线程就会尝试自旋来获取锁,当自旋次数达到临界值时,轻量级锁就不再有效,必须进行锁膨胀将轻量级锁变为重量级锁。 - 当释放轻量级锁时,如果对象的对象头仍指向当前线程的锁记录,那就使用CAS操作将对象的指向执行锁记录的指针用锁记录中的
Mark Word
替换。 - 如果替换失败了,说明对象锁的状态重量级锁,此时就进入重量级锁的释放过程。
线程通信
通过以下方法进行线程通信的前提是获取对象的对象锁,此时的对象锁一定处于重量级锁状态,当一个线程成为管程的Owner时,发现某些运行条件不满足,此时可以使用wait
方法使线程进入WaitSet,进入WaitSet的线程会在Owner线程调用notify
方法时被唤醒,唤醒后的线程进入EntryList进行锁抢夺。
void wait() //加入锁对象的等待序列
void wait(long timeout) //计时等待
void notify() //随机挑选一个等待线程激活
void notifyAll() //激活所有的等待线程
值得注意的是调用wait
方法的线程在哪里等待就会在哪里被唤醒,所以下面代码中的if
语句应换成while
,以免线程被唤醒时跳过if
语句而产生虚假唤醒现象。
synchronized (obj){
if (condition){
obj.wait()
}
}
AQS
AQS是一个用于构建锁或其它同步组件的重量级框架。
在具体的实现上,它维护了一个代表临界资源状态的变量(state
)和一个用于存放被阻塞线程的等待队列(CLH锁队列的变体)。
private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;
在临界资源的访问上,它提供了以下两种方式:
- 独占式:同一时间只允许一个线程访问临界资源。
- 共享式:允许多个线程访问临界资源。
状态变量
状态变量在不同的临界资源访问方式上有不同的含义:
- 独占式:表示临界资源的锁定状态,0表示未锁定,1表示已锁定。
- 共享式:表示可以访问临界资源线程的个数。
AQS提供以下方法操作状态变量:
protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)
等待队列
等待队列由一个带有哨兵结点的双向链表实现,链表结点是对等待线程的封装,它包含线程本身以及结点的状态,它的实现如下:
static final class Node {
//标记,表示节点正在共享模式中等待
static final Node SHARED = new Node();
//标记,表示节点正在独占模式下等待
static final Node EXCLUSIVE = null;
//等待状态
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
//等待线程
volatile Thread thread;
//前结点
volatile Node prev;
//后结点
volatile Node next;
//一个正常排队的后继节点
Node nextWaiter;
...
}
waitStatus
变量表示结点的等待状态:
0
:新结点入队时的状态。CANCELLED(1)
:表示当前结点已取消排队(超时或被中断),会触发变更为此状态,进入该状态后的结点的状态将不会再变化。SIGNAL(-1)
:表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。CONDITION(-2)
:表示结点等待在Condition上,当其它线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。PROPAGATE(-3)
:共享模式下,前驱结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
等待队列的核心功能就是调度处于不同状态的结点,可以归结为以下几点:
- 将等待获取临界资源的线程加入队列进行排队。
- 清除在队列内但不参与排队的节点。
- 让在队列内并且参与排队的节点尝试获取临界资源或阻塞。
- 在满足条件下唤醒在队列内并且参与排队并且处于阻塞状态的节点。
- 保存处在排队节点的中断标志,排队结束后再响应中断。
AQS核心方法分析:
//将当前线程根据指定模式加入等待队列
private Node addWaiter(Node mode) {
//构造结点
Node node = new Node(Thread.currentThread(), mode);
//将node加入队尾
...
return node;
}
//排队过程中尝试获取临界资源,成功获取临界资源后返回排队过程中是否被请求中断
final boolean acquireQueued(final Node node, int arg) {
//是否成功获取资源
boolean failed = true;
try {
//自旋过程中是否被中断过
boolean interrupted = false;
for (;;) {
//前驱结点
final Node p = node.predecessor();
//如果前驱节点是头节点(说明自己有资格获取锁)并且成功获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);//将当前节点设置为头节点
p.next = null;
failed = false;//成功获取锁
return interrupted;//自旋过程中是否被中断
}
//如果可以休息那么就阻塞休息
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果被请求中断那么将interrupted设置为true
interrupted = true;
}
} finally {
//自旋失败(超时或被中断)
if (failed)
//取消获取锁
cancelAcquire(node);
}
}
//获取临界资源失败后是否可以阻塞(休息)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//前驱节点的等待标志
//如果前驱节点的状态为SIGNAL,那就可以阻塞了
if (ws == Node.SIGNAL)
return true;
//如果前驱节点已经放弃获取临界资源了(超时、中断或被取消)
if (ws > 0) {
//找到一个正常排队的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;//排在这个正常前驱节点的后边
//如果前驱节点的状态不为SIGNAL并且没有放弃排队
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将前驱节点的等待状态设置为SIGNAL
}
return false;//继续排队并获取临界资源
}
//继续排队但停止获取临界资源并返回当前线程是否被请求中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//取消排队
private void cancelAcquire(Node node) {
//避免空结点
if (node == null)
return;
//结点封装的线程设置为null
node.thread = null;
//寻找正常排队的前驱结点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//记录前驱节点的后继节点
Node predNext = pred.next;
//将当前节点的等待状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前结点是尾结点
if (node == tail && compareAndSetTail(node, pred)) {
//直接将前驱结点之后的结点清除
compareAndSetNext(pred, predNext, null);
} else {
//保存前驱结点的等待状态
int ws;
//如果前驱结点不是头节点
if (pred != head &&
//并且前驱节点的等待状态为SIGNAL
((ws = pred.waitStatus) == Node.SIGNAL ||
//或者前驱节点正常排队并将前驱节点的等待状态成功设置为SIGNAL
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
//并且前驱节点的线程不为空
pred.thread != null) {
Node next = node.next;
//如果当前节点的后继节点不为null并且正在排队
if (next != null && next.waitStatus <= 0)
//将前驱节点的后继节点设置为当前节点的后继节点
compareAndSetNext(pred, predNext, next);
} else {
//如果前驱节点是头节点或者前驱节点的状态不是SIGNAL也无法设置为SIGNAL并且前驱节点的线程为null,就唤醒当前结点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
//唤醒当前结点的后继节点
private void unparkSuccessor(Node node) {
//记录参数节点的状态
int ws = node.waitStatus;
//如果参数节点正常排队
if (ws < 0)
//将参数节点的状态设置为0
compareAndSetWaitStatus(node, ws, 0);
//记录参数节点的后继节点
Node s = node.next;
//如果后继节点是null或者已取消排队
if (s == null || s.waitStatus > 0) {
s = null;
//遍历队列,寻找一个不为null并且正常排队的结点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒该结点
LockSupport.unpark(s.thread);
}
AQS的实现流程
AQS维护了状态变量和等待队列的操作方法,在使用AQS框架实现同步器时只需要实现以下方法即可:
protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
以独占锁为例,一个具体的实现如下:
public class IdentifyLock {
private static class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0,arg)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}else {
return false;
}
}
@Override
protected boolean tryRelease(int arg) {
if (getState()==0||compareAndSetState(1,arg)){
setExclusiveOwnerThread(null);
return true;
}else{
return false;
}
}
}
}
AQS的使用流程
通过acquire
方法就可以通过AQS的某一具体实现安全的获取临界资源,该方法的源码分析如下:
public final void acquire(int arg) {
//通过tryAcquire尝试获取临界资源,如果成功直接返回
if (!tryAcquire(arg) &&
//获取失败则通过addWaiter方法将当前线程加入等待队列,并将等待队列设置为独占模式
//加入队列后通过acquireQueued排队获取临界资源,在排队过程中被请求中断时返回true
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//向线程发送排队过程中积攒的终端请求
selfInterrupt();
}
acquire
方法的执行流程图如下:
通过release
方法就可以通过AQS的某一具体实现安全的释放临界资源,该方法的源码分析如下:
public final boolean release(int arg) {
//使用tryRelease尝试释放临界资源,如果失败直接返回
if (tryRelease(arg)) {
Node h = head;
//如果头节点不为空(那就为自己)并且还有后继节点
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
//成功释放资源
return true;
}
return false;
}
继续完善上文的中的IdentifyLock
:
public class IdentifyLock {
...
private final static Sync SYNC=new Sync();
public void lock(){
SYNC.acquire(1);
}
public boolean unlock(){
return SYNC.release(0);
}
}
以上分析都是在独占模式下,在共享模式下过程基本相同,区别在于在唤醒后继节点时,如果还有剩余获取数会继续唤醒后继节点。
线程通信
void await()
boolean await(long time, TimeUnit unit)//使当前线程等待,直到收到信号或被中断,或指定的等待时间结束
long awaitNanos(long nanosTimeout)//返回还要等待的时间
void awaitUninterruptibly()
boolean awaitUntil(Date deadline)
void signal()
void signalAll()
JDK中AQS的实现
-
独占式实现:
Lock
定义了一种多条件的、可中断的、可定时的、可公平的以及可重入的锁,ReentrantLock
是它的一个具体的实现。ReentrantLock
不仅具有与synchronized
相同的功能,还在此基础上提供了更高的灵活性。它与synchronized
不同之处在于:ReentrantLock
默认是非公平锁,但可以设置为公平锁;synchronized
只能是非公公平锁。ReentrantLock
可以与多个Condition
绑定,从而提供多种等待和通知条件。ReentrantLock
可以在阻塞时响应中断,synchronized
不可以。- 如果在释放锁之前出现异常,那么
ReentrantLock
将不会被释放,而synchronized
在出现异常时可以自动释放对象锁。因此在使用ReentrantLock
时通常把释放锁的语句放在finally
代码中。
-
共享式实现:
- Semaphore:一次可以允许多个线程获取临界资源。
- CountDownLatch:一次可以等待多个线程访问临界资源完成。值得注意的是一个倒计时器实例只能使用一次。
- CyclicBarrier:一次可以等待多个线程到达临界资源,并且循环栅栏可以重复使用。
-
混合式实现:
- ReadWriteLock:在一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行的情况下,可以使用读写锁。读锁是一个共享锁,不支持条件变量。写锁是一个独占锁,两者都能造成死锁。它的可重入性还允许从写锁降级为读锁,先获取写锁,然后是读锁,然后释放写锁。但是,从读锁升级到写锁是不可能的。
- StampedLock:StampedLock是读写锁的升级版,它在读写锁的基础上使得读锁与写锁之间不会相互阻塞,而是使用了乐观读锁的方式。它不支持条件变量和可重入。
线程安全集合
JUC在集合框架的基础上加入了一些含有CopyOnWrite
、Concurrent
以及Blocking
字段的线程安全集合,下文将选择具有代表性的线程安全集合进行源码分析。
CopyOnWriteArrayList
CopyOnWriteArrayList
所有读方法都不加锁,只有写方法加锁,并且在写方法内通过写时复制技术来保证读写(写读)共享,也就是在写操作时先复制一个内部存储结构的副本,在副本内写入,之后再将原引用指向副本
CopyOnWriteArrayList
的优点在于实现了读读共享、写写互斥、读写(写读)共享的同步策略保证线程安全,效率高;缺点在于它只能保证最终结果的完整性,即在过程中读操作读到的数据可能不是最新加入的数据。
ConcurrentHashMap
ConcurrentXxx并不是在每个方法上都在同一个锁同步,而是使用分段锁机制来实现更大程度上的共享,在这种机制下,允许读操作和一定数量的写操作并发访问。ConcurrentXxx会存在弱一致性问题,比如在使用迭代器迭代时,虽然可以修改但是迭代的结果可能是旧的,这是一种fail-safe
机制。
ConcurrentSkipListMap
BlockingQueue
阻塞队列是一个可以在线程之间共享的队列。当队列为空时,从队列获取元素的操作将被阻塞,直到其它线程添加元素。当队列为满时,向队列添加元素的操作将被阻塞,直到其它线程移除元素。使用阻塞队列的好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这一切都由阻塞队列完成。
在阻塞队列中提供了以下四类API来适应不同的场景:
- 抛出异常类:
//成功添加时返回true,队列已满抛出异常
//添加元素为null时抛出NullPointerException
boolean add(E e)
//成功删除返回true,队列为空时抛出异常
boolean remove()
//检索但不删除队列头元素,队列为空时抛出异常
E element()
- 返回特殊值类:
//成功添加时返回true,队列已满返回false
//添加元素为null时抛出NullPointerException
boolean offer(E e)
//成功删除返回true,队列为空返回null
E poll()
//检索但不删除队列头元素,队列为空返回null
E peek()
- 阻塞类:
//成功添加时直接返回,队列已满时阻塞
//添加元素为null时抛出NullPointerException
//阻塞时被中断抛出InterruptedException
void put(E e)
//队列为空时阻塞
//阻塞时被中断抛出InterruptedException
E take()
- 超时类:
//成功添加返回true,队列已满时等待指定时间,等待超时返回false
//添加元素为null时抛出NullPointerException
//阻塞时被中断抛出InterruptedException
boolean offer(E e, long timeout, TimeUnit unit)
//队列为空等待指定时间,等待超时返回null
//阻塞时被中断抛出InterruptedException
E poll(long timeout, TimeUnit unit)
JDK提供的实现类如下:
ArrayBlockingQueue
:存储结构上基于定长数组实现。线程安全上基于ReentrantLock
实现。LinkedBlockingQueue
:存储结构上基于链表实现,默认大小为Integer.MAX_VALUE
。线程安全上基于ReentrantLock
实现。PriorityBlockingQueue
:存储结构上由基于数组的平衡二叉堆实现。线程安全上基于ReentrantLock
实现。SynchronousQueue
:同步队列中的同步指的是当一个线程向其添加一个元素时会阻塞至线程将其取出,反之亦然,因此它没有提供任何空间存储元素。线程安全上基于LockSupport
实现。DelayQueue
:底层由由PriorityBlockingQueue
实现,其中的元素只有在延时时间到达后才能取出。TransferQueue
:TransferQueue
似于BlockingQueue
和SynchronousQueue
的组合,主要体现在transfer
方法,当有读线程阻塞时,调用transfer
方法的写线程就不会将元素存入队列,而是直接将元素传递给读线程;如果调用transfer
方法的写线程发现没有正在等待的读线程,则会将元素加入队列,然后会阻塞等待,直到有一个读线程来获取该元素。该队列是一个接口,JDK提供了一个LinkedTransferQueue
实现。
void transfer(E e)
//传输一个值,或者尝试在给定超时时间内传输这个值,这个调用将阻塞,直到另一个线程将元素消费
boolean tryTransfer(E e, long timeout, TimeUnit unit)
异步编程
Executor
Executor
是一个接口,用于定义自定义线程池、异步I/O和任务框架。ExecutorService
是其子接口,提供了更完整的异步任务执行管理。ScheduledExecutorService
及其相关接口支持延迟和周期任务执行。
ThreadPoolExecutor
和ScheduledThreadPoolExecutor
类提供了可调整、灵活的线程池。Executors
类提供了工厂方法,用于创建最常见种类和配置的Executor
,以及一些用于使用它们的实用方法。ForkJoinPool
类提供了一个主要用于处理ForkJoinTask
及其子类实例的Executor
。这些类采用了一种工作窃取调度器,对于符合计算密集型并行处理中经常出现的限制的任务,可以实现高吞吐量。
Future
线程在创建时必须为其指定任务,Java中有以下两种任务:
Runnable
:没有返回结果并且不会抛出异常。Callable
:有返回结果并且会抛出异常。
FutureTask
将这两种任务包装起来。一方面,它实现了Runnable
接口,可以被线程执行;另一方面,它实现了Future
接口,可以表示异步计算的结果。Future
提供的方法可以用于检查任务是否完成、等待任务完成和检索任务结果。例如,get()
方法可以阻塞获取结果;cancel()
方法可以取消任务。但是,一旦任务开始就不会被其它线程重复执行,一旦任务完成就不能被取消。
任务调度
CompletionStage
CompletionStage是用于处理可能异步计算的阶段的接口。一个CompletionStage代表了一个计算阶段,当另一个CompletionStage完成时,它会执行一个操作或计算一个值。阶段的计算结果在计算终止时完成,并且可能会触发其他依赖的阶段。该接口定义了几种基本形式的功能,这些功能通过一组更大的方法来捕获各种用法:
- 阶段的计算可以表示为Function、Consumer或Runnable,具体取决于它是否需要参数和/或产生结果。这些方法具有不同的名称,比如apply、accept或run。例如:
stage.thenApply(x -> square(x))
.thenAccept(x -> System.out.print(x))
.thenRun(() -> System.out.println());
-
compose方法允许从返回CompletionStage的函数构建计算管道。阶段计算的参数是触发阶段计算的结果。
-
一个阶段的执行可以由单个阶段的完成、两个阶段的任一个或两个阶段的都完成触发。使用then前缀的方法安排对单个阶段的依赖关系。由两个阶段都完成触发的依赖关系可能会合并它们的结果或效果,使用相应命名的方法。由两个阶段中的任意一个触发的阶段不保证使用哪个结果或效果进行依赖阶段的计算。
-
阶段之间的依赖关系控制计算的触发,但不以任何特定的顺序保证执行。新阶段的计算执行可以以三种方式之一安排:默认执行、默认异步执行或自定义执行。默认执行和异步执行的行为由CompletionStage实现指定。具有显式Executor参数的方法可能具有不同的执行属性,但通常以适应异步执行的方式处理。
-
handle和whenComplete方法支持无条件计算,无论触发阶段是否正常完成或异常完成。exceptionally方法仅在触发阶段异常完成时支持计算,计算替换结果。如果一个阶段的计算突然终止并出现异常,则所有依赖于它的阶段也会以异常方式完成。如果一个阶段依赖于两个阶段中的两个,且两者都异常完成,则异常可能对应于这两个异常之一。如果一个阶段依赖于两者中的任何一个,并且只有一个异常完成,则不能保证依赖阶段是正常完成还是异常完成。在whenComplete方法的情况下,如果提供的操作本身遇到异常,则阶段会以异常方式完成,除非源阶段也以异常方式完成,在这种情况下,源阶段的异常完成将优先。
CompletableFuture
CompletableFuture是一个Future,可以显式地完成(设置其值和状态),并且可以用作CompletionStage,支持依赖函数和操作,这些函数和操作在其完成时触发。
当两个或多个线程尝试完成、完成异常或取消CompletableFuture时,只有一个线程成功。
CompletableFuture 实现了 CompletionStage 接口,具有以下策略:
-
非异步方法的依赖完成操作可以由当前 CompletableFuture 的完成线程执行,也可以由任何其他调用完成方法的线程执行。
-
所有没有显式 Executor 参数的异步方法都使用 ForkJoinPool.commonPool() 执行(除非它不支持至少两个并行级别,在这种情况下,将创建一个新的线程来运行每个任务)。所有生成的异步任务都是 CompletableFuture.AsynchronousCompletionTask 接口的实例。为了简化监视、调试和跟踪,异步任务可以使用此类中定义的适配器方法,例如 supplyAsync(supplier, delayedExecutor(timeout, timeUnit))。
-
CompletableFuture 类最多维护一个守护线程用于触发和取消操作,而不是用于运行操作。
-
所有 CompletionStage 方法都是独立于其他公共方法实现的,因此一个方法的行为不受子类中其他方法的重写的影响。
-
所有 CompletionStage 方法都返回 CompletableFuture。要限制用法为仅在 CompletionStage 接口中定义的方法,请使用 minimalCompletionStage() 方法。或者为了确保客户端不修改 Future 本身,请使用 copy() 方法。
CompletableFuture 还实现了 Future 接口,具有以下策略:
-
由于(与 FutureTask 不同)这个类没有直接控制导致其完成的计算,因此取消被视为另一种异常完成形式。cancel 方法的效果与 completeExceptionally(new CancellationException()) 相同。可以使用 isCompletedExceptionally() 方法来确定 CompletableFuture 是否以任何异常方式完成。
-
在出现 CompletionException 的情况下,get() 和 get(long, TimeUnit) 方法会抛出一个带有与相应 CompletionException 中相同原因的 ExecutionException。为了简化大多数情况下的使用,这个类还定义了 join() 和 getNow(T) 方法,在这些情况下直接抛出 CompletionException。
用于传递完成结果(即对于接受这些参数的方法的类型 T 的参数)的参数可以为 null,但是对于任何其他参数传递 null 值将导致抛出 NullPointerException。
这个类的子类通常应该重写“虚拟构造函数”方法 newIncompleteFuture(),该方法确定由 CompletionStage 方法返回的具体类型。例如,这是一个替换不同默认 Executor 并禁用 obtrude 方法的类:
class MyCompletableFuture<T> extends CompletableFuture<T> {
static final Executor myExecutor = ...;
public MyCompletableFuture() { }
public <U> CompletableFuture<U> newIncompleteFuture() {
return new MyCompletableFuture<U>(); }
public Executor defaultExecutor() {
return myExecutor; }
public void obtrudeValue(T value) {
throw new UnsupportedOperationException(); }
public void obtrudeException(Throwable ex) {
throw new UnsupportedOperationException(); }
}
CompletionService
CompletionService 是一个服务,它将新的异步任务的生产与完成任务结果的消费解耦。生产者提交任务以供执行,而消费者则按照任务完成的顺序处理它们的结果。CompletionService 可以用于管理异步 I/O,其中执行读取的任务在程序或系统的某个部分被提交,然后在程序的另一部分在读取完成时对其进行处理,可能不同于请求它们的顺序。
通常,CompletionService 依赖于一个单独的 Executor 来实际执行任务,此时 CompletionService 只管理内部的完成队列。ExecutorCompletionService 类提供了这种方法的实现。
内存一致性效应:在一个线程中,将任务提交给 CompletionService 之前的操作先于该任务所执行的操作,而后者又先于从相应的 take() 方法成功返回后的操作。
同步器
有五个类可以帮助实现常见的特定目的的同步习语。
- Semaphore是一个经典的并发工具。
- CountDownLatch是一个非常简单但非常常见的实用程序,用于阻塞直到给定数量的信号、事件或条件发生。
- CyclicBarrier是一个可重置的多向同步点,在某些并行编程风格中非常有用。
- Phaser提供了一种更灵活的屏障形式,可以用来在多个线程之间控制分阶段计算。
- Exchanger允许两个线程在一个会合点交换对象,在几种流水线设计中非常有用。