一:什么是线程池?
java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池,多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
机器完成一项任务所需时间为:创建线程时间t1,执行任务的时间t2, 销毁线程时间t3;如果t1 + t3 远大于 t2,则可以采用线程池,以提高服务器性能。线程池技术主要关注于线程的创建和销毁时间,把创建时间和销毁时间分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有创建时间和销毁时间的开销了。
二:线程池的四大基本组成成分?
- 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
- 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
- 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
三:线程池Executors工具类,三大方法
- 一次只能执行一个线程的线程池
static void demo1(){
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
//使用完线程池需要关闭,可以用 try-catch-finally
threadPool.shutdown();
/*
pool-1-thread-1执行了!!!
pool-1-thread-1执行了!!!
pool-1-thread-1执行了!!!
pool-1-thread-1执行了!!!
pool-1-thread-1执行了!!!
*/
}
- 一次可以执行固定参数的线程池
static void demo2(){
ExecutorService threadPool = Executors.newFixedThreadPool(3); //固定三个线程
for (int i = 0; i < 6; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
threadPool.shutdown();
/*
pool-1-thread-1执行了!!!
pool-1-thread-2执行了!!!
pool-1-thread-3执行了!!!
pool-1-thread-2执行了!!!
pool-1-thread-1执行了!!!
pool-1-thread-3执行了!!!
*/
}
- 可以自动变化的线程池
static void demo3(){
ExecutorService threadPool = Executors.newCachedThreadPool(); //可以自动变化的线程池
for (int i = 0; i < 6; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
//使用完线程池需要关闭,可以用 try-catch-finally
threadPool.shutdown();
/*
pool-1-thread-1执行了!!!
pool-1-thread-4执行了!!!
pool-1-thread-3执行了!!!
pool-1-thread-2执行了!!!
pool-1-thread-5执行了!!!
pool-1-thread-6执行了!!!
*/
}
四:线程池的七大参数?
在阿里云开发手册中明确指出创建线程时不能使用Executors创建线程,并且给出了原因,如下所示:
因此我们要对ThreadPoolExecutor的七个参数进行理解,下面时ThreadPoolExecutor的七个参数说明:
public ThreadPoolExecutor(
int corePoolSize, //核心池子大小,初始值
int maximumPoolSize, //最大池子大小 ,可以用CPU核数( Runtime.getRuntime().availableProcessors()),也可以根据IO
long keepAliveTime, // 开启备用池子之后,若规定时间keepAliveTime内没有线程执行,则关闭当前备用池子
TimeUnit unit, //超时时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列,休息等待区
ThreadFactory threadFactory, //创建线程的工厂
RejectedExecutionHandler handler//超额线程处理策略
)
比如我们定义一个线程池corePoolSize = 3,maximumPoolSize = 5,阻塞队列大小为3,我们用银行柜台的案例来解释整个过程,可以分成四个阶段;
- 当来的人不超过三个人时,系统正常运行,业务都能顺利办理;
- 当来的人超过3而不超过6时,那么整个系统也可以正常运行,只不过这个时候银行的休息区(BlockingQueue<>(3))里等待着要办理业务的人;
- 当来的人超过6而不超过8(8 = maximumPoolSize + 阻塞队列大小)时,这个时候系统就会有些变化,银行会重新新开一些还未使用的柜台共来的人使用,如果开启之后在keepAliveTime时间段内柜台还没有一个人,柜台又会重新关闭;
- 当银行来的人超过8之后,系统已经承受不住人流量了,系统就会崩溃,这个时候就需要一种拒绝策略来拒绝爆满之后的人;
五:线程的四大拒绝策略?
- 第一种拒绝策略:new ThreadPoolExecutor.AbortPolicy(),如果线程满了,则不处理新的进程,抛出异常;
如下:
static void demo1(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
/*
当totalThread达到3时,初始柜台刚好够处理;
当totalThread达到6时,休息区(阻塞队列)被占满,
当totalThread达到8时,额外的柜台也全部开启,
当totalThread超过8时,若新进去的线程无法处理,则抛出异常java.util.concurrent.RejectedExecutionException
*/
int totalThread = 8;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
- 第二种处理策略:new ThreadPoolExecutor.CallerRunsPolicy(),线程池满了,如果有新的哪里来的去哪里,不会抛出异常;
static void demo2(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());//如果超额,这里会交给main线程
int totalThread = 12;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
/*
pool-1-thread-2执行了!!!
pool-1-thread-4执行了!!!
main执行了!!!
pool-1-thread-1执行了!!!
pool-1-thread-3执行了!!!
pool-1-thread-1执行了!!!
pool-1-thread-4执行了!!!
main执行了!!!
pool-1-thread-5执行了!!!
pool-1-thread-2执行了!!!
pool-1-thread-1执行了!!!
pool-1-thread-3执行了!!!
*/
}
- 第三种处理策略:new ThreadPoolExecutor.DiscardPolicy(),线程池满了,丢掉线程,不会抛出异常
static void demo3(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());//如果超额,这里会交给main线程
int totalThread = 15;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
- 第四种处理策略:new ThreadPoolExecutor.DiscardOldestPolicy(), 线程池满了,会尝试和最早的线程去竞争,如果成功则加入,否则丢弃,不会抛出异常
static void demo4(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());//如果超额,这里会交给main线程
int totalThread = 14;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
六:多线程的常用辅助类
- 减法器CountDownLatch
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。 CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。这是一个一次性的现象 - 计数无法重置。
如下:
public class countDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5); //设置统计5个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕!");
countDownLatch.countDown();//计数器减1
}
,String.valueOf(i)).start();
}
countDownLatch.await();//5条线程不执行完不会执行下面代码,如果不加上这条代码,会先输出“全部执行完毕”
System.out.println("全部执行完毕");
/*
线程1执行完毕!
线程4执行完毕!
线程3执行完毕!
线程2执行完毕!
线程0执行完毕!
全部执行完毕
*/
}
}
- 加法器CyclicBarrier
允许一组线程全部等待彼此达到共同屏障点的同步辅助。
如下:
public class cyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
System.out.println("5条线程执行完毕");
});
for (int i = 0; i < 5; i++) {
final int j = i;
new Thread(() -> {
System.out.println("线程"+j+"执行完毕!");//拿到循环i需要加final
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
/*
线程0执行完毕!
线程4执行完毕!
线程3执行完毕!
线程1执行完毕!
线程2执行完毕!
5条线程执行完毕
*/
}
}
- 计数信号量Semaphore
在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞, 直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
如下:
public class semaphoreTest {
public static void main(String[] args) {
//三个停车位,并发限流操作
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
semaphore.acquire();//占领车位
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);//模拟停车2s
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//释放车位
}
},String.valueOf(i)).start();
}
/*
1抢到车位
0抢到车位
2抢到车位
1离开车位
0离开车位
3抢到车位
2离开车位
4抢到车位
5抢到车位
5离开车位
3离开车位
4离开车位
*/
}
}