1.JUC概述
1.1什么是Juc
JUC就是java.util .concurrent工具包的简称 Java1.5 开始出现
1.2 线程和进程的概念
1.2.1进程和线程
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。
1.2.2线程的状态
NEW,(新建) RUNNABLE,(准备就绪) BLOCKED,(阻塞)
WAITING,(不见不散) TIMED_WAITING,(过时不候)
TERMINATED;(终结)
1.2.3wait和sleep
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
- sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
- 它们都可以被interrupted方法中断。
1.2.4并发和并行
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
并行:多项工作一起执行,之后再汇总
1.2.5管程
管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行
JVM中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着java对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
1.2.6用户线程和守护线程
用户线程:平时用到的普通线程,自定义线程
守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
当主线程结束后,用户线程还在运行,JVM存活
如果没有用户线程,都是守护线程,JVM结束
2.Lock接口
2.1 Synchronized
2.1.1 Synchronized 作用范围
-
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
-
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。 复制代码
-
修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
-
修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
2.1.2 多线程编程步骤
第一、创建资源类,创建属性和操作方法
第二、创建多线程调用资源类的方法
2.2 什么是Lock接口
Lock锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock提供了比synchronized更多的功能。
Lock和synchronized有以下几点不同:
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
2.3 创建线程的多种方式
- 继承Thread类
- 实现Runnable接口
- 使用Callable接口
- 使用线程池
2.4 使用Lock实现卖票例子
private final ReentrantLock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " : 卖出: " + (num --) + "剩下: " + num);
}
} finally {
lock.unlock();
}
}
复制代码
3.线程间通信
sychronized
public synchronized void incr() throws InterruptedException {
while (number != 0) {
this.wait();
}
number ++;
System.out.println(Thread.currentThread().getName() + ": : " + number);
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
while (number != 1) {
this.wait();
}
number --;
System.out.println(Thread.currentThread().getName() + ": : " + number);
this.notifyAll();
}
复制代码
Lock()
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void incr() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number ++;
System.out.println(Thread.currentThread().getName() + ": : " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try {
while (number != 1) {
condition.await();
}
number --;
System.out.println(Thread.currentThread().getName() + ": : " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
复制代码
4.线程间的定制化通信
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 1) {
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i + "轮数: " + loop);
}
flag = 2;
c2.signal();
}finally {
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 2) {
c2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i + "轮数: " + loop);
}
flag = 3;
c3.signal();
}finally {
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 3) {
c3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i + "轮数: " + loop);
}
flag = 1;
c1.signal();
}finally {
lock.unlock();
}
}
复制代码
5.集合间的线程安全
5.1 ArrayList线程不安全
演示
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
复制代码
解决方案一 Vector
List<String> list = new Vector<>();
复制代码
解决方案二 Collections
List<String> list = Collections.synchronizedList(new ArrayList<>());
复制代码
解决方案三 CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
复制代码
5.2 Hashset线程不安全
解决方案
Set set = new CopyOnWriteArraySet();
复制代码
5.3 HashMap线程不安全
解决方案
Map map = new ConcurrentHashMap();
复制代码
6.多线程锁
6.1 演示锁的八种情况
class Phone {
public static synchronized void sendSMS() throws Exception {
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
复制代码
1 标准访问,先打印短信还是邮件
——sendSMS
——sendEmail
2 停4秒在短信方法内,先打印短信还是邮件
——sendSMS
——sendEmail
3 新增普通的hello方法,是先打短信还是hello
——getHello
——sendSMS
4 现在有两部手机,先打印短信还是邮件
——sendEmail
——sendSMS
5 两个静态同步方法,1部手机,先打印短信还是邮件
——sendSMS
——sendEmail
6 两个静态同步方法,2部手机,先打印短信还是邮件 –
—–sendSMS
——sendEmail
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
——sendEmail
——sendSMS
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
——sendEmail
——sendSMS
结论
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象
6.2 公平锁和非公平锁
非公平锁:线程饿死 效率高
公平锁:阳光普照,效率相对高
6.3 可重入锁
sychronized演示
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(Thread.currentThread().getName() + " 外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + " 中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + " 内层");
}
}
}
}, "t1").start();
复制代码
lock演示
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 外层");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 中层");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 内层");
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
}, "t1").start();
复制代码
6.4 死锁
1、什么是死锁
两个或两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去
2、产生死锁原因
- 系统资源不足
- 进程运行推进顺序不合适
- 资源分配不当
3、死锁的演示
4、验证是否是死锁
- jps 类似linux中 ps -ef
- jstack jvm自带堆栈跟踪工具
7.Callable接口&Future接口
7.1、Callable接口
- 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法
- call方法可以引发异常,run()不能
- 为实现Callable而必须重写call方法
- 不能直接替换runnable,因为Thread类的构造方法根本没有Callable
7.2、Future接口
当call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用Future对象。。要实现此接口,必须重写5种方法,这里列出了重要的方法,如下:
-
public boolean cancel(boolean mayInterrupt)
用于停止任务。如果尚未启动,它将停止任务。如果已启动,则仅在mayInterrupt为true时才会中断任务 复制代码
-
public Object get()throws InterruptedException,ExecutionException
用于获取任务的结果。如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果 复制代码
-
public boolean isDone()
如果任务完成,则返回true,否则返回false 复制代码
7.3、FutureTask
Java库具有具体的FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。 可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。
核心原理
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成
- 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态
- 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
- 一旦计算完成,就不能再重新开始或取消计算
- get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
- get只计算一次,因此get方法放到最后
案例
public class Demo1 {
static class MyThread1 implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "线程进入了run方法");
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class MyThread2 implements Callable {
@Override
public Long call() throws Exception {
try {
System.out.println(Thread.currentThread().getName() + "线程进入了call方法,开始sleep");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "睡醒了");
} catch (Exception e) {
e.printStackTrace();
}
return System.currentTimeMillis();
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Runnable runnable = new MyThread1();
Callable callable = new MyThread2();
FutureTask futureTask = new FutureTask(callable);
new Thread(futureTask, "线程二").start();
for (int i = 0; i < 10; i++) {
Long res = (Long) futureTask.get();
System.out.println(res);
}
new Thread(runnable, "线程一").start();
}
}
复制代码
7.4、小结
- 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成, 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态
- 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
- 只计算一次
8.JUC的三大辅助类
8.1、减少计数CountDownLatch
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句。
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
- 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
场景
//场景:六个同学全部离开教室,班长关门
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "号同学离开了教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("班长给教室锁门");
}
}
复制代码
8.2、循环栅栏CyclicBarrier
CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。可以将CyclicBarrier理解为加1操作
场景
//场景:集齐七颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {
private static final int NUMBER = 7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
System.out.println("集齐" + NUMBER + "召唤神龙");
});
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "星龙珠被收集");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
复制代码
8.3、信号灯 Semaphore
emaphore的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用acquire方法获得许可证,release方法释放许可
场景
//6辆汽车抢3个停车位
public class SemaphoreDemo {
public static void main(String[] args) {
//创建3个车位
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
//抢占车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "号汽车抢到了车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + "离开了");
} catch (Exception e) {
e.printStackTrace();
}finally {
//空出车位
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
复制代码
9.读写锁
9.1、读写锁介绍
JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
1 线程进入读锁的前提条件:
- 没有其他线程的写锁
- 没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。
2 线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
3 读写锁三个重要的特性
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- 重进入:读锁和写锁都支持线程重进入。
- 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
9.2、ReentrantReadWriteLock
ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock实现了自己的序列化逻辑。
9.3、案例
class MyCache {
private Map<String, Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//放数据
public void put(String key, Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始写");
TimeUnit.MICROSECONDS.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写完了");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//读数据
public Object get(String key) {
Object res = null;
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始读");
TimeUnit.MICROSECONDS.sleep(300);
res = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完了");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
return res;
}
}
public class Demo {
public static void main(String[] args) {
MyCache m = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
m.put(num + "", num);
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
m.get(num + "");
}, String.valueOf(i)).start();
}
}
}
复制代码
10.BlockingQueue阻塞队列
10.1 阻塞队列概述
Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
10.2 阻塞队列的架构
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
10.3 阻塞队列分类
- ArrayBlockingQueue 由数组结构组成的有界阻塞队列
- LinkedBlockingQueue 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
10.4 阻塞队列核心方法
- 放入数据
- offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程)
- offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败
- put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
- 获取数据
- poll(time): 取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
- .poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败
- take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
- drainTo(): 一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
11.ThreadPool线程池
11.1、线程池简介
优势
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
特点
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
- 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
11.2、 线程池的参数说明
1 常用参数
- corePoolSize线程池的核心线程数
- maximumPoolSize能容纳的最大线程数
- keepAliveTime空闲线程存活时间
- unit 存活的时间单位
- workQueue 存放提交但未执行任务的队列
- threadFactory 创建线程的工厂类
- handler 等待队列满后的拒绝策略
当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略
2 拒绝策略
CallerRunsPolicy:当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy: 直接丢弃,其他啥都没有
DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
11.3 线程池的种类和创建
1 newCachedThreadPool(常用)
作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
特点:
• 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
• 线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
• 当线程池中,没有可用线程,会重新创建一个线程
创建方式
public static ExecutorService newCachedThreadPool(){
/**
* corePoolSize线程池的核心线程数
* maximumPoolSize能容纳的最大线程数
* keepAliveTime空闲线程存活时间
* unit 存活的时间单位
* workQueue 存放提交但未执行任务的队列
* threadFactory 创建线程的工厂类:可以省略
* handler 等待队列满后的拒绝策略:可以省略
*/
return new ThreadPoolExecutor(0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<>(), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
复制代码
场景
适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景
2 newFixedThreadPool(常用)
作用
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
特征
• 线程池中的线程处于一定的量,可以很好的控制线程的并发量
• 线程可以重复被使用,在显示关闭之前,都将一直存在
• 超出一定量的线程被提交时候需在队列中等待
创建方式
public static ExecutorService newFixedThreadPool(){
/**
* corePoolSize线程池的核心线程数
* maximumPoolSize能容纳的最大线程数
* keepAliveTime空闲线程存活时间
* unit 存活的时间单位
* workQueue 存放提交但未执行任务的队列
* threadFactory 创建线程的工厂类:可以省略
* handler 等待队列满后的拒绝策略:可以省略
*/
return new ThreadPoolExecutor(10,
10,
0L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
复制代码
场景
适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景
3 newSingleThreadExecutor(常用)
作用
创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
特征
线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
创建方式
public static ExecutorService newSingleThreadExecutor(){
/**
* corePoolSize线程池的核心线程数
* maximumPoolSize能容纳的最大线程数
* keepAliveTime空闲线程存活时间
* unit 存活的时间单位
* workQueue 存放提交但未执行任务的队列
* threadFactory 创建线程的工厂类:可以省略
* handler 等待队列满后的拒绝策略:可以省略
*/
return new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
复制代码
4 newScheduleThreadPool(了解)
作用
线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池
特征
-
线程池中具有指定数量的线程,即便是空线程也将保留
-
可定时或者延迟执行线程活动
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
复制代码
场景
适用于需要多个后台线程执行周期任务的场景
5 newWorkStealingPool
jdk1.8提供的线程池,底层使用的是ForkJoinPool实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu核数的线程来并行执行任务
创建方式
public static ExecutorService newWorkStealingPool(int parallelism) {
/**
* parallelism:并行级别,通常默认为JVM可用的处理器个数
* factory:用于创建ForkJoinPool中使用的线程。
* handler:用于处理工作线程未处理的异常,默认为null
* asyncMode:用于控制WorkQueue的工作模式:队列---反队列
*/
return new ForkJoinPool(parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
复制代码
场景
适用于大耗时,可并行执行的场景
11.4、线程池案例演示
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadService = new ThreadPoolExecutor(3, 3, 60l, TimeUnit.SECONDS, new LinkedBlockingDeque()
, Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
try{
for (int i = 1; i <= 10; i++) {
threadService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "开始卖票");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票结束");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadService.shutdown();
}
}
}
复制代码
11.5、线程池的底层工作原理
- 在创建了线程池后,线程池中的线程数为零
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
- 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
11.6 注意事项
创建线程池推荐适用ThreadPoolExecutor及其7个参数手动创建