加油,每天一篇博客,听一遍好运来
目录
1.简介
线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。
2.线程池
2.1 线程池的作用
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
2.2 为什么要用线程池
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
3.线程池的创建
3.1 线程池实例
线程池的最上层接口是Executor,这个接口定义了一个核心方法execute(Runnablecommand),这个方法是用来传入任务的,最后被ThreadPoolExecutor类实现。而且ThreadPoolExecutor是线程池的核心类
我来写一段代码给大家先熟悉熟悉线程池:
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行....");
}
}
public class singleThreadExecutorTest{
public static void main(String[] args) {
//阻塞队列
LinkedBlockingDeque queue = new LinkedBlockingDeque(3);
ThreadPoolExecutor pool = new ThreadPoolExecutor(1,2,3, TimeUnit.SECONDS, queue);
//执行第一个任务
pool.execute(new MyThread());
//队列有三个任务等待
pool.execute(new MyThread());
pool.execute(new MyThread());
pool.execute(new MyThread());
//执行第五个任务
pool.execute(new MyThread());
//执行第六个任务
// pool.execute(new MyThread()); 会出现什么问题?
pool.shutdown();
}
}
上述:ThreadPoolExecutor(…)里面的参数都是什么意思,我们看看源码:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序
图解分析:
过程:当我们启动第一个任务,会先判断核心线程是否满了,没满就创建核心线程执行这个任务,满了就把任务添加到任务队列中,当任务队列也满的时候,则创建临时线程执行任务,当我们的线程数量达到最大值的时候,会执行拒绝策略,说白了就是:我没多了线程执行你新增的任务了,我不干了!
比较重要的几个类:
ExecutorService | 真正的线程池接口。 |
---|---|
ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 |
ThreadPoolExecutor | ExecutorService的默认实现。 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
在Executors类里面提供了一些静态工厂,生成一些常用的线程池:
3.1 四种线程池的使用
- newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
public class singleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread().getName()+" "+index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
public class newFixedThreadPoolTest {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread().getName()+" "+index);
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
3.newCachedThreadPool
1.一个在需要处理任务时才会创建线程的线程池,如果一个线程处理完任务了还没有被回收,那么线程可以被重复使用。
2.当我们调用execute方法时,如果之前创建的线程有空闲可用的,则会复用之前创建好的线程,否则就会创建新的线程加入到线程池中。
3.创建好的线程如果在60s内没被使用,那么线程就会被终止并移出缓存。因此,这种线程池可以保持长时间空闲状态而不会消耗任何资源。
方式1:我们在在执行execute之前休眠一段时间,部分线程可以被复用
public class Test {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+" "+index);
}
});
}
cachedThreadPool.shutdown();
}
}
方式2:不加休眠时间
public class Test {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+" "+index);
}
});
}
cachedThreadPool.shutdown();
}
}
大家可以运行上述代码,我们在execute之前休眠一段时间目的是:我们加入任务的时候,可能上一个任务结束,线程还未被回收,那我们就可以重复利用这个线程
4.newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
public class Test1 {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 15; i++) {
scheduledThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
scheduledThreadPool.shutdown();
}
}
3.2 线程池实现原理
1、原理:
提交一个任务到线程池中,线程池的处理流程如下:
1、判断**线程池里的核心线程**
是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断**线程池里的线程
**是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
4.ThreadPoolExecutor详解
1、ThreadPoolExecutor的execute()方法
2.addWork方法
private boolean addWorker(Runnable firstTask, boolean core) {
//第一段逻辑:线程数+1
retry:
for (;;) {
int c = ctl.get();//获取线程池容量
int rs = runStateOf(c);//获取状态
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&//即:SHUTDOWN,STOP,TIDYING,TERMINATED
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))//即:rs==RUNNING,firstTask!=null,queue==null
return false;//如果已经关闭,不接受任务;如果正在运行,且queue为null,也返回false
for (;;) {
int wc = workerCountOf(c);//获取当前的工作线程数
//如果工作线程数大于等于容量或者大于等于核心线程数(最大线程数),那么就不能再添加worker
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))//cas增加线程数,失败则再次自旋尝试
break retry;
c = ctl.get(); // Re-read ctl //再次获取工作线程数
if (runStateOf(c) != rs)//不相等说明线程池的状态发生了变化,继续自旋尝试
continue retry;
}
}
//第二段逻辑:将线程构造成Worker对象,并添加到线程池
boolean workerStarted = false;//工作线程是否启动成功
boolean workerAdded = false;//工作线程是否添加成功
Worker w = null;
try {
w = new Worker(firstTask);//构建一个worker
final Thread t = w.thread;//去除worker中的线程
if (t != null) {
final ReentrantLock mainLock = this.mainLock;//获取重入锁
mainLock.lock();//上锁
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());//获得锁之后,再次检查状态
//只有当前线程池是正在运行状态,[或是 SHUTDOWN 且 firstTask 为空],才能添加到 workers 集合中
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);//将新创建的 Worker 添加到 workers 集合中
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;//更新线程池中线程的数量
workerAdded = true;//添加线程(worker)成功
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();//这里就会去执行Worker中的run()方法
workerStarted = true;//启动成功
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);//如果启动线程失败,需要回滚
}
return workerStarted;
}
这个方法主要就是做两件事:将线程数+1、将线程构造成Worker对象,加入到线程池中,并调用start()方法启动线程
3、Worker对象
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;//真正执行的task
/** Initial task to run. Possibly null. */
Runnable firstTask;//需要执行的task
/** Per-thread task counter */
上面这个方法继承了AbstractQueuedSynchronizer,前面我们讲述AQS同步队列的时候知道,AQS就是一个同步器
Worker中state初始化状态设置为-1,原因是在初始化Worker对象的时候,在线程真正执行runWorker()方法之前,不能被中断。而一旦线程构造完毕并开始执行任务的时候,是允许被中断的,所以在线程进入runWorker()之后的第一件事就是将state设置为0(无锁状态),也就是允许被中断。
我们再看看Worker的构造器:
addWork方法执行到这句:w = new Worker(firstTask);//构建一个worker 的时候,就会调用构造器创建一个Worker对象,state=-1,并且将当前任务作为firstTask,后面再运行的时候会优先执行firstTask。
上面addWorker方法在worker构造成功之后,就会调用worker.start方法,这时候就会去执行Worker中的run()方法,这也是一种委派的方式
run()方法中调用了runWorker(this)方法,这个方法就是真正执行任务的方法:
4、runWorker(this)
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
/**
* 表示当前worker线程允许中断,因为new Worker默认的 state=-1,此处是调用
* Worker类的 tryRelease()方法,state置为 0,
* 而 interruptIfStarted()中只有 state>=0 才允许调用中断
*/
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
/**
* 加锁,这里加锁不仅仅是为了防止并发,更是为了当调用shutDown()方法的时候线程不被中断,
* 因为shutDown()的时候在中断线程之前会调用tryLock方法尝试获取锁,获取锁成功才会中断
*/
w.lock();
// If pool is stopping, ensure thread is interrupted;
//ifnot, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
/**
* 如果是以下两种情况,需要中断线程
* 1.如果state>=STOP,且线程中断标记为false
* 2.如果state<STOP,获取中断标记并复位,如果线程被中断,那么,再次判断state是否STOP
* 如果是的话,且线程中断标记为false
*/
if ((runStateAtLeast(ctl.get(), STOP) ||//状态>=STOP
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();//中断线程
try {
beforeExecute(wt, task);//空方法,我们可以重写它,在执行任务前做点事情,常用于线程池运行的监控和统计
Throwable thrown = null;
try {
task.run();//正式调用run()执行任务
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);//执行任务之后调用,也是个空方法,我们可以重写它,在执行任务后做点事情,常用于线程池运行的监控和统计
}
} finally {
task = null;//将任务设置为空,那么下次循环就会通过getTask()方法从workerQueue中取任务了
w.completedTasks++;//任务完成数+1
w.unlock();
}
}
completedAbruptly = false;
} finally {
//核心线程会阻塞在getTask()方法中等待线程,除非设置了允许核心线程被销毁,
// 否则正常的情况下只有非核心线程才会执行这里
processWorkerExit(w, completedAbruptly);//销毁线程
}
}
主要执行步骤为:
1、首先释放锁,因为进入这个方法之后线程允许被中断
2、首先看看传入的firstTask是否为空,不为空则优先执行
3、如果firstTask为空(执行完了),则尝试从getTask()中获取任务,getTask()就是从队列l里面获取任务
4、如果获取到任务则开始执行,执行的时候需要重新上锁,因为执行任务期间也不允许中断
5、任务运行前后分别有一个空方法,我们可以在有需要的时候重写这两个方法,实现付线程池的监控
6、如果获取不到任务,则会执行processWorkerExit方法销毁线程
6、getTask()方法
privateRunnablegetTask(){
//上一次获取任务是否超时,第一次进来默认false,第一次自旋后如果超时就会设置为true,则第二次自旋就会返回null
booleantimedOut=false;//Didthelastpoll()timeout?
for(;;){
intc=ctl.get();
intrs=runStateOf(c);
//Checkifqueueemptyonlyifnecessary.
/**
*1.线程池状态为shutdown,那么就必须要等到workQueue为空才行,因为shutdown()状态是需要执行队列中剩余任务的
*2.线程池状态为stop,那么就不需要关注workQueue中是否有任务
*/
if(rs>=SHUTDOWN&&(rs>=STOP||workQueue.isEmpty())){
decrementWorkerCount();//线程池中的线程数-1
returnnull;//返回null的话,那么runWorker方法中就会跳出循环,执行finally中的processWorkerExit方法销毁线程
}
intwc=workerCountOf(c);
//Areworkerssubjecttoculling?
//1.allowCoreThreadTimeOut-默认false,表示核心线程数不会超时
//2.如果总线程数大于核心线程数,那就说明需要有线程被销毁
booleantimed=allowCoreThreadTimeOut||wc>corePoolSize;
/**
*1.线程数量超过maximumPoolSize可能是线程池在运行时被调用了setMaximumPoolSize()
*被改变了大小,否则已经addWorker()成功的话是不会超过maximumPoolSize。
*2.timed&&timedOut如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中
*获取任务发生了超时.其实就是体现了空闲线程的存活时间
*/
if((wc>maximumPoolSize||(timed&&timedOut))
&&(wc>1||workQueue.isEmpty())){
if(compareAndDecrementWorkerCount(c))
returnnull;
continue;
}
try{
Runnabler=timed?
workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)://等待指定时间后返回
workQueue.take();//拿不到任务会一直阻塞(如核心线程)
if(r!=null)
returnr;//如果拿到任务了,返回给worker进行处理
timedOut=true;//走到这里就说明到了超期时间还没拿到任务,设置为true,第二次自旋就可以直接返回null
}catch(InterruptedExceptionretry){
timedOut=false;
}
}
}
1、首先判断状态是不是对的,如果是SHUTDOWN之类不符合要求的状态,那就直接返回null,并把线程数-1,而返回null之后前面的方法就会跳出while循环,执行销毁线程流程。
2、判断下是不是有设置超时时间或者最大线程数超过了核心线程数
3、根据上面的判断决定是执行带有超时时间的poll方法还是take方法从队列中获取元素。 情况一:如果是执行带超时时间的poll方法,那么时间到了如果还没取到元素,那么就返回空,这种情况说明当前系统并不繁忙,所以返回null之后线程就会被销毁; 情况二:如果是执行take方法,根据第2点的判断知道,除非我们人为设置了核心线程可以被回收,否则核心线程就是会执行take方法,如果获取不到任务就会一直阻塞等待获取到任务为止。
7、processWorkerExit方法
这是销毁线程的方法,上面的getTask()方法返回空,就会执行线程销毁方法,因为getTask()当中已经把线程数-1了,所以这里可以直接执行线程销毁工作。
直接调用的是workers集合的remove()方法,后面还有就是尝试中止和一些异常异常情况的补偿操作。
RejectedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:
1、AbortPolicy:直接抛出异常
2、CallerRunsPolicy:只用调用所在的线程运行任务
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。
5.面试题
问题一
Q:为什么不建议直接使用Executors来构建线程池?
A:用Executors
使得我们不用关心线程池的参数含义,这样可能会导致问题,比如我们用newFixdThreadPool或者newSingleThreadPool.允许的队列长度为Integer.MAX_VALUE,如果使用不当会导致大量请求堆积到队列中导致OOM的风险而newCachedThreadPool,允许创建线程数量为
Integer.MAX_VALUE,也可能会导致大量
线程的创建出现CPU使用过高或者OOM的问题。而如果我们通过ThreadPoolExecutor来构造线程池的话,我们势必要了解线程池构造中每个
参数的具体含义,会更加谨慎。
问题二
Q:如何合理配置线程池的大小?
A:要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析: 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级:高、中和低。 任务的执行时间:长、中和短。 任务的依赖性:是否依赖其他系统资源,如数据库连接。 CPU密集型:
CPU密集型的特点是响应时间很快,cpu一直在运行,这种任务cpu
的利用率很高,那么线程数的配置应该根据CPU核心数来决定,CPU核心数=最大同时执行线程数,假如CPU核心数为4,那么服务器最多能同时执行4个线程。过多的线程会导致上
下文切换反而使得效率降低。那线程池的最大线程数可以配置为cpu核心数+1。IO密集型: 主要是进行IO操作,执行IO操作的时间较长,这是cpu会处于空闲状态,
导致cpu的利用率不高,这种情况下可以增加线程池的大小。可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置cpu核心数的2倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/ 线程CPU时间 )* CPU数目附:获取CPU个数方法:Runtime.getRuntime().availableProcessors()
问题三
Q:线程池中的核心线程什么时候会初始化?
A:默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过如下两个方法:prestartCoreThread():初始化一个核心线程。 prestartAllCoreThreads():初始化所有核心线程
问题四
Q:线程池被关闭时,如果还有任务在执行,怎么办?
A:线程池的关闭有两个方法:
shutdown() 不会立即终止线程池,要等所有任务缓存队列中的任务都执行完后才终止,但是不会接受新的任务 shutdownNow()
立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务任务
问题五
Q:线程池容量是否可以动态调整?
A:可以通过两个方法动态调整线程池的大小。
setCorePoolSize():设置最大核心线程数 setMaximumPoolSize():设置最大工作线程数