并发编程之线程池原理分析
第一节
- 并发队列(先进先出)
(1)ConcurrentLinkedQueue(实体类) 非阻塞式队列
(2)BlockingQueue(接口) 阻塞式队列
阻塞:会进行等待 BlockingQueue
非阻塞:不会进行等待
阻塞式队列和非阻塞式队列的区别:
- 添加,出列的时候,如果超出了队列的总数,这时候阻塞式队列会进行等待,非阻塞式队列不会等待,报错或者报相关的提示信息,同理出队列和入队列一致
注意:
消费者在消息队列的时候,消费成功后,应将消费成功的消息清空掉。
非阻塞式的队列效率是高于阻塞式的队列。
第二节
- 阻塞式队列最大的好处就是能够防止队列容器溢出,防止数据丢失。
- ConcurrentLinkedQueue是并发包下面的,即java.util.concurrent下面的,该包下面的都是现成安全的。
- offer表示向队列中放值,poll表示从队列中取值(默认是先进先出,只能取一个),且用完就删,但是peek不会删除,其作用也是消费队列。
- poll方法:消费者消费成功后,自动将元素从队列中删除。
- peek方法:消费者消费成功后,不会将元素从队列中删除。(不推荐)
- concurrentLinkedQueue是一个无界队列。
- 如果队列中已经没有数据了,再往出取值为null
重要
总结下BlockingQueue:
- 插入方法:
add(E e) : 添加成功返回true,失败抛IllegalStateException异常
offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
- 删除方法:
remove(Object o) :移除指定元素,成功返回true,失败返回false
poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
take():获取并移除此队列头元素,若没有元素则一直阻塞。
- 检查方法
element() :获取但不移除此队列的头元素,没有元素则抛异常
peek() :获取但不移除此队列的头;若队列为空,则返回 null。
总结下ConcurrentLinkedQueue(无界)
- 阻塞式队列和非阻塞式队列的用法
非阻塞式队列:
package com.xiyou.mayi.thread5.bingfaQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* 阻塞式队列和非阻塞式队列的用法
*/
public class Test001 {
public static void main(String[] args) {
/**
* 非阻塞式队列
*/
// 生成非阻塞式队列,无界队列
ConcurrentLinkedQueue<String> concurrentLinkedQueue = new ConcurrentLinkedQueue<>();
// offer表示添加队列
concurrentLinkedQueue.offer("张三");
concurrentLinkedQueue.offer("李四");
concurrentLinkedQueue.offer("王五");
// 获取队列,只能获取一个队列元素,先进先出
System.out.println(concurrentLinkedQueue.poll());
// 获取队列的个数, 上面从队列中取出一个值后,自动将其从队列中删除
System.out.println(concurrentLinkedQueue.size());
// peek方法不会将消费的数据从队列中删除
System.out.println(concurrentLinkedQueue.peek());
System.out.println(concurrentLinkedQueue.size());
// 队列为空,则取出的值为null
System.out.println(concurrentLinkedQueue.poll());
System.out.println(concurrentLinkedQueue.poll());
System.out.println(concurrentLinkedQueue.poll());
}
}
输出结果:
张三
2
李四
2
李四
王五
null
阻塞式队列:
- ArrayBlockingQueue:是一个有边界的阻塞式队列。内部实现是一个数组。有边界的意思是他的容量是有限的,我们必须在初始化的时候就指定他的容量。一旦确定就无法改变。
- LinkedBlockingQueue:阻塞队列大小的配置是可选的,如果我们初始化时指定了一个大小,他就是有边界的,如果不指定,他就是无边界的。说是无边界其实采用了默认的大小Integer.MAX_VALUE的容量,数量不足就扩容。他的内部实现是一个列表。
- PriorityBlockingQueue:是一个没有边界的队列,他的排序规则和PriorityQueue一样。注意,PriorityBlockingQueue中是允许插入null对象的。所有插入该队列中的对象必须实现Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的
- SynchronousQueue:该队列内部只允许容纳一个元素,当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费掉。
/**
* 阻塞式队列的用法
*/
public class Test002 {
public static void main(String[] args) throws InterruptedException {
// 设定长度,表示是一个有界的队列。最大长度是3
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
// 添加非阻塞式队列的值,直接超过最大长度
arrayBlockingQueue.offer("张三");
arrayBlockingQueue.offer("李四");
arrayBlockingQueue.offer("王麻子");
// 队列长度是3,队列满了就不放数据,返回值是false,表示存入队列失败
arrayBlockingQueue.offer("小五");
// 如果队列满了等待3s放数据,如果队列满了就不放退出
// 设置队列满的时候的等待时长
arrayBlockingQueue.offer("阻塞式队列", 3, TimeUnit.SECONDS);
for (String a : arrayBlockingQueue) {
System.out.println(a);
}
}
}
结果:
(会等待3s,因为队列长度满了,等待3s队列还是满的,所以阻塞式队列的赋值由于设置了等待时长,所以先会等待,等待之后还是满的,所以存不进去)
张三
李四
王麻子
上面调用的是offer方法不会进行阻塞,只会返回true,false表示是否成功放入。(具体的上面讲到了)
第三节
- 使用BlockingQueue来模拟生产者和消费者
- 生产者线程负责添加队列,消费者线程负责消费队列
- 生产者消费者都是用的同一个blockingQueue
- volatile可以禁止重排序,保持线程的可见性
- AutomicInteger是一个原子类,一般用该类进行i++操作,保证了线程的安全。
(1)automicInteger.incrementAndGet(); // i++ - LinkedBlockingQueue:是一个伪无界队列,如果不传入长度,则表示无界,实际上他也是有长度的,长度是Integer的最大值,如果长度超过该最大值,就会对其进行扩容、
- 启动的时候应该先启动生产者,再启动消费者。
concurrentLinkedQueue是Queue的一个安全实现,Queue实现的是先入先出,采用CAS操作 ,来保证元素的一致性;而BlockingQueue往前推用的就是synchronized关键字。
代码用offer和poll实现,即用非阻塞的方式实现。:
package com.xiyou.mayiThread.myexecutors.mayiThread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 主类
*/
public class ProfucerAndCusByQueue {
public static void main(String[] args) {
// 若不规定长度就是无界的,规定了就是有界的
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<String>(3);
Producer producer = new Producer(blockingQueue);
Consumer consumer = new Consumer(blockingQueue);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
// 10s后停止
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 改变volatile的标志位,不再生产数据
producer.stop();
}
}
/**
* 生产者
**/
class Producer implements Runnable{
// 声明队列,构造函数传入,生产者和消费者都是一个队列
private BlockingQueue<String> blockingQueue;
// 原子类,保证线程安全
private AtomicInteger atomicInteger = new AtomicInteger();
private volatile boolean FLAG = true;
/**
* 构造函数用来赋值队列
* @param blockingQueue
*/
public Producer(BlockingQueue<String> blockingQueue){
this.blockingQueue = blockingQueue;
}
public void run() {
System.out.println("生产者开始启动.....: " + Thread.currentThread().getName());
while (FLAG) {
// 待发送数据
String data = atomicInteger.incrementAndGet() + "";
try {
// 向队列中存数据
// 若向队列中添加成功,返回true否则返回false
boolean offer = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
if (offer) {
System.out.println(Thread.currentThread().getName() + " , 生产队列" + data + " 成功....");
} else {
System.out.println(Thread.currentThread().getName() + " , 生产队列" + data + " 失败....");
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者线程: " + Thread.currentThread().getName() + " 停止运行....");
}
public void stop(){
FLAG = false;
}
}
/**
* 消费者
*/
class Consumer implements Runnable{
private volatile boolean FLAG = true;
// 声明一个队列用来从里面消费数据,和生产者应该是同一个数据
private BlockingQueue<String> blockingQueue;
/**
* 构造函数
*/
public Consumer(BlockingQueue<String> blockingQueue){
this.blockingQueue = blockingQueue;
}
public void run() {
System.out.println(Thread.currentThread().getName() + "消费者已经开始启动...");
while (FLAG) {
try {
// poll取数据,若队列为空,取出数据为null
// 2s内取出数据,无法取出返回null
String data = blockingQueue.poll(2,TimeUnit.SECONDS);
if (data == null) {
// 设置标志位,不再继续取数据
FLAG = false;
System.out.println("消费者超过2s未取出数据.....");
return;
}
System.out.println("消费者获取到队列的信息成功, data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者: " + Thread.currentThread().getName() + " 已经结束....");
}
}
- 运行结果:
生产者开始启动.....: Thread-0
Thread-1消费者已经开始启动...
Thread-0 , 生产队列1 成功....
消费者获取到队列的信息成功, data: 1
Thread-0 , 生产队列2 成功....
消费者获取到队列的信息成功, data: 2
Thread-0 , 生产队列3 成功....
消费者获取到队列的信息成功, data: 3
Thread-0 , 生产队列4 成功....
消费者获取到队列的信息成功, data: 4
Thread-0 , 生产队列5 成功....
消费者获取到队列的信息成功, data: 5
Thread-0 , 生产队列6 成功....
消费者获取到队列的信息成功, data: 6
Thread-0 , 生产队列7 成功....
消费者获取到队列的信息成功, data: 7
Thread-0 , 生产队列8 成功....
消费者获取到队列的信息成功, data: 8
Thread-0 , 生产队列9 成功....
消费者获取到队列的信息成功, data: 9
Thread-0 , 生产队列10 成功....
消费者获取到队列的信息成功, data: 10
生产者线程: Thread-0 停止运行....
消费者超过2s未取出数据.....
Process finished with exit code 0
第四节 线程池的原理分析
- 线程池的目的:
(1)降低资源消耗,通过重复利用已经创建的线程,降低线程的创建和销毁造成的消耗。(创建线程比较费时间)
(2)提高程序的效率,当任务达到的时候,任务可以不需要等待线程的创建就直接执行,因为线程池中的每一个线程并没有停止掉,无需重复进行创建。
(3)方便管理,无限创建线程,不会出问题,底层有一个缓存队列,创建超出线程池容纳的最大数量的时候,会将其放到阻塞队列中,直至线程池有线程销毁,其可以继续创建 - 线程池溢出:频繁的创建多线程,非常占用CPU内存,CPU来不及进行线程的切换,会引发线程池溢出的异常,创建的线程超过了可容纳的总数。
- 如何配置合理的线程数
第五节 线程池的分类
- 四种创建方式:Java通过Executors类(jdk1.5并发包)提供四种线程池
(1)newCachedThreadPool:创建一个可缓存线程池,如果线程池的长度超过处理需求,可灵活回收空闲线程,若无可回收,则创建新的线程(不会规定初始的线程池的长度,自己创建,可以复用就复用,不能复用就新建。)其构造方法中有默认创建的最大线程数,最大线程数其实是无限大的。
(2)newFixedThreadPool:创建一个定长线程池,可以控制线程最大的并发数,超出的线程会在阻塞队列中等待。该线程池规定了最大的线程数,只创建这么多的线程,其他的会等其运行完对其进行复用。底层使用BlockingQueue,超过了最大线程数会放到阻塞队列中,等有空余线程的时候,从队列中取任务复用线程执行。
(3)newScheduleThreadPool:创建一个定长线程池,支持定时以及周期任务执行。定长的线程池,所以会传入具体的最大线程数。调用的时候调用.schedule方法,不用execute或者submit,因为只有schedule方法才可以定时执行。
(4)newSingleThreadExecutor:创建一个单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。 - Executors是一个具体的类。Executor是一个接口。
- 线程池创建的方式:
Executors.newCachedThreadPool/newFixedThreadPool/newScheduleThreadPool/newSingleThreadExecutor()
创建四种线程池
举例说明:
- execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。——实现Runnable接口
- submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。——实现Callable接口
- 没有停止线程池,程序就会一直运行,调用.shutdown()就可以关闭线程池。
第六节 线程池的原理
- 任何一个线程池的核心都是走的是ThreadPoolExecutor
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters and default thread factory and rejected execution handler.
* It may be more convenient to use one of the {@link Executors} factory
* methods instead of this general purpose constructor.
*
* @param corePoolSize 核心线程数的数量
* @param maximumPoolSize 最大线程的数量
* @param 线程空闲超时的时间, (视频主角建议配置30s)
* @param 超时时间的单位,分,秒等
* @param 缓存队列,用于存放核心线程所执行的任务,核心线程执行该队列中的内容
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize(核心线程数)和maximumPoolSize(最大线程数)的区别是:(重要)
核心线程数表示的是可以实际进行应用的线程数,表示当前线程池实际上会有多少个线程数进行使用。(最大可以运行的线程数)
最大线程数,表示线程池最大创建多少个线程。(最大可以创建的线程)
- 过程:
(1)用户提交线程给线程池
(2)如果当前线程池中的线程数目<=核心线程数,则每来一个任务,就会创建一个线程去执行这个任务;
(3)如果当前线程池中的线程数目>=核心线程数,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲的线程将其取出去执行;若添加失败(一般情况下是任务缓存队列已经存满了),则会尝试创建新的线程去执行任务。(放到缓存队列中的线程,都是由核心线程去执行,不会重新拉起新的线程)
(4)如果队列已经满了,则在总线程数<=最大线程数的前提下,则创建新的线程,单独执行(队列已经满了表示当前线程执行的任务数够了,此时如果线程总数小于规定的最大的线程,那么我们就会新建一个线程执行下面的任务);如果当前线程池中的线程数目达到了maximumPoolSize(最大线程数)的时候 ,则会采取任务拒绝策略进行处理(就是说线程数大于最大线程数且阻塞缓存队列已经满了的时候,就无法新建了,直接报错);
(5)如果线程池中的线程数量大于corePoolSize(核心线程数),如果某线程空闲时间超过了keepAliveTime,线程将被终止,直到线程池中的线程数目不大于corePoolSize(核心线程数);如果允许线程池中的核心线程设置存活时间,那么核心池中的线程空闲时间超过了keepAliveTime的时候,线程也会被终止、
第七节
自定义线程池:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 自定义线程池
*/
public class ZiDingYiThreadPool {
public static void main(String[] args) {
// 所有的线程池其实就是调用了这个构造函数
// 核心线程数,最大线程数,过期时长,时间单位,若超过核心线程将其保存到的阻塞队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,
2,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3));
for (int i = 0; i < 5; i++) {
TaskThread taskThread = new TaskThread("任务:" + i);
// 向线程池中提交线程
executor.execute(taskThread);
}
// 关闭线程池
executor.shutdown();
}
}
class TaskThread implements Runnable{
private String taskName;
public TaskThread(String taskName) {
this.taskName = taskName;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " : " + taskName);
}
}
- 运行结果:
pool-1-thread-1 : 任务:0
pool-1-thread-1 : 任务:1
pool-1-thread-1 : 任务:2
pool-1-thread-2 : 任务:4
pool-1-thread-1 : 任务:3
可以看到我们规定了其核心线程数是1,最大线程数是2,阻塞队列的长度是3,我们同时给线程池提交了5个任务,此时任务0进入被核心线程执行,任务1进入,此时核心线程已经使用,只能放到阻塞队列中,任务2,任务3同理,都是等待核心线程将其运行,此时任务4进入,此时核心线程数有用,队列已经满了,但是总线程只有一个是核心线程,但我们规定的是最大线程数是2,因此,我们会再新建一个线程去执行任务4.