Java 多线程相关笔记
线程简介
运行线程等价于 调用 Thread.run()
方法。而线程实际运行是由线程调度器决定,因此线程可能稍后执行或不被执行。
线程创建方法:
- 继承自Thread类,重写run方法。
- 实现Java.lang.Runnable接口,重写run方法。
- 实现Callable接口。
- 创建线程池。
线程的属性:ID(只读),Name,Daemon(线程类别),Priority。
Daemon指定线程为用户线程还是守护线程。Java虚拟机只有在所有用户线程退出之后才会停止,因此守护线程通常执行重要性不高的任务。
层次关系:线程是否为守护线程默认取决于父线程,线程优先级默认继承于父线程的优先级。
生命周期:NEW,RUNNABLE(READY,RUNNING),BLOCKED,WAITING,TIMED_WAITING,TERMINATED。(可用 Thread.getState()
获取)
多线程基础
竞态
竞态:计算的正确性依赖于相对时间顺序或线程的交错。通常由读取脏数据引起。
如多个线程读取共享变量a,读取一次a++,多个线程可能会读取同一个a值造成错误。
竞态的模式:
- read-modify-write:读取共享变量的值,执行相关操作,更新变量的值。然而在执行相关操作中,其他线程进行了共享变量的更新,导致其读取了脏数据,最后造成变量更新错误。
- check-then-act:读取共享变量的值,进行判断,执行相关操作。然而在判断完成后,共享变量可能被其他线程更新,导致该线程错误的执行了相关代码。
解决竞态:使用局部变量、使用锁(如synchronized)等。
原子性
线程安全性:在单线程下正常运行,多线程下不用做任何改变也正常运行的类是线程安全的。
而标准库中ArrayList、HashMap、SimpleDateFormat都不是线程安全的,在多线程下不采取措施使用会造成死循环或内存泄漏。
原子性(Atomicity):若一个操作从其执行线程以外的任意其他线程来看都是不可分割的,即其他线程对变量的观察要么没有更新,要么更新完毕,不会观察到变量更新的过程,称该操作是原子的(Atomic)。
实现原子性的方式:
- 使用锁(Lock),保证一个共享变量在任意一个时刻只能由一个线程读取写入。
- 使用CAS(Compare-and-Swap)指令,锁实现在软件层次,而CAS实现在硬件层次。
Tips:Java中,除了long与double的其他基本变量类型的写操作都是原子操作。而使用volatile关键字的修饰可以仅保证long与double的写操作原子性。Java中对任何变量的读操作都是原子操作。
上述竞态中产生的问题就是由无原子性导致的。
可见性
可见性(Visibility):如果一个线程对某个共享变量更新之后,其他线程可以读取到更新的结果,则称该线程对变量的更新对其他线程可见。
有如下逻辑:
class a implements Runnable {
private boolean toCancel = false;
@Override
public void run() {
while (!toCancel) {
if (doSomething()) {
break;
}
}
}
public boolean doSomething() {
//doSomething
return true;
}
}
在某一时刻,其他线程会将toCancel变量更新为true,然而该程序不会停止运行,说明while循环中没有读取到toCancel变量的更新,即,该更新对此线程不可见。
原因在于JIT编译器默认为单线程运行程序,因此会对代码进行相应优化,在此例中对循环进行循环提升(Loop Hoisting)优化,如下:
class a implements Runnable {
private boolean toCancel = false;
@Override
public void run() {
if (!toCancel) {
while (true) {
if (doSomething()) {
break;
}
}
}
}
public boolean doSomething() {
//doSomething
return true;
}
}
因此导致变量更新不可见。实际上,变量可能被分配到寄存器而不是主存进行存储,导致各个处理器观察到的变量不同,也可能导致更新对其他处理器不可见。此外,Cache对与IO的优化也可能导致可见性问题。一般硬件采用缓存一致性协议保证Cache的可见性。
在上述问题中:添加volatile关键字修饰toCancel变量的声明即可提示JIT编译器该变量可能被多个线程共享,从而防止编译器的优化。并且添加volatile关键字的变量会使相应处理器执行冲刷处理器缓存的动作,保证可见性。
线程之间的可见性:
- 父线程在启动子线程之前对共享变量的更新对于子线程是可见的。
- 一个线程终止后对共享变量的更新对于调用该线程的join方法的线程而言是可见的。
有序性
重排序:在结果相同的情况下,编译器可能会将代码运行顺序进行改变,导致重排序。同时,在一个处理器上执行的操作,从其他处理器的角度来看其顺序可能与目标代码指定顺序不一致,也称重排序。分别对应指令重排序与存储子系统重排序。
重排序是对内存访问相关操作的优化,可以在不影响单线程程序正确性的情况下提升性能,然而会对多线程的正确性造成影响。
指令重排序:如有以下逻辑:
//分配testClass实例target所需要的空间并返回空间的引用
obj = allocate(testClass);
//使用testClass的构造器对obj所指空间进行初始化
construct(obj);
//将实例引用赋值给target
target = obj;
实际上,编译器在执行第一条语句后可能会执行第三条语句,随后执行第二条语句对空间进行初始化,导致了重排序。而在多线程运行下,若有线程时刻判断target是否初始化完成(以 target == null
进行判断),这就会导致该线程误以为target已经创建实例完成执行后续代码,而此时target的相关构造尚未完成,因此导致了错误。
处理器也会导致指令重排序,现代处理器有时会采用顺序读取,乱序执行(按顺序读取指令,等到哪条指令就绪就会立刻执行操作写入到重排序缓冲器以便后续更新)的方式导致重排序。例如,乱序执行中的猜测执行会对如下逻辑如此处理:
if(someJudge()){
ifTrueOperation();
}
处理器可能在接收到判断if和if中操作两段代码时,首先运行if内相关操作记录到重排序缓冲器(ROB)中,若if中判断为true就将ROB中的值写入主存,若为false则丢弃ROB中的值产生重排序。
存储子系统重排序(内存重排序):没有对指令本身造成重排序,而是对内存操作的结果重排序,使得其他处理器对该处理器的感知执行顺序不同。
貌似串行(As-if-serial):编译器和处理器都会遵循貌似串行语义,保证在单线程下重排序后的运行结果不影响程序的正确性。而为了保证貌似穿行语义,对存在数据依赖关系的操纵不进行重排序。例如有如下逻辑:
a = 1;
b = 2;
c = a + b;
其中c变量的赋值操作与a和b都有数据依赖,因此不会对c赋值语句同a、b赋值语句进行重排序。而a、b赋值语句与其他变量都没有数据依赖性,所以可能会对a、b赋值顺序进行重排序。
有序性:程序运行的感知顺序与源代码顺序一致。
实际上,可以使用volatile关键字修饰变量防止重排序或者synchronized关键字保证有序性。
上下文切换
切出:一个线程被剥夺处理器的使用权而被暂停运行。
切入:一个线程被操作系统选中占用处理器开始或者继续运行。
从Java角度来看,当一个线程生命周期状态在RUNNABLE与非RUNNABLE状态切换的过程就是上下文切换的过程。上下文切换分为自发性上下文切换与非自发性上下文切换。
自发性上下文切换:Thread.sleep()
、Thread.join()
、发起IO操作、等待其他线程持有的锁等等都会导致自发性上下文切换。
非自发性上下文切换:线程由于线程调度器而被迫切出(如一个线程被迫暂停,将处理器给优先级更高的线程)。
上下文切换会导致开销,因此减少多线程项目的上下文切换会提高程序的性能。相反,一个多线程程序由于上下文开销可能会导致线程越多,性能越低。
线程同步
锁
锁分为内部锁和显式锁,内部锁由关键字synchronized实现,显式锁通过java.concurrent.locks.Lock接口的实现类实现。
锁通过互斥性,即一个锁一次只能被一个线程持有,来保障原子性、可见性和有序性。
可重入锁:若一个线程在持有一个锁的情况下还能多次申请该锁,则称该锁是可重入的(Reentrant),内部锁和显式锁都是可重入的。代码逻辑如下:
void methodA(){
acquireLock(lock);
//do something...
methodB();
releaseLock(lock);
}
void methodB(){
acquireLock(lock);
//do something...
releaseLock(lock);
}
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁的锁。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁的锁。
公平锁更适用于锁被持有的时间相对长或线程申请锁的平均间隔市场相对长的情形。synchronized只能实现非公平锁,而显式锁两者均可实现,默认实现非公平锁。
内部锁
synchronized关键字:该关键字可以通过修饰方法以及代码块保证原子性、可见性和有序性。逻辑如下:
public class a {
private int index = 1;
public synchronized int nextIndex() {
if (index >= 999) {
index = 1;
} else {
index++;
}
return index;
}
public int nextIndexEqualVersion() {
synchronized (this) {
if (index >= 999) {
index = 1;
} else {
index++;
}
return index;
}
}
}
若nextIndex是static方法,则只需将 synchronized(this)
修改为 synchronized(a.class)
即可。
内部锁不会导致锁泄露,Java编译器会将异常进行捕获进行特殊处理。
注意,重排序和有序性是不同的两个概念。synchronized保证的有序性是指锁内的代码块在不同线程眼里是有序的,而不能防止锁内代码块的指令重排序。
显式锁
返回 | 方法 | 作用 |
---|---|---|
void | lock() | 获取锁 |
void | lockInterruptibly() | 如果线程未中断,则获取锁 |
Condition | newCondition() | 返回绑定到此lock实例的新Condition实例 |
boolean | tryLock() | 调用时若锁是空闲的则获取该锁,否则不获取 |
boolean | tryLock(long time, TimeUnit unit) | 若锁在给定的时间内空闲且线程未被中断则获取该锁,否则不获取 |
void | unlock() | 释放锁 |
逻辑如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class a {
private final Lock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
//do something
} finally {
lock.unlock();
}
}
}
需要注意的是,必须将 lock.unlock()
方法放入finally代码块中,防止发生锁泄漏。此外 new ReentrantLock(true)
会返回公平锁实例。
显式锁的优点:内部锁的持有线程如果发生错误(如进入死循环)就会一直持有该锁,导致其他申请锁的线程无法继续进行,始终停留在申请锁状态。而显式锁可以使用 lock.tryLock()
方法进行尝试申请,若申请失败则可以立刻退出申请继续执行相关代码。
此外,显式锁还提供了接口以查看锁的相关状态,例如 isLocked()
方法可以查看锁是否被某个线程持有。getQueueLength()
方法可以检查相应锁的等待线程的数量。
读写锁
锁的排他性使得要求大量读的并发操作有着极大的局限性,读写锁可以解决此问题。
读写锁的读锁是共享的,而写锁是排他的,因此其能够既能保证原子性、可见性、有序性的同时,还能解决大量读操作并发的需求。逻辑如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class a {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock readLock = rwlock.readLock();
private final Lock writeLock = rwlock.writeLock();
public void reader() {
readLock.lock();
try {
//read something
} finally {
readLock.unlock();
}
}
public void writer() {
writeLock.lock();
try{
//write something
} finally {
writeLock.unlock();
}
}
}
在一个线程持有读写锁的写锁情况下,该线程可以继续申请读锁。而在一个线程持有读锁的情况下,必须将读锁释放才能申请写锁。
volatile
volatile关键字意味着对变量的读和写操作都必须从高速缓存或主存中读取,以读取变量的相对新值。其次,volatile关键字的使用不会引起上下文切换,并可以保证可见性、有序性(禁止相关指令重排序)和long、double型变量读写操作的原子性(无法保证其他操作的原子性)。
单例模式
单例模式:保持一个类有且仅有一个实例。在单线程下代码逻辑如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
//constructor
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
而在多线程下,if语句造成check-then-act操作可能使得多个线程同时进行if语句块内操作造成多个实例的创建。可以使用简单的对 getInstance()
方法加锁来解决。但是这意味着每个执行 getInstance()
方法的线程都需要等待锁的释放,故可采取双重检查锁定(Double-checked Locking)来解决该问题。代码逻辑如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
//constructor
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这样就防止了instance已经实例化后重复的加锁释放锁操作。
然而,该程序是错误的,虽然保证了实例化后的问题,但却没有保证实例化中发生的问题。当instance被实例化时,根据上述重排序知识可知,instance可能先获得一个空间的引用,再会对该空间进行初始化。而若如此,若有其他线程执行第一个if判断时,此时instance不为null但同时也没有被初始化完成,这时此线程就会直接返回instance,导致返回了一个未初始化完毕的实例,造成程序错误。因此,需要给instance再加上volatile关键字,保证其不会被重排序来解决问题。正确代码如下:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
//constructor
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
实际上,还可以利用Java中static的机制来实现单例模式,代码如下:
public class Singleton {
private Singleton() {
//constructor
}
private static class InstanceHolder {
final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.instance;
}
}
还可以利用枚举类实现,相关代码省略。
CAS
CAS(Compare and Swap)是对一种处理器指令的称呼,它可以看作如下所示的函数:
boolean compareAndSwap (Variable V, Object A, Object B) {
if(A == V.get()) {
V.set(B);
return true;
}
return false;
}
do {
oldValue = V.get();
newValue = someOperation(oldValue);
}while(V, oldValue, newValue);
CAS可以保证更新操作的原子性,而不能保证可见性。
ABA问题:若对变量V的观察值为A的一刻,其他线程将其更新为B,随后在执行CAS时,又被其他线程更新为A,则是否认为变量V被更新过?是否接受要基于CAS所要实现的算法。而规避此问题可以采用加时间戳的方式,将每一次更新后记录值加一与更新值绑定即可。
原子类
原子变量类(Atomics)是基于CAS实现的能够保障对共享变量进行read-modify-write更新操作的原子性和可见性的工具类。例如有AtomicInteger,AtomicLongArray,AtomicReference等等类,其中AtomicInteger有 get()
、getAndIncrement()
(自增并获取自增前的值)、decrementAndGet()
(自减并获取自减后的值)、set(long newValue)
等方法。
线程协作
wait/notify
当程序需要一定条件才能继续执行时,可以使用wait方法进行等待,当收到notify方法通知后继续进行。注意,要保证操作的原子性。
synchronized(someObject){
while(!aimedCondition()){
someObject.wait();
}
//do something
}
当不满足条件时,执行 someObject.wait()
,此时该线程释放其持有的内部锁。而当有其他线程通知时,该线程重新请求锁且返回,若此时条件成立则跳出循环执行后续代码。
synchronized(someObject){
//do something
someObject.notify();
//someObject.notifyAll();
}
注意,由于等待线程在得到通知时会重新请求锁,因此要将通知的代码写到最后防止无谓的请求而导致上下文切换。而 notifyAll()
方法会通知所有等待线程,notify()
方法会通知等待线程中随机一个。
此外,wait(long time)
方法可以设置超时时间,当时间过后Java虚拟机会自动唤醒该线程。该方法无返回值,所以是超时返回还是被其他线程通知返回需要用时间戳自行判断。
过早唤醒:如果多个线程的条件不同,那么当使用 notifyAll()
方法时会唤醒所有线程,会有仍不满足的线程被唤醒。
Condition
为了解决过早唤醒的问题,我们可以使用显式锁并显式地指出唤醒的线程。
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await(){
lock.lock();
try{
while(!aimedCondition()){
condition.await();
}
//do something
}finally{
lock.unlock();
}
}
通知类似,只需将 condition.await()
改为 condition.signal()
。
而同时 condition.await(Date deadline)
方法可以实现超时等待,且超时返回为false,被通知返回为true。
CountDownLatch(倒计时协调器)
CountDownLatch可以实现线程等待多个条件满足后再执行操作。
CountDownLatch.countDown()
每执行一次,其计数器就会减一,而当计数器为零时,执行 CountDownLatch.await()
的线程就会被通知,执行后续代码。同时,该类内部已经实现等待与通知的逻辑,因此运用该类时无需因等待和通知加锁。计数器在初始创建时指定(public CountDownLatch(int count)
)。
需要注意的是,为了防止 countDown()
方法调用不正确导致 await()
无法返回,可以选择使用超时等待,并且将 countDown()
方法均放在finally代码块内。
该类是一次性的。一个实例的计数器为零时后续对其操作均无效。
CyclicBarrier
栅栏执行 await()
方法后,其中所有线程都会调用 start()
方法,只有当所有线程调用完成返回后,栅栏才会返回 await()
方法执行后续代码。
final CyclicBarrier sampleBarrier;
sampleBarrier = new CyclicBarrier(N, new Runnable(){
@Override
public void run(){
//do something
}
});
sampleBarrier.await();
因此,栅栏可以用于在代码中模拟高并发测试。
生产者-消费者模式
生产者将生产出的结果打包放进管道内,而消费者可以从管道取出结果进行消费。
public class Sample {
public static void main(String[] args) throws InterruptedException {
final BlockingQueue<int[]> channel = new ArrayBlockingQueue<int[]>(2);
int[] e = new int[] { 0, 1, 2 };
channel.put(e);
int[] batch = channel.take();
for (int i : batch) {
System.out.println(i);
}
}
}
阻塞队列
上文所使用的管道实际是一个阻塞队列,可以选择创建有界队列或无界队列。当使用有界队列且队列已满时,生产者再进行 put()
方法会被阻塞(利用了锁),直到队列中有元素被消费者线程取出。其中BlockingQueue的常用实现类有ArrayBlockingQueue、LinkedBlockingQueue和SynchronousQueue等。
SynchronousQueue是一个特殊的有界队列,当其调用 take()
方法时,若没有对应 put()
方法被调用会一直等待,知道元素被取出,对应的生产者线程才会重新进行。反之同理。
阻塞队列也可以使用非阻塞式操作。
信号量
我们可以利用信号量(Semaphore)来限制同一时间对资源的访问程度。
public class Sample {
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(5);
final BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10);
semaphore.acquire();
try{
blockingQueue.put(1000);
} finally {
semaphore.release();
}
}
}
每次执行 acquire()
方法配额减一,每次执行 release()
方法配额加一。配额不足时,线程会暂停。
双缓冲
有缓冲区bufferA和bufferB,生产者向bufferA中写入,消费者从bufferB中拿取。而当A被写满同时B中为空时便可以将A,B交换,让生产者向bufferB写入,消费者从bufferA中拿取,如此往复。
双缓冲可以实现数据生成和消费的并发。
多线程设计
无状态对象/不可变对象
- 无状态对象:该类的任一个实例都不含有各个线程的共享变量。
- 不可变对象:该类的任一个实例都不含有可写的变量,或可写的变量没有暴露接口使其改变。
上述两种对象都具有线程安全性。
ThreadLocal
ThreadLocal关联了线程特有对象,每个线程对该类的同一个实例的返回都是不同的,所以其也具有线程安全性,实现了线程间的数据隔离。
线程故障
死锁
死锁的四个必要条件:
- 资源互斥:每个资源只能被一个线程占有。
- 资源不可抢夺:每个资源只能被其占有线程释放。
- 占用并等待资源:涉及的线程产生死锁时至少占有了一个资源并且申请了其他资源。
- 循环等待:涉及的线程产生死锁时必然等待其他线程持有的资源,这种等待造成了循环。
死锁可以通过线程转储或者jconsole图形化工具来分析。
锁死
锁死分为信号丢失锁死和嵌套监视器锁死。
- 信号丢失锁死:如线程执行
Object.wait()
方法时,没有对应线程执行Object.notify()
,导致死锁。 - 嵌套监视器锁死:当线程涉及锁的嵌套时,使用
Object.wait()
方法可能因为通知线程无法申请外部锁导致死锁。逻辑如下:
synchronized(ObjectX){
synchronized(ObjectY){
while(!something){
wait();
}
//do something
}
}
synchronized(ObjectX){
synchronized(ObjectY){
something = true;
notify();
}
}
等待线程执行 wait()
方法后释放了ObjectY锁,但是没有释放ObjectX锁,这就导致通知线程不能够申请到ObjectX锁,因此就不能执行通知代码,导致死锁。
线程饥饿
线程饥饿是指线程一直无法获得其所需的资源导致任务一直无法进展。例如,在高并发高争用的环境下使用非公平锁,这就会导致优先度高的线程始终会占有锁,优先度低的线程始终无法抢夺到锁的申请。
活锁
线程始终在申请其资源而申请不成功,与线程饥饿不同的是,活锁是可以申请到锁的,但是申请的相关资源由于某些条件判断不成功导致始终申请失败。
线程池
线程池可以看作是一个生产者-消费者模式,主线程或其他线程可以给线程池添加任务,其任务可能直接被线程池中已有的线程拿取或者存放到队列内,线程池中的其他线程不断从队列中取出任务进行执行。
线程池的优点:
- 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线程的数目,放置消耗过多的内存。
线程池的创建
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(
corePoolSize, maximumPoolSize,
keepAliveTime, unit, workQueue,
threadFactory, handler)
- corePoolSize(int):线程池核心线程容量。
- maximumPoolSize(int):线程池最大线程容量。
- keepAliveTime(long):当线程池中的线程数量大于 corePoolSize 时,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁。
- unit(TimeUnit):补充keepAliveTime的时间单位。
- workQueue(BlockingQueue
<Runnable>
):任务队列,用于存放提交但是尚未被执行的任务。 - threadFactory(ThreadFactory):线程工厂,用于创建线程,一般采取默认即可。
- handler(RejectedExecutionHandler):拒绝处理的规则。
规则
线程处理规则:核心线程会随着任务的提交而不断生成,当核心线程数量等于由构造函数指定的最大核心线程容量时,任务会被存放在队列中。而当队列满时,线程池又会创建新的工作者线程直到线程池中总线程大小等于最大线程容量。其中,线程池将任务存放到队列中时使用的是BlockingQueue的非阻塞方法 offer(E e)
,所以不会导致提交任务线程的阻塞。此外,当线程池队列已满而仍有任务被提交时,线程池会拒绝该任务,也可使用handler来指定拒绝时采取的规则。
ThreadPollExecutor已实现的类如下:
- ThreadPoolExecutor.AbortPolicy:直接抛出异常。
- ThreadPoolExecutor.DiscardPolicy:直接丢弃该任务。
- ThreadPoolExecutor.DiscardOldestPolicy:将工作队列中最老的任务丢弃,重新尝试接纳该任务。
- ThreadPoolExecutor.CallerRunsPolicy:在客户端线程中执行被拒绝的任务。
线程池可以使用 ThreadPoolExecutor.prestartAllCoreThreads()
方法使得在线程池还未接受到任务时就创建所有核心线程,减少了创建时的等待时间。
线程池可以使用 ThreadPoolExecutor.shutdown()
方法关闭线程池,此时已提交的任务会继续执行,而未提交的任务会被拒绝。还可以使用 ThreadPoolExecutor.shutdownNow()
方法,此时已提交的任务停止执行,未提交任务被拒绝,并返回一个已提交未执行的任务列表。
可以使用 ThreadPoolExecutor.submit()
方法向线程池中提交任务,该方法接受一个Runnable或Callable实例,并返回Future<?>实例,可以使用 get()
方法获得具体线程实现信息(?处为Callable所返回的类型,若接受的是Runnable实例,则返回null)。
java.util.concurrent.Callable接口定义了唯一方法 call()
,其可以在重写 call()
方法中返回值,便于后续的分析。
监控
getPoolSize()
:获取当前线程池大小。getQueue()
:返回工作队列的实例,可用于查看队列的大小。getLargestPoolSize()
:获取线程池线程达到的最大数,可用于判定线程核心数设置是否合理。getActiveCount()
:获取线程池正在执行任务的线程数量(近似)。getTaskCount()
:获取线程池从创建到调用方法前的总任务运行数量(近似)。getCompletedTaskCount()
:获取线程池从创建到调用方法前的总任务完成数量(近似)。
异步编程
Executors
Executors中提供的创建线程池的方法:
Executors.newCachedThreadPool()
:创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。适用于执行大量耗时较短,提交频率较高的任务。Executors.newFixedThreadPool(int nThreads)
:创建一个固定线程数量的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,新的任务会暂存在任务队列中,待有线程空闲时便处理任务。队列是无限队列,且线程不会因时间而被自动清理。Executors.newSingleThreadExecutor()
:基本相当于Executors.newFixedThreadPool(1)
,可用于实现单(多)生产者-单消费者模式。注意由于只有一个线程,因此任务是串行执行的。newScheduledThreadPool(int corePoolSize)
:创建了一个固定长度的线程池,以定时的方式来执行任务,适用于定期执行任务的场景。
FutureTask
Runnable实例虽然可以交给线程池或者一个专门的工作者线程执行,但是其没有返回值难以分析。Callable实例虽然有返回值,但是其只能提交给线程池,有着局限性。因此,我们可以使用FutureTask类,其是一个Runnable接口的实现类,并且还能够直接返回其代表异步任务的结果,融合了Runnable和Callable两者的优点。
通常我们可以先设计一个Callable实例,随后以该实例为参数通过FutureTask的构造器 public FutureTask(Callable<V> callable)
实现一个FutureTask实例(注意,其实现了Runnable接口)。我们可以使用 FutureTask.get()
方法来获得该任务的执行结果。而当该任务执行完毕后,会自动执行 FutureTask.done()
方法,因此我们可以在FutureTask子类中覆盖该方法,使用 FutureTask.get()
来获取结果。注意,通常我们先会执行 FutureTask.isCancelled()
方法来判断任务是否被取消,否则在执行 get()
方法是可能会出现异常。
此外 FutureTask.runAndReset()
方法可以实现任务的重复执行,但是不会记录任务的处理结果。