线程池以及自定义线程池
为了放置资源被滥用,合理控制连接的数量,我们采用池化的操作。
数据库连接池:不能随便new连接
也不能随便new线程,保证线程的统一处理。
线程池
java通过Excutors提供四种线程池,分别为:
- newCachedThreadPool
- newFixedThreadPool
- newScheduledThreadPool
- newSingleThreadPool
线程池的核心参数
根据给这些参数传入不同的值来确定我们想要什么样的线程池。
int corePoolSize, //核心线程数,无论忙不忙,池子里都会有这么多线程,只要new除这个池子,就会有这么多的线程
int maximumPoolSize,//最大线程数,当任务比核心线程数多时,就会new线程,但上限是maximumPoolSize
long keepAliveTime,//线程超过核心线程时,闲下来的时候过多久会回收
TimeUnit unit,//上面时间的单位
BlockingQueue<Runnable> workQueue,//阻塞队列(卡在那里等着你取),线程池无法处理的多余的任务放在这里排队
ThreadFactory threadFactory,//线程工厂,负责创建线程
RejectedExecutionHandler handler//拒绝策略
阻塞队列
放10个数字,取12个数字,此时程序不会结束,会等着人往里再送两个。
注意BlockingQueue是个接口,具体的实现是ArrayBlockingQueue和LinkedBlockingQueue
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author SJ
* @date 2021/4/2
*/
public class TestBlockingQueue {
public static void main(String[] args) throws InterruptedException {
//新建一个容量为5的阻塞队列
BlockingQueue<Integer> blockingQueue=new ArrayBlockingQueue<>(3);
//新建两个任务,一个负责放,一个负责取
//放三个数字
Runnable t1=()->{
for (int i = 0; i < 3; i++) {
System.out.println("放入"+i);
try {
blockingQueue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//取5个数字
Runnable t2=()->{
for (int i = 0; i < 5; i++) {
System.out.println("取出"+i);
try {
blockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//创建一个线程池,把任务丢进去
ExecutorService pools= Executors.newFixedThreadPool(2);
pools.submit(t2);
pools.submit(t1);
}
}
执行后程序并未终止,因为需要取5个数字,但队列中只有三个数字,任务还未完成,所以阻塞在这里等人放数字进去。
拒绝策略
如果使用阻塞队列使用ArrayBlockingQueue,那么任务排的队就是有序的。LinkedBlockingQueue可以做无界队列。
当线程也满了,队列也满了,这时就要拒绝任务。
拒绝策略:
有四种
AbortPolicy:抛异常
CallerRunsPolicy:先尝试运行再丢
DiscardOldestPolicy:丢弃队列里等待时间最长的那个
DiscardPolicy:直接丢
newFixedThreadPool
核心线程数:nThreads
最大线程数:nThreads
核心线程数和最大线程数相同,就没有活跃时间这一说法了。活跃时间是指超出核心线程数的部分的活跃时间
单位:毫秒
等待队列是个LinkedBlockingQueue无界队列,所有任务全部接收,没有拒绝策略
newSingleThreadPool
核心线程数:1
最大线程数:1
无界队列
newCachedThreadPool
核心线程数:0
最大线程数:整型范围上届,相当于不限制最大线程数
活跃时间:60
单位:秒
等待队列:同步队列
同步队列
SynchronousQueue简单使用
经典的生产者-消费者模式,操作流程是这样的:
有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;
有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞
SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。
put线程执行queue.put(1) 后就被阻塞了,只有take线程进行了消费,put线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。
实现原理
不像ArrayBlockingQueue、LinkedBlockingQueque之类的阻塞队列依赖AQS实现并发操作,SynchronousQueue直接使用CAS实现线程的安全访问。
队列的实现策略通常分为公平模式和非公平模式。
**公平模式:**底层实现使用的是TransferQueue这个内部队列,它有一个head和tail指针,用于指向当前正在等待匹配的线程节点。
队尾匹配队头出队,先进先出,体现公平原则
**非公平模式:**底层的实现使用的是TransferStack,一个栈,实现中用head指针指向栈顶,
后进先匹配。
newScheduledThreadPool
核心线程数:corePoolSize
最大线程数:没有限制
超时时间:0
等待队列:延时队列
延时队列
该队列是定制的优先级队列,只能用来存储RunnableScheduledFutures任务。底层为堆结构
我们知道线程池运行时,会不断从任务队列中获取任务,然后执行任务。如果我们想实现延时或者定时执行任务,重要一点就是任务队列会根据任务延时时间的不同进行排序,延时时间越短地就排在队列的前面,先被获取执行。
自定义线程池
手动创建线程池,效果会更好。
1.线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。
Executors返回的线程池对象的弊端如下:
-
FixedThreadPool和SingleThreadPool
允许请求队列长度为Integer.MaxValue, 可能会堆积大量的请求,从而导致内存溢出
-
CachedThreadPool:
允许创建线程数量为Integer.MaxValue,可能会创建大量的线程,从而导致内存溢出
实例
下面我们来自己写一个线程池:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author SJ
* @date 2021/4/3
*/
public class MyExecutor {
public static void main(String[] args) {
//使用ThreadPoolExecutor这个类来构造线程池
ThreadPoolExecutor MyThreadPool = new ThreadPoolExecutor(
4,//核心线程数
8,//最大线程数
5,//活跃时间
TimeUnit.SECONDS,//单位
new ArrayBlockingQueue<>(300),//阻塞队列,最多300个排队
new MyThreadFactory(new ThreadGroup("sunjie"),"goods-thread"),//线程工场
new ThreadPoolExecutor.AbortPolicy()//拒绝策略采用抛异常
);
}
//创建自己的线程工厂,不会写就源码拿过来改一改
static class MyThreadFactory implements ThreadFactory{
private static final AtomicInteger poolNumber = new AtomicInteger(0);
private ThreadGroup group;
private String namePrefix;
//构造函数,传入组名和前缀
public MyThreadFactory(ThreadGroup group, String namePrefix) {
this.group = group;
this.namePrefix = namePrefix;
}
@Override
public Thread newThread(Runnable r) {
//新建一个线程,指明组名和线程名
Thread t = new Thread(group, r,
namePrefix +"-"+ poolNumber.getAndIncrement());
//设为非守护线程
if (t.isDaemon())
t.setDaemon(false);
//设定线程优先级为常规优先级
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
}
使用
使用自定义线程池
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author SJ
* @date 2021/4/3
*/
public class TestMyExecutor {
public static void main(String[] args) {
//新建一个线程池
MyExecutor myExecutor = new MyExecutor();
//调用自己创建的线程池
ThreadPoolExecutor myThreadPool = myExecutor.MyThreadPool;
//新建任务,不停的给线程池丢任务
while (true){
myThreadPool.execute(()->{
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(123);
});
}
}
}
再命令行输入 jconsole 选择自己定义的类查看当前活跃的线程
此时一共有4个活跃的线程,因为任务只是输出,执行的非常快,所以4个核心线程就够用了。
如果我们把任务改成
//新建任务,不停的给线程池丢任务
while (true){
myThreadPool.execute(()->{
try {
Thread.sleep(10000);//线程运行10s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(123);
});
}
控制台抛异常,因为排队的任务最多300个,while(true)会疯狂的丢任务进去,所以执行了拒绝策略。
此时4个核心线程不够用,就会new线程,最大线程数为8,此时有8个线程。