前言
前几章都在讲一些锁的使用和原理,主要是为了保证多线程情况下变量的原子性,但这并不是说多线程不好
,合理利用还是有好处的。至于什么好处,看下面内容就懂了,先打个比方吧(谁叫比方,上来挨打
):假如你体育考试,要跑1000米,你现在有两个选择:
- 一个人跑完1000米。
- 找三个人陪你一起跑,每个人跑250米就好
两种方案你选哪个?
今天写一下面试必问的内容:多线程与线程池。主要从以下几方面来说:
- 什么是线程(什么是多线程)
- 线程状态
- 多线程的优点和弊端
- 线程池的好处
- 线程池的新建
- 线程池状态
- 线程池执行任务
- 线程池异常处理
- 为什么submit()方法提交任务产生异常会被"吞掉"
6月6日,好吉利的数字,祝大家六六大顺,话不多说,开始搞事!
1、什么是线程(什么是多线程)
线程是操作系统调度的最小单元,每个线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。而多个线程组成了一个进程。一个程序,比如爱奇艺客户端、腾讯客户端、wegam等,至少有一个进程,而一个进程至少有一个线程。
那么问题来了,通过一个main方法启动一个Java程序
,这算是进程还是线程呢?代码如下:
public class ThreadTest {
public static void main(String[] args) {
}
}
答案是:进程
,改进一下上述代码,执行如下:
public class ThreadTest {
public static void main(String[] args) {
// 获取Java线程管理的MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// dumpAllThreads(boolean lockedMonitors, boolean lockedSynchronizers)
// 不需要获取同步的synchronizer信息,仅获取线程和线程堆栈晋西
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, false);
for (ThreadInfo threadInfo : threadInfos){
System.out.println(
"[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName()
);
}
}
}
- main:main线程,程序入口
- Reference Handler:清除Reference的线程(对象的引用存在虚拟机栈中,GC的时候需要用到)
- Finalizer:调用对象finalizer方法的线程(GC的时候需要用到)
- Signal Dispatcher:分发处理发送给JVM信号的线程
- Monitor Ctrl-Break:同步的monitor线程
看上面例子,能清楚的看到,一个Java程序的运行不仅是main()方法的运行,而是main线程和多个其他的线程同时运行,这就是多线程。
多线程:多线程就是指一个进程中同时有多个执行路径(线程)正在执行
。
2、线程状态
Java线程在运行的生命周期中可能处于下表所示的6种不同状态,在给定的一个时刻,线程只能处于其中一个状态。
状态名称 | 说明 |
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的成为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁(这里有个容易混淆的问题,下面会讲) |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断,想下前几篇文章,进入同步队列获取锁的过程,想想有什么问题) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
注意,在上一篇讲ReentrantLock(重入锁)的时候,当已经有线程获取了,其余线程会进入同步队列,尝试自旋几次之后调用LockSupport.park()方法,这个方法是用来阻塞当前线程,那么在调用了这个方法之后,在同步队列中线程的状态是什么呢?BLOCKED还是WAITING?答案是线程处于WAITING(等待状态)
,阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块时的状态,但是阻塞在Lock接口的线程状态是等待状态。
线程在自身的生命周期中状态变迁图如下所示:
3、多线程的优点和弊端
3.1 优点
- 多线程技术使程序的响应速度更快(
比如打开一个网页,调用一个接口,这个接口创建了很多线程去数据库读取数据异步去返回数据,这样用户立马就打开了网页,里面的内容的展现可能不是一起展示,但是总比空白界面停留十几秒再全部展示出来好吧
); - 当前没有进行处理的任务时可以将处理器时间让给其它任务;
- 占用大量处理时间的任务可以定期将处理器时间让给其它任务;
- 可以随时停止任务(
中断某个线程
); - 可以分别设置各个任务的优先级以优化性能(
在线程构建的时候通过setPriority(int)方法来修改优先级,范围为1-10,默认优先级为5,优先级高的线程分配时间片的数量要多于优先级低的线程
)。
一个线程在一个时刻只能运行在一个处理器核心上,使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,变得更有效率:
3.2 弊端
- 等候使用共享资源时造成程序的运行速度变慢(
比如库存,如果多个线程同时去扣减,就有可能变成负数,这样是不被允许的,所以就需要之前几篇文章讲的锁来控制,所以前一个线程获取了资源,后一个线程就会被阻塞,造成程序的运行速度变慢
)。 - 对线程进行管理要求额外的CPU开销,线程的使用会给系统带来上下文切换的额外负担(
多线程是通过分配CPU时间片来实现的,时间片非常短,所以CPU会不停的切换线程执行,当一个线程执行一个时间片后会切到下一个任务,但是在切换之前会保存上一个任务的状态,下次在切换回这个任务的时候,可以再加载这个任务的状态,这就是上下文切换
)。 - 线程的死锁。即对共享资源加锁实现同步的过程中可能会死锁。
- 对公有变量的同时读或写,可能对造成脏读等(
这就是锁该做的事情
)。 - 线程创建和销毁会造成消耗(
这就是线程池该做的事情了
)。
4、线程池的好处
-
降低资源消耗(
可以通过重复利用已创建的线程来降低线程创建和销毁造成的消耗
)。 -
提高相应速度(
当任务到达时,任务可以不需要等到线程创建就能立即执行
)。 -
提高线程的可管理性(
线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控
)。
5、线程池的新建
public class ThreadTest {
/**
* 基于数组的有界阻塞队列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 创建一个线程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new ThreadPoolExecutor.AbortPolicy());
// 上面是创建线程池的代码,下面只是用来测试拒绝策略的
for (int i = 0; i < 30; i++){
threadPoolExecutor.execute(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
});
}
}
}
- corePoolSize:线程池核心线程数大小。
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时不再创建。 - maximumPoolSize:线程池最大线程数量。
线程池允许创建的最大线程数。如果阻塞队列满了,并且已创建的线程数小于最大线程数,则线程池会在创建新的线程执行任务(如果使用了无界队列,那么这个参数就没什么用了
)。 - keepAliveTime:线程池中非核心线程空闲的存活时间大小。
- unit:线程空闲存活时间单位。
- workQueue:存放任务的阻塞队列。
用于保存等待执行的任务的阻塞队列。可以选择以下几个队列:
1、ArrayBlockingQueue
:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
:一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue。
3、SynchronousQueue
:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作。否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
4、PriorityBlockingQueue
:一个具有优先级的无限阻塞队列。 - threadFactory:用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题,也可以设置线程执行出现异常的处理策略(下面文章会讲)。
如上图,每个线程的name都以wx开头。 - handler:线城池的饱和策略事件,主要有四种类型。
1、AbortPolicy
:直接抛出异常,如下图: - 2、
CallerRunsPolicy
:用调用者所在线程来运行任务。
3、DiscardOldestPolicy
:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy
:不处理,丢弃掉。 - 除了上面这四种,还有一种自定义策略,实现RejectedExecutionHandler接口即可:
public class Handler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 打印线程信息
System.out.println(r.toString());
}
}
6、线程池状态
线程池的状态主要有以下几种:
状态名称 | 说明 |
RUNNING | 初始状态,能够接收新任务,以及对已添加的任务进行处理 |
SHUTDOWN | 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务,处理完成之后才会退出。 |
STOP | 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 |
TIDYING | 当所有的任务已终止,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理,可以通过重载terminated()函数来实现。 |
TERMINATED | 线程池执行完钩子函数terminated()之后,就变成TERMINATED状态。 |
这里有几个要注意的点:
- 当调用了shutdown()方法之后,就会从RUNNING转变为SHUTDOWN状态,此时不能再向线程池添加新任务,否则将会抛出RejectedExecutionException异常。
- 当调用了shutdownNow()方法之后,就会从RUNNING转变为STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
- 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN转变为TIDYING状态。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP转变为TIDYING状态。
图片来源:百度搜索
7、线程池执行任务
上面我们创建了一个线程池,并且通过execute()方法提交了任务,然后可以看出上面抛出了异常(拒绝策略), 为什么会这样呢,下面可以看下线程池的处理流程图就明白了:
线程池处理任务流程:
- 1: 通过判断核心线程池里的线程是否都在执行任务,如果不是,则创建一个线程去执行,如果核心线程都在执行任务。那么就判断阻塞队列。
-
2: 判断阻塞队列是否已满,如果没满,就将任务加到队列中,如果满了,就判断创建的线程是否达到了最大数量(
所以这里有个问题,如果你队列是无界的,那么可以一直往里面添加任务,这就有可能引起内存溢出,这也是阿里官方手册为什么建议用ThreadPoolExecutor去创建线程池了
)。 -
3: ,判断创建的线程是否达到了最大数量,如果没有达到,就创建一个线程去执行任务,如果有达到,就执行拒绝策略(
默认的拒绝策略是抛出异常,就上面例子抛出的那个异常
)。
接下来看下execute()方法的源代码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取当前有效的线程数和线程池的状态
int c = ctl.get();
// 判断正在运行线程数是否小于核心线程池,是则新创建一个线程执行任务,否则将任务放到任务队列中
if (workerCountOf(c) < corePoolSize) {
// 在addWorker中创建工作线程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 线程池是否处于运行状态,且是否任务插入任务队列成功
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 线程池是否处于运行状态,如果不是则使刚刚的任务出队
if (! isRunning(recheck) && remove(command))
// 执行拒绝策略
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 插入队列不成功,且当前线程数数量小于最大线程池数量,此时则创建新线程执行任务,创建失败的话就执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
可以看出,execute()方法是没有返回值的
,所以你提交任务之后,是无法判断任务是否被多线程执行成功,所以多线程还有一种提交方式,submit()方法,通过submit()方法提交任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否提交成功,,并且可以通过future.get()来获取返回值,get()方法会阻塞当前线程直到任务完成。实例如下:
public class ThreadTest {
/**
* 基于数组的有界阻塞队列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 创建一个线程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
Future<?> submit = null;
// 上面是创建线程池的代码,下面只是用来测试拒绝策略的
for (int i = 0; i < 10; i++){
int num = i;
submit = threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
});
try {
submit.get();
}catch (Exception e){
System.out.println("线程执行出现异常");
}
}
}
}
线程池异常处理
先来看一段代码:
public class ThreadTest {
/**
* 基于数组的有界阻塞队列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 创建一个线程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
// 上面是创建线程池的代码,下面只是用来测试拒绝策略的
for (int i = 0; i < 10; i++){
int num = i;
threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
if (num == 4){
throw new RuntimeException();
}else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}
});
}
}
}
上述代码可以看到,当i=4的时候,会抛出一个异常,然后看下结果:
本该打印10行结果的,现在只打印了9行,执行报错但是没有抛出异常,这样我们无法感知任务出现了异常,也就无法做相应处理。
但你把上面代码的提交方式改为execute()
,再次运行,你会发现有异常抛出的:
这是为啥子呢,怎么解决呢,先来说怎么解决,再说为啥submit()方法提交任务会将其中可能发生的异常吃
掉。解决方法如下:
- 添加try/catch捕获异常
public class ThreadTest {
/**
* 基于数组的有界阻塞队列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 创建一个线程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
// 上面是创建线程池的代码,下面只是用来测试拒绝策略的
for (int i = 0; i < 10; i++){
int num = i;
threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
try {
if (num == 4){
throw new RuntimeException();
}else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}catch (Exception e){
System.out.println("线程:" + Thread.currentThread().getName() + "执行任务出现了异常");
}
}
});
}
}
}
查看结果:
- 利用submit()方法返回的future对象的get()方法来查看程序执行是否有异常产生:
public class ThreadTest {
/**
* 基于数组的有界阻塞队列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 创建一个线程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
// 上面是创建线程池的代码,下面只是用来测试拒绝策略的
for (int i = 0; i < 10; i++){
int num = i;
Future<?> submit = threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
if (num == 4) {
throw new RuntimeException();
} else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}
});
try {
submit.get();
}catch (Exception e){
System.out.println("线程:" + Thread.currentThread().getName() + "执行任务出现了异常");
}
}
}
}
查看结果:
你会发现,它不像上面那个try/catch具体到线程池内那个线程出现了问题,而是说你的主线程执行任务出现了异常
。
还有一种解决方案,这种异常解决方案是execute()方法提交的任务执行出现异常的处理方式,submit()方法提交的不适用。
在定义ThreadFactory的时候,调用setUncaughtExceptionHandler()方法来自定义异常处理方式:
public class ThreadTest {
private static final Logger logger = LoggerFactory.getLogger(ThreadTest.class);
/**
* 基于数组的有界阻塞队列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 创建一个线程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder()
// 设置线程名称
.setNameFormat("wx-%d")
// 添加自定义异常处理方式:打印error日志
.setUncaughtExceptionHandler((thread, throwable)-> logger.error("ThreadPoolExecutor {} produce exception", thread,throwable))
.build(),
// handler
new Handler());
// 上面是创建线程池的代码,下面只是用来测试拒绝策略的
for (int i = 0; i < 10; i++){
int num = i;
threadPoolExecutor.execute(new Runnable() {
@SneakyThrows
@Override
public void run() {
if (num == 4) {
throw new RuntimeException();
} else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}
});
}
}
}
查看结果:
为什么submit()方法提交任务产生异常会被"吞掉"
说到这个问题,我们得先来看下submit()方法的源码:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 任务被包装成RunnableFuture对象,准备添加到工作队列中
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
newTaskFor()方法代码如下:
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
FutureTask类代码如下:
public class FutureTask<V> implements RunnableFuture<V> {
......
}
RunnableFuture接口提供了一个run()方法:
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
看下FutureTask类的run()方法做了什么:
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 捕获异常
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
再看下setException()方法:
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 将异常放入outcome对象中
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
至此,我们可以看到,submit()方法其实是将任务包装成RunnableFuture对象,其实最终是一个FutureTask实例,FutureTask实现了Future和Runnable接口。重写了run(),而在run()方法里面,该任务抛出的异常将被捕获,通过setException()方法将异常放在outcome中
,这就是为什么没有抛出异常的原因。
那么问题来了,为什么调用submit()提交任务之后返回的FutureTask对象的get()方法就会看到异常呢,看get()方法源码:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
report()方法代码:
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
因为get()方法会将存放异常的outcome对象返回出去
,这就是为什么调用submit()提交任务之后返回的FutureTask对象的get()方法就会看到异常的原因!