Callable接口
1、为什么使用Callable接口
Thread和Runnable
都有的缺点:启动子线程的线程 不能获取子线程的执行结果,也不能捕获子线程的异常
从java5开始,提供了Callable接口,是Runable接口的增强版。用Call()方法作为线程的执行体,增强了之前的run()方法。因为call方法可以有返回值,也可以声明抛出异常。
1.Runnable方式创建:在主线程里捕获子线程的异常? 不可以。
try { //Runnable方式:主线程无法捕获子线程执行的异常 new Thread(()->{ int i = 1/0; String result = "jieguo"; }).start(); } catch (Exception e) { System.out.println("主线程获取到了子线程异常:"+ e.getMessage()); }
根据控制台打印,在main线程里并没有捕获到子线程出现的异常
2、使用Callable创建线程并运行
Callable接口中注意点: 可以使用泛型<V>指定返回值类型,并且call方法可以抛出异常
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
使用Callable创建的子线程需要借助FutureTask对象来执行它的call方法
无论哪种方式创建多线程 都必须借助Thread对象的start方法启动线程,Thread只能接受Runnable对象: thread.run()-> runnable.run()
public static void main(String[] args) { Callable<String> callable = ()->{ System.out.println("callable的call方法执行了....."); return "hehe...."; }; //juc包中提供了FutureTask 间接实现了Runnable接口,并实现了run方法 new Thread(new FutureTask<String>(callable)).start(); }
1.Thread 调用了start方法后,系统CPU调度执行线程的run方法,run方法中判断 传入的runnable对象如果不为空则调用它的run方法。
2.我们传入的runnable对象是FutureTask的对象,所以调用的是FutureTask的run方法
3.FutureTask的run方法执行时,调用了我们传入的Callable对象的call方法执行 并接受返回的结果总的来说:就是移花接木,FutureTask间接实现了Runnable接口 并实现了run方法:run方法中调用了Callable的call方法
java.util.concurrent.FutureTask类
1.juc包中提供了FutureTask 间接实现了Runnable接口,并实现了run方法
public class FutureTask<V> implements RunnableFuture<V> public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
2.FutureTask的run方法执行时的结果 和异常 会通过FutureTask的成员属性接收,并通过一个布尔类型的标记记录执行是否有异常
//结果和异常使用同一个变量接收 private Object outcome;
3.FutureTask中会在run方法执行结束时 将线程执行的状态从0(线程还未执行完毕)改为1(线程执行结束)
private volatile int state; private static final int NEW = 0; private static final int COMPLETING = 1;
3、FutureTask的get()方法
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
callable可以返回方法的执行结果:通过 futureTask的get方法 阻塞获取返回结果
public static void main(String[] args) { //Callable:获取执行结果+捕获异常 Callable<String> callable = ()->{ System.out.println(Thread.currentThread().getName()+"执行了call方法.."); return "haha...."; }; //FutureTask它的泛型跟Callable的泛型要一样,因为都是代表该子线程的执行结果类型 FutureTask<String> futureTask = new FutureTask<String>(callable); new Thread(futureTask,"AA").start(); try { //获取子线程执行的结果 调用get方法会阻塞主线程,所以一定要将获取子线程结果的操作写在方法的最后 String result = futureTask.get(); System.out.println("主线程获取到的子线程结果:"+futureTask.get()); } catch (Exception e) {//捕获子线程执行的异常 System.out.println(Thread.currentThread().getName()+" 获取到子线程的异常:"+e.getMessage()); } }
在使用futureTask的get方法时,要去捕获异常,可以获取子线程的异常
public static void main(String[] args) { //Callable:获取执行结果+捕获异常 Callable<String> callable = ()->{ int i = 1/0; System.out.println(Thread.currentThread().getName()+"执行了call方法.."); return "haha...."; }; //FutureTask它的泛型跟Callable的泛型要一样,因为都是代表该子线程的执行结果类型 FutureTask<String> futureTask = new FutureTask<String>(callable); new Thread(futureTask,"AA").start(); try { //获取子线程执行的结果 调用get方法会阻塞主线程,所以一定要将获取子线程结果的操作写在方法的最后 String result = futureTask.get(); System.out.println("主线程获取到的子线程结果:"+futureTask.get()); } catch (Exception e) {//捕获子线程执行的异常 System.out.println(Thread.currentThread().getName()+" 获取到子线程的异常:"+e.getMessage()); } }
控制台打印:
为什么说get方法会阻塞主线程呢?
//以下为FutureTask的源码
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
开发时:一定要把获取子线程结果的位置放在方法的最后
4、FutureTask的复用
只计算一次,FutureTask会复用之前计算过得结果
public static void main(String[] args) {
//FutureTask复用问题: 执行异步任务时 为了提高效率它会缓存执行结果
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName()+"...");
return "haha..";
});
new Thread(futureTask,"AA").start();
new Thread(futureTask,"BB").start();
}
执行异步任务时 为了提高效率它会缓存执行结果。。所以BB线程是从缓存拿的,并没有去走call方法
不想复用之前的计算结果。怎么办?再创建一个FutureTask对象即可
public static void main(String[] args) {
//FutureTask复用问题: 执行异步任务时 为了提高效率它会缓存执行结果
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName()+"...");
return "haha..";
});
FutureTask<String> futureTask2 = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName()+"...");
return "haha..";
});
new Thread(futureTask,"AA").start();
new Thread(futureTask2,"BB").start();
}
5、Callable接口与Runnable接口的区别
老师版:
1、callable有返回值 可以抛出异常
2、runnable可以直接通过Thread启动
3、callable需要通过FutureTask来接收,再由Thread启动
4、callable的任务方法时call(),runnable的是run()
笔记版:
相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程
不同点:
具体方法不同:一个是run,一个是call
Runnable没有返回值;Callable可以返回执行结果,是个泛型
Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛
它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
阻塞队列
1、队列 queue 和 栈 stack
queue: 先进先出,后进后出。怎么容易理解呢:左进右出
线程池使用、mq也使用了
---------------------
Stack: 特点 先进后出 后进先出
public class Stack<E> extends Vector<E>{// Stack就是一个集合类 是一个线程安全的集合类
//1、入栈方法
public E push(E item) {
addElement(item);
return item;
}
// 数组:连续的一块内存,按照添加的索引先后顺序有序
// Vector中的方法:添加元素到 elementData元素数组中
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;//将元素添加到elementData数组的最后一个位置
}
//2、出栈方法
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);//将获取到的最后一个位置元素删除
return obj;
}
// 获取数组最后一个索引的元素 返回
public synchronized E peek() {
int len = size();//获取元素个数
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);//数组长度-1 也就是数组最后一个位置的元素
}
}
扩展
方法栈:java代码执行时,每个线程jvm会为他创建一个栈来存储线程调用方法的执行过程数据
线程方法栈。每个方法都是一个栈帧
2、阻塞队列 BlockingQueue
线程池用来存不能及时处理的任务的数据结构
BlockingQueue:阻塞队列
在开发中我们不用关心向队列中添加元素的线程 如果队列满了 它如何阻塞等待
获取队列中元素的线程 如果队列空了 它如何阻塞等待 以及阻塞的线程如何被唤醒
BlockingQueue是一个接口,继承Queue接口,Queue接口继承 Collection接口
BlockingQueue接口主要有以下7个实现类:
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
LinkedTransferQueue:由链表组成的无界阻塞队列。
LinkedBlockingDeque:由链表组成的双向阻塞队列。
线程池使用阻塞队列
- ArrayBlockingQueue: 数组阻塞队列
- LinkedBlockingQueue:链表阻塞队列
- SynchronousQueue:同步阻塞队列(不存储元素)
ArrayBlockingQueue:创建时手动指定的长度就是该队列的最大的长度
LinkedBlockingQueue:
默认长度:Integer.MAX_VALUE 最多存储21亿左右的元素
阻塞队列:添加元素 获取元素 移除元素的方法有4套,根据方法是否返回结果 是否抛出异常 是否可以阻塞 是否可以阻塞超时划分
抛出异常 | 特殊值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
获取 | element() | peek() | 不可用 | 不可用 |
要是看着懵,是因为没继续看下去,看完再回来看就一目了然了
3、抛出异常的方法
add正常执行返回true,element(不删除)和remove正常执行会返回阻塞队列中的第一个元素
- 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
- 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
- 当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
add():添加元素失败抛出异常
public static void main(String[] args) { //数组阻塞队列初始化时需要传入 初始化数组的长度 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3); System.out.println("1:"+queue.add("a")); System.out.println("2:"+queue.add("b")); System.out.println("3:"+queue.add("c")); System.out.println("4:"+queue.add("d")); }
----------------------
remove():移除成功返回移除的数据 移除失败抛出异常
public static void main(String[] args) { //数组阻塞队列初始化时需要传入 初始化数组的长度 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3); System.out.println("1:"+queue.add("a")); System.out.println("2:"+queue.add("b")); System.out.println("3:"+queue.add("c")); //移除成功返回移除的数据 System.out.println(queue.remove()); System.out.println(queue.remove()); System.out.println(queue.remove()); //移除失败抛出异常 System.out.println(queue.remove()); }
--------------------------element():获取最先添加的元素 获取不到抛出异常
public static void main(String[] args) { //数组阻塞队列初始化时需要传入 初始化数组的长度 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3); //获取最先添加的元素,即左进右出情况下,获取最右边的元素,获取不到抛出异常 System.out.println(queue.element()); System.out.println("1:"+queue.add("a")); }
4、返回特殊值的方法
offer()/poll()/peek()
offer() 插入方法,成功ture失败false
public static void main(String[] args) { //数组阻塞队列初始化时需要传入 初始化数组的长度 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3); //添加成功ture,添加失败false System.out.println(queue.offer("a")); System.out.println(queue.offer("b")); System.out.println(queue.offer("c")); System.out.println(queue.offer("d")); }
-------------------------------
poll() 移除方法,成功返回出队列的元素,队列里没有就返回null
public static void main(String[] args) { //数组阻塞队列初始化时需要传入 初始化数组的长度 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3); //添加成功ture,添加失败false System.out.println(queue.offer("a")); System.out.println(queue.offer("b")); System.out.println(queue.offer("c")); //移除元素成功返回出队列的元素,移除失败返回null System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); }
-------------------------------
peek() 获取方法,成功返回队列中的元素,没有返回null
public static void main(String[] args) { //数组阻塞队列初始化时需要传入 初始化数组的长度 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3); //获取成功返回队列中的最右边的元素,没有返回null System.out.println(queue.peek()); //添加成功ture,添加失败false System.out.println(queue.offer("a")); System.out.println(queue.offer("b")); System.out.println(queue.offer("c")); //获取成功返回队列中的最右边的元素,没有返回null System.out.println(queue.peek()); }
5、阻塞等待方法
put() / take()
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出 当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程直到队列可用
public static void main(String[] args) throws InterruptedException {
//数组阻塞队列初始化时需要传入 初始化数组的长度
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
//put给队列中从添加元素,左进右出
queue.put("a");
queue.put("b");
queue.put("c");
System.out.println("take前,队列大小:"+queue.size());
//take取走队列中最右面的元素
System.out.println(queue.take());
System.out.println("take后,队列大小:"+queue.size());
}
6、超时等待方法
offer( timeout) /poll(timeout)
- offer( timeout):阻塞等待添加元素,成功返回true 超时失败返回false
- poll(timeout): 超时不能移除元素返回null
public static void main(String[] args) throws InterruptedException {
//数组阻塞队列初始化时需要传入 初始化数组的长度
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
//poll(timeout): 超时不能移除元素返回null
System.out.println(queue.poll(3, TimeUnit.SECONDS));
//offer( timeout):阻塞等待添加元素,成功返回true 超时失败返回false
System.out.println(queue.offer("aa", 3, TimeUnit.SECONDS));
System.out.println(queue.offer("cc", 3, TimeUnit.SECONDS));
System.out.println(queue.offer("bb", 3, TimeUnit.SECONDS));
System.out.println(queue.offer("dd", 3, TimeUnit.SECONDS));
}
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。
7、阻塞等待方法的源码
put() / take()
请欣赏我的画作,以我的理解应该是这么画的。。。
put:
添加元素时使用ReentrantLock加锁
如果队列的长度等于队列中存入元素的个数代表队列已满,当前线程使用notFull.await()进入阻塞等待状态//以下为ArrayBlockingQueue的源码 public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } }
如果队列的长度不等于队列中存入元素的个数代表队列未满,当前线程将元素添加到队列数组中,notEmpty.signal()唤醒等待消费队列中元素的线程
//以下为ArrayBlockingQueue的源码 private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); }
take:
移除元素时使用Lock加锁
如果队列中元素个数为0,代表队列是空的,当前线程使用notEmpty.await()让自己等待//以下为ArrayBlockingQueue的源码 public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }
如果队列中元素个数>0,队列中有元素,当前线程获取元素返回 并调用notFull.signal()唤醒向队列添加元素阻塞的线程
//以下为ArrayBlockingQueue的源码 private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; final Object[] items = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); notFull.signal(); return x; }
ThreadPool线程池
线程池作用
维护复用线程,控制线程的数量,线程对系统比较珍贵 ,一个CPU如果执行的线程过多,会频繁的切换每个线程的上下文,线程过多 性能会下降,所以在多核的系统中会使用多线程 但是线程的数量也不会无限创建
线程池特点
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
1、线程池架构
Executor接口:线程池顶级接口
//线程池顶级父接口
public interface Executor {
//只有一个execute方法,执行Runnable任务
void execute(Runnable command);
}
ExecutorService接口:继承了Executor并对它进行了扩展
public interface ExecutorService extends Executor {
void shutdown();//关闭线程池
<T> Future<T> submit(Callable<T> task);//执行Callable任务并返回任务结果
Future<?> submit(Runnable task);//重载方法,执行Runnable任务
}
找实现类,执行延时任务
ThreadPoolExecutor:实现了ExecutorService,是以后最常用的线程池类
ScheduledThreadPoolExecutor:继承了ThreadPoolExecutor,可以执行延迟任务(可以实现简单的定时任务)
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
}
2、工具类Executors
为了方便创建线程池对象,juc包中提供了工具类Executors可以快速创建线程池对象:
Executors: 内部提供了多种初始化线程池的方法
线程池对象的使用
1、执行任务
void execute(Runnable r);//执行任务 没有返回结果
Future<T> submit(Callable<T> c);//执行任务并返回任务结果,通过返回的future对象调用get方法获取任务结果
2、关闭线程池
shutdown();
3、4种常见线程池
public static ExecutorService newCachedThreadPool():初始化执行短期任务的线程池
当需要执⾏很多短时间的任务时, CacheThreadPool 的线程复⽤率⽐较⾼, 会显著的提⾼性能。
public static void main(String[] args) throws InterruptedException { //执行短期任务的线程池 可以开的线程较多 //如果没有空闲的线程 它会新创建一个线程处理请求 ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 1; i <= 200; i++) { int a = i; pool.execute(()->{ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行任务... i = " + a); }); } }
控制台效果:直接干到200个线程。。。来处理任务
newCachedThreadPool()
1、如果有新的任务,会立即创建新线程处理,因为线程池使用的是不存储元素的阻塞队列SynchronousQueue
2、最多可以创建Integer.MAX_VALUE多个线程 ,过多线程也会导致以后出现oom,因为每个线程都需要自己的栈空间
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
public static ExecutorService newFixedThreadPool(int nThreads):初始化固定线程数量线程池
public static void main(String[] args) throws InterruptedException { //只有固定数量的线程,不会再新创建,有任务不能及时处理则存到阻塞队列中,线程池使用的是LinkedBlockingQueue ExecutorService pool = Executors.newFixedThreadPool(3); for (int i = 1; i <= 100; i++) { int a = i; pool.execute(()->{ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" i = "+ a); }); } }
控制台效果:不管多少个任务,都是指定个数的(这里是3个)线程来处理任务
newFixedThreadPool()
1、只有固定数量的线程,不会再新创建,有任务不能及时处理则存到阻塞队列中,线程池使用的是LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
2、最多可以缓存Integer最大值个任务, 也可能出现oom
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
public static ExecutorService newSingleThreadExecutor():创建单个线程的线程池
可以用来执行固定耗时任务,,,说实话我觉得这个玩意设计出来有啥用
public static void main(String[] args) throws InterruptedException { //和FixedThreadPool一样,但是线程数量固定为1个,任务队列长度为Integer的最大值,任务过多可能出现OOM ExecutorService pool = Executors.newSingleThreadExecutor(); for (int i = 1; i <= 100; i++) { int a = i; pool.execute(()->{ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" i = "+ a); }); } }
控制台效果:不管多少个任务,都是1个线程来处理任务
newSingleThreadExecutor()
1、任务队列长度为Integer的最大值,任务过多可能出现OOM
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建执行延迟任务的固定线程数量的线程池
这里的执行任务不是execute,也不是submit。是否考虑上一个任务结束,分为俩种。
public static void main(String[] args) throws InterruptedException { //系统线程池之ScheduledThreadPool:延迟任务线程池 一般用来处理简单的定时操作 ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); //3秒后执行第一次传入的任务,以后每过5秒执行一次传入的任务 pool.scheduleWithFixedDelay(()->{ System.out.println(Thread.currentThread().getName()+"...."+new Date()); },3,5, TimeUnit.SECONDS); System.out.println(new Date()); }
控制台效果:先打印当前日期时间,过了3秒后打印第一行,接着每隔5秒打印一行
newScheduledThreadPool()
1、和CachedThreadPool类似,通过一个空的延迟阻塞队列缓存任务,有新的任务时会分配一个线程来处理
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService { //静态内部类 static class DelayedWorkQueue extends AbstractQueue<Runnable> implements BlockingQueue<Runnable> { } }
2、任务过多会创建多个线程处理请求,线程数量最大为Integer的最大值,也会导致OOM
总结
线程池不允许使用 Executors 去创建,而是通过自定义线程池方式,这样可以更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
开发中如果使用线程池 强制使用ThreadPoolExecutor来创建
阿里开发手册
4、自定义线程池
线程池的7大参数
int corePoolSize, 核心线程数
线程池运行稳定后最终收缩到的 线程数
int maximumPoolSize, 最大线程数
线程池运行时,核心线程可能不够用,为了保证线程池可以及时处理并发访问的大量请求,可以设置最大线程数
最大线程数-核心线程数 为 线程池最多可以临时创建的处理高并发请求的线程数
long keepAliveTime, 存活时间
多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到 只剩下corePoolSize个线程为止
TimeUnit unit, 存活时间单位
BlockingQueue<Runnable> workQueue, 任务队列
核心线程不能及时处理的任务 优先存到阻塞任务队列中缓存
核心线程空闲后 会自动取出队列中的任务执行(这话不对改为 :任务队列的任务由空闲的线程处理)
ThreadFactory threadFactory, 线程工厂
用来初始化线程对象,一般使用Executors提供的defaultThreadFactory
RejectedExecutionHandler handler 拒绝策略(任务不能处理时的拒绝处理器)
当任务没有新的线程可以分配,同时任务队列已满,此时线程池会使用拒绝策略来拒绝请求
自定义线程池:
线程池是维护管理一组线程的。for循环主线程执行的,只是为了给线程池添加任务
public static void main(String[] args) { //核心线程数为3,最大可以创建10个线程,任务队列长度为5,线程空闲存活时间为1秒的线程池 ThreadPoolExecutor pool = new ThreadPoolExecutor(3,10, 1000,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for (int i = 1; i <= 15; i++) { int a = i; pool.execute(()->{ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行了任务:i = "+a); }); } }
控制台运行效果:
我还以为我懂了呢,现实给我一大嘴巴子。。。我还他妈傻乎乎的以为阻塞队列中的任务都是由核心线程来处理呢!
1.任务队列的任务由空闲的线程处理
2.还有核心线程数量固定,但是不一定哪个线程才能活到最后,最终剩余的存活的线程数量是配置的核心线程数
5、线程池的底层工作原理
以线程池为核心来看,任务是外来的。也就是说在任务达到时,不同的状况的线程池对任务的处理方式是不一样的。
线程池初始化时,线程池中的线程数量为0
重要的事情说三遍:以下重要:以下重要:以下重要:
在创建了线程池后,线程池中的线程数为零(懒加载)。等到有任务过来的时候才会创建线程。也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
6、拒绝策略
据绝策略:当来了新的任务,且线程池没有空闲的线程处理,任务队列已满时。使用拒绝策略拒绝不能被及时处理的任务
AbortPolicy: 默认拒绝策略
不能处理的任务抛出异常,这种方式好在:捕捉到异常便于动态调整线程池的参数
public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() //AbortPolicy: 默认拒绝策略,不能处理的任务抛出异常 ); //选8:是因为最大线程数+任务队列=5+2=7 for (int i = 1; i <= 8; i++) { int a = i; pool.execute(()->{ try { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" 执行了任务:"+a); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
控制台运行会抛出异常,符合预期效果
CallerRunsPolicy: 调用者执行的拒绝策略
线程池不能处理的任务,将任务回退到调用线程
此时控制台打印,符合预期。由main线程处理第8个任务,其实是手动调用run方法
DiscardPolicy: 丢弃任务的拒绝策略
线程池将不能处理的任务直接丢弃掉,不会抛出异常此时控制台打印如下,符合预期,因为第8个任务被丢弃了。。丢弃任务应用场景:评论或者点赞啥的
DiscardOldestPolicy: 丢弃等待时间最长的任务的拒绝策略
线程池将任务队列中等待时间最长的任务丢弃此时控制打印如下,符合预期。因为队列中的元素是4,5,6.任务4是最早放入队列即队列中等待时间最长的任务,直接丢弃
自定义拒绝策略:有些任务不能处理时 我们也不希望抛出异常 也不希望丢弃消息 可以自定义策略
RejectedExecutionHandler:可以通过Lambda表达式创建对象
public interface RejectedExecutionHandler { //该接口只有这么一个方法 void rejectedExecution(Runnable r, ThreadPoolExecutor executor); }
参数1:表示线程池不能执行的任务
参数2:表示不能处理任务的线程池
自定义拒绝策略
这里是使用list集合,把不能处理的任务存起来,再通过额外的线程去处理。
也可以使用run让主线程处理这个任务
public static void main(String[] args) { //创建一个线程安全的list集合 List<Runnable> list = Collections.synchronizedList(new ArrayList<>()); ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(), (r,executor)->{//参数1:表示线程池不能执行的任务, 参数2:表示不能处理任务的线程池 System.out.println("executor: "+executor); list.add(r);//把不能处理的任务存起来,再通过额外的线程去处理 //r.run();//手动在主线程中调用runnable任务的run方法 }); System.out.println("pool: "+pool); //选8:是因为最大线程数+任务队列=5+2=7 for (int i = 1; i <= 8; i++) { int a = i; pool.execute(()->{ try { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" 执行了任务:"+a); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
控制台打印如下:
7、线程复⽤原理
8、线程池的线程数如何设置
面试题:有没有使用过线程池,自定义时如何配置线程数?
开发中我们可以把任务分为计算(CPU)密集型和IO密集型。
CPU计算密集型任务:因为每个任务都需要cpu持续计算操作,如果一个cpu执行了多个这样的线程任务频繁的在多个线程中切换,花在任务切换的时间就越多,CPU执行任务的效率就越低 ,所以线程池中线程数应该设置为 cpu核心数+1
IO密集型任务: 每个任务进行IO操作时,CPU计算可能连1%的时长都用不到,99%的时间都花在IO上。此时一个CPU执行多个任务效率最高。阻塞系数一般设置为(0.8~0.9) 。
所以线程池中线程数设置为:cpu核数/(1-阻塞系数)
生产环境核心线程数设置: IO密集型:10*cpu核数
队列一般使用有界队列
ArrayBlockingQueue(10) 内存连续 查询快 内存利用率不高
LinkedBlockingQueue(10) 可以利用内存碎片提高内存使用率 查询慢