up:: 线程池构造函数
说明:
(1) 本篇博客的主要内容:
● 是演示JDK给我们提供的4种自动创建线程的方式;可以得出【不推荐采用自动创建线程的方式,去创建线程】的结论;
● 自己根据具体需求,创建线程池时,线程数量的确定原则;
一:线程池应该手动创建还是自动创建?:应该手动创建;
手动创建更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险;
自动创建线程,会带来一些问题;
二:我们可以直接调用JDK封装好的构造方法,去自动创建线程池;但这可能会带来一些问题;
1.自动创建线程池的策略一:Executors.newFixedThreadPool();
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 演示JDK提供的自动创建线程池的方法:newFixedThreadPool();
*/
public class FixedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 0; i <100 ; i++) {
executorService.execute(new TaskTest());
}
}
}
/**
* 任务;
* 这个任务,仅仅用于演示;
*/
class TaskTest implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
说明:
(1) 程序内容说明;(这部分不是重点啦)
(2) newFixedThreadPool(int nThreads)原理分析;
● 这种线程池的特点:核心线程数和最大线程数相同,都设置为我们传递参数大小;队列使用的是无界队列;
● 那么很显然,对于这种线程池;因为队列是无界队列,所以,如果任务很多处理不完的时候,任务就可能会堆积在队列中,从而产生OOM错误;
● 这种线程池为什么要使用无界队列?:因为,无界队列可以满足这种线程池功能;因为,这种线程池,最大线程数和核心线程数相同,那么当任务很多的时候,我们不得不使用无界队列去存储新来的任务;
(3) 演示OOM内存溢出异常;
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 演示JDK提供的自动创建线程池的方法:newFixedThreadPool():产生OOM错误的情况;
*/
public class FixedThreadPoolOOM {
//为了能更对的把任务弄到队列中,更好的能看到OOM的效果,我们这个线程池的线程数设为了1
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
for (int i = 0; i <Integer.MAX_VALUE ; i++) {
executorService.execute(new TaskTest1());
}
}
}
class TaskTest1 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
● 为了能够更好的让任务到队列中去排队,以更快的能看到内存溢出,我们把线程池的线程数设为了1;
● 每个任务,我们都让其休眠很长时间,以模拟每个任务的执行都要占用很长时间;从而,能够更快的看到内存溢出;
● 我们执行的任务次数很大;这样能更好的消耗内存,从而能更快的看到OOM异常;
● 自己的PC机器的异常还是挺大的,所以为了能更快的消耗内存;我们这儿设置一下程序的可用异常;
● 运行程序,会报OOM异常;
2.自动创建线程池的策略二:Executors.newSingleThreadExecutor();
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutor {
private static ExecutorService executorService = Executors.newSingleThreadExecutor();
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
executorService.execute(new TaskTest2());
}
}
}
/**
* 任务;这个任务,仅仅用于演示;
*/
class TaskTest2 implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
说明:
(1) newSingleThreadExecutor()内容说明;
● 这种线程池的特点:核心线程数和最大线程数相同,都设置为了1;队列使用的是无界队列;
● 那么很显然,对于这种线程池;因为队列是无界队列,所以,如果任务很多处理不完的时候,任务就可能会堆积在队列中,占用大量的内存,就可能会产生OOM错误;
● 这种线程池为什么要使用无界队列?:因为,无界队列可以满足这种线程池功能;因为,这种线程池,最大线程数和核心线程数都是1,那么当任务很多的时候,我们不得不使用无界队列去存储新来的任务;
● newSingleThreadExecutor();这种线程池在实际中,使用的不多;
(2) 运行结果;
3.自动创建线程池的策略三:Executors.newCachedThreadPool();
这种方式创建的线程池可以缓存;这种线程池,会自动回收多余的线程;
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i <1000 ; i++) {
executorService.execute(new TaskTest3());
}
}
}
/**
* 任务;这个任务,仅仅用于演示;
*/
class TaskTest3 implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
说明:
(1) newCachedThreadPool()内容说明;
● 这种方式创建的线程池可以缓存;这种线程池,会自动回收多余的线程;
● 那么,因为newCachedThreadPool()创建线程时,没有限制最大线程数;那么,如果任务很多的话,其就会创建很多的线程,这就可能会导致OOM异常;
● 又因为,其核心线程数为0;所以如果长期没有任务的话,其所有的线程都会被销毁;
● 因为,这种线程池,我们没有限制最大线程数,所以,我们可以不需要队列来存储任务;;;;所以,这种线程池的队列,可以使用【new SynchronousQueue<Runnable()】这种没有容量的直接交接队列;
(2) 运行结果;
4.自动创建线程池的策略四:Executors.newScheduledThreadPool();
这种方式创建的线程池支持定时及周期性的去执行任务;在实际开发中,遇到定时任务的时候,就可以使用这种线程池;
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPool {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
//用法一:延时执行任务;比如,这儿我们设置:5秒后,再执行这个任务;
scheduledExecutorService.schedule(new TaskTest4(), 5, TimeUnit.SECONDS);
//用法二:以一定频率重复运行任务;比如,这儿我们设置:第一是在1秒后执行这个任务,然后每隔3秒中,再执行一下这个任务;
scheduledExecutorService.scheduleAtFixedRate(new TaskTest4(), 1, 3, TimeUnit.SECONDS);
//……
}
}
/**
* 任务;这个任务,仅仅用于演示;
*/
class TaskTest4 implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
说明:
(1) newScheduledThreadPool()内容说明;
● 这种方式创建的线程池支持定时及周期性的去执行任务;在实际开发中,遇到定时任务的时候,就可以使用这种线程池;
● 这种线程池,核心线程数设为我们传递值,最大线程数没有限制;
● 这种线程池,使用的是DelayedWorkQueue延迟队列;延迟队列的能力,就是可以根据我们的设置的时间先后,把队列中的任务作延迟;这是符合newScheduledThreadPool()这种线程池的的需求的;
(2) 运行结果;
5.四种线程池的总结;
三:所以,比较合适的做法是:根据自己的业务场景去设置线程池参数,手动创建线程池;
1. 根据具体的业务场景,去设置线程池的参数;
● 比如,自身硬件设备的内存有多大;想给线程取什么名字(如果我们在创建线程时,想给线程取一个自己的名字;那么就需要利用自己的线程工厂);
● 还比如,任务被拒绝时,该如何记录日志;
● 再比如,根据我们业务的并发量,来决定线程池的线程数量;
2.线程池的线程数量,设为多少?
(1) 如果任务是CPU密集型(大量的计算的任务;比如加密,计算hash等;执行这些任务时,CPU会是满负荷的;)的,那么线程池的线程数量应该设为CPU核心数的1-2倍;(比如,我们的CPU是8核,那么我们线程池的核心线程数就可以设为8-16之间)
(2) 如果任务是耗时IO型(读写数据库、文件,网络通信等;一旦涉及到网络通信时,一般CPU是不工作的;或者读写文件时,外设的速度是低于CPU的速度的,所以CPU很可能会空闲)的,那么线程池的线程数量可以设为CPU核心数的很多倍;(比如,我们的CPU是8核,那么我们线程池的核心线程数就可以设为80;;;;虽然有80个线程,但是实际执行的时候,因为读写速度或者网络速度较慢,所以会有很多线程在等待,即会有很多线程是没有占用CPU的,,但是,由于我们设置了80个线程,这就就能充分利用CPU)
(3) 一个可以参考的线程数计算方法是:线程数=CPU核心数*(1 + 平均等待时间/平均工作时间);
● 平均等待时间长,表示线程的等待时间;;平均工作时间,指的是线程占用数据库的时间;
● 比如,对于读取数据库这种业务,线程大部分时间是等待的,其等待时间假如说是100秒,而其占用CPU的计算时间假如说是1秒;那么,其线程数=CPU核心数*(1+100/1);假如CPU是8核,那么此时线程池的核心线程数就可以设为808;
(4) 其实,整体的原则就是:
● 如果一个业务在执行的时候,大部分时间是需要占用CPU的,那么线程数就需要设置的小一点;(因为,业务的大部分时间已经占用了CPU了,CPU的空闲时间已经不多了,即CPU的可压榨空间已经很少了;继续创建线程的意义不大);
● 如果一个业务在执行的时候,大部分时间是不需要占用CPU的,那么线程数就可以设置大一点;即,我们尽量多开几个线程,以充分压榨CPU;
(5) 在实际开发中,最精准的策略是:根据不同的程序去做压测;根据压测结果,得到一个相对合适的线程数量;