开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情
本文主要讲解JUC中的阻塞队列、同步队列和线程池,线程池内容包含三大方法、七大参数、四大拒绝策略。
10. 阻塞队列
写入:如果队列满,就必须阻塞等待;取出:如果队列空,必须阻塞等待生产。
继承关系简略结构:
队列添加、移除、检测队列首元素方法:
会抛出异常的:add(添加)、remove(移除)、element(检测队首元素)
有返回值,不抛出异常:offer(添加)、poll(移除)、peek(检测队首)
阻塞等待:put、take
超时等待:offer和poll
- put和take是无返回值的
- offer和poll是可以含有多个参数的
-
* offer有三个参数,第一个参数是添加的东西,第二个是等待的时间,第三个是时间的单位 * poll有两个参数,第一个是等待的时间,第二个是时间的单位 * 满了等待,不抛异常不返回布尔;等待,阻塞(等待超时,超时就不等了)
public static void test4() throws InterruptedException {
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a",2, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("b",2,TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("c",2,TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("d",2,TimeUnit.SECONDS));
System.out.println("=================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
}
同步队列SynchronousQueue
没有容量,进去一个元素就要等待这个元素被取出,才能再往里面放一个元素。
用的是put和take方法。
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
//两个线程一个放一个取
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + " put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName() + " put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName() + " put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() +"="+ blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() +"="+ blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() +"="+ blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
运行结果:
T1 put 1
T2=1
T1 put 2
T2=2
T1 put 3
T2=3
11.线程池
内容:三大方法、七大参数、四种拒绝策略。
池化技术:程序的运行本质是占用系统的资源,而池化技术则可以优化资源的使用。
池化技术,实现准备好一些资源,有人要用,就直接拿,拿完还回来。
线程池的好处:降低资源消耗;提高响应速度;方便管理。线程复用、可以控制最大并发数、管理线程。
三大方法
ExecutorService threadPool = Executors.newSingleThreadExecutor();//线程池中只有单个线程。任务都是由这一个线程执行
Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小,任务有固定的这几个线程执行
Executors.newCachedThreadPool();//可伸缩的,遇强则强、遇弱则弱
- newSingleThreadExecutor()这个方法使得线程池中只有单个线程,任务都是由该线程去完成的。
- newFixedThreadPool(5);这种方式创建的线程池中线程数量是固定的。
- newCachedThreadPool();这种方式创建的线程池中线程数量不固定。
【注】:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式创建,这样更能明确线程池的运行规则以及规避资源耗尽的风险。
Executors.newSingleThreadExecutor();Executors.newFixedThreadPool(5);这两种方式创建的线程允许的最大长度是Integer.MAX_VALUE,大约21亿个,可能会堆积大量的请求,从而导致OOM。newCachedThreadPool()允许创建的线程数Integer.MAX_VALUE,也不要用。
线程池基本使用方式【以Exector举例】
创建线程池,使用线程池来创建线程,最后用完线程要关闭线程池。
public static void main(String[] args) {
//三大方法
ExecutorService threadPool = Executors.newSingleThreadExecutor();//线程池中只有单个线程。任
try {
for (int i = 0; i < 10; i++) {
//使用线程池之后要使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束要关闭线程池。
threadPool.shutdown();
}
}
七大参数
上面的三种方式本质是ThreadPoolExecutor()。下面是ThreadPoolExecutor的源码
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle // 拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
七大参数:
int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle // 拒绝策略
线程池核心大小指一般任务只要用核心数量的线程,不会全部线程都用。当队列满了,就会唤醒其他线程。拒绝策略就是队列满了,再来新的就会采用拒绝策略。七大参数用银行例子来讲解就是:银行有五个窗口办事(最大线程数),一般情况下只有两个窗口开着应对办事(核心线程池数),有需要到银行办事的来了,就先去窗口办事,开的两个窗口人满了就需要再候客区等待(阻塞队列),候客区假设有三个座位。今天来银行办事的人有点多,候客区也满了,然后银行那边就打电话让剩下三个窗口的工作人员回来工作,然后五个窗口都开了(阻塞唤醒)。那如果这个时候又有人来了,那他只能站着等了,或者离开(拒绝策略)。(保持时间)和(超时单位)都好理解,线程工厂一般不需要管。看下面的程序,相信很好理解。
public class Demo01 {
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和
最早的竞争,也不会抛出异常!
try {
// 最大承载:Deque + max
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
四种拒绝策略
利用上述银行例子来解释四种策略
new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
线程池的最大的大小如何去设置?
两种方式:CPU密集型;IO密集型
CPU核数获取:Runtime.getRuntime().availableProcessors();
CPU密集型:指的指CPU是几核,最大线程就设置几,可以保持CPU效率最高。
IO密集型:判断程序中十分耗IO的线程数量,最大线程设置为其数量的二倍。