目录
线程池
创建线程池的目的
线程池创建的目的在于:如果我们直接采用像之前的Thread构造方法创建线程,其实在每次创建和销毁线程时都有一定的开支,当线程太多时这个开支还是挺明显的。
“池”:目的就是让某些对象被多次重复利用,减少频繁创建和销毁对象带来的开支问题
所以说,线程池最大的好处就是可以减少每次启动和销毁线程的损耗(提高时间和空间利用率)
线程池的概念
线程池内部创建了若干个线程,这些线程都是Runnable的状态,只需要从系统中取出任务(run),就可以立即开始执行。
我们将线程池比作一个餐厅:
餐厅中的固定员工:线程池中的线程
后面招聘的临时员工:当线程池中的线程不够用时,可产生临时员工。
JDK中线程池的使用
线程池的核心父类接口:ExecutorService接口
执行任务方法:excute
提交任务方法:submit
提交一个任务到线程池(线程的run或者call方法),池中会派遣空闲线程执行任务
终止线程方法:shutdown
立即终止所有线程:shutdownNow(); 无论线程是否在空闲状态
停止在空闲态的线程,运行线程在结束后停止:shutdown();
Executors=>线程池的工具类
这个类可以创建JDK内置的四大线程池
加s的基本都是工具类 比如Arrays(数组工具类,copyOf,sort等等)
固定大小的线程池
//固定大小的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
采用.submit()方法执行任务
pool.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"绕"+i+"圈");
}
}
});
工具类源码:
数量动态变化的缓存池
//数量动态变化的缓存池
ExecutorService pool1 = Executors.newCachedThreadPool();
工具类源码:
橘色的参数基本是用不到的,因为最大线程数有四十多亿
单线程池
ps:单线程池的意义在于,省去创建线程和销毁线程的时间,来一个任务执行一个任务,还未执行的任务在工作队列排队执行。
//只包含一个线程的单线程池
ExecutorService pool2 = Executors.newSingleThreadExecutor();
工具类源码:
定时线程池
ps:使用的是定时器线程接口,线程池核心接口的子接口
//定时器线程池
ScheduledExecutorService pool3 = Executors.newScheduledThreadPool(10);
采用.schedule接收任务,可规定延迟多久执行任务
pool3.schedule(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"绕"+i+"圈");
}
}
},3, TimeUnit.SECONDS);
工具类源码:
线程池的接口和类
ExecutorService:线程池的核心接口,提供excute方法,submit方法,shutdown方法
ScheduledExecutorService:定时器接口,线程池核心接口的子接口,追加.schedule方法,可以延迟启动任务
ThreadPoolExector类:实现核心接口的子类,也是线程池接口的最常用实现子类
Executors:线程池的工具类,提供了ThreadPoolExector的对象,内置创建好的四大线程池。
ThreadPoolExector子类的核心构造方法参数
一图流
线程池工作流程
此工作流程基于submit提交一个新任务时线程池内部的执行流程
首先判断核心池是否已经拉满,没拉满我们可以再创建一个固定线程(正式工)执行此任务
若已经拉满,我们就去判断工作队列是否已满,没有满的话,我们就在队列中排队等待,
否则,进行线程池的最大数量判断,如果没满的话就创建一个空闲线程来执行此任务,
否则,若线程池已经达到了最大数量,说明此时线程池已经达到最大负荷了,我们就需要执行拒绝策略了
开始执行
若已经达到核心池最大数量
若队列已经满了
当线程数量达到上线,执行拒绝策略
常见锁的策略
1.乐观锁和悲观锁
synchronized最开始就是乐观锁,当竞争激烈就变成悲观锁。
乐观锁
每次读写数据都认为不会发生冲突,不会阻塞,一般来说只有在数据更新时才会检查是否发生冲突,若没有冲突直接更新,有冲突再去解决冲突。
当线程冲突不严重时,可以采用乐观锁策略来避免多次的加锁解锁操作
eg:
乐观锁一般通过版本号机制来实现
比如这两个线程,如果线程1先结束并将主内存的版本从version1变成version2时,线程2写回主内存检测到主内存的版本和自己的工作内存版本对不上,就会直接报错,不写回。
当下次再进行线程操作时,就会读取新的版本号,这个时候重新进行操作,尝试写回。
悲观锁:
每次进行读写数据都会冲突,都需要尝试加锁操作,保证同一时间只有一个线程在读写数据。
当线程冲突严重时,就需要加锁,来避免线程频繁访问共享数据失败带来的CPU空转问题
2.读写锁
读写锁的适用条件
特别适用于线程基本都在读数据,很少有些数据的情况。
多数据在访问数据时,并发读数据不会冲突,只有在写数据时才有可能发生冲突,所有就有了读写锁的概念,JDK内置的读写锁是ReentrantReadWriteLock,用这个可实现读写操作。
读写锁的特性
1.多个线程并发读数据时,都可以访问到读锁,并发执行不互斥。
2.多个线程写数据时,两个线程互斥,只有一个能访问到写锁,其他线程阻塞。
3.一个线程读,另一个线程写,也会互斥,当写的过程结束读线程才能继续执行。
3.重量级锁和非重量级锁
重量级锁
需要操作系统和硬件支持,线程获取重量级锁失败进入阻塞状态(os,用户态切换到内核态,开销非常大)
eg:
比如去银行办理业务
用户态:在窗口外自己处理的业务
内核态:窗口内部,需要工作人员协助
这样来回切换是非常耗时的
轻量级锁
尽量在用户态执行操作,线程不阻塞,不会进行状态切换。
轻量级锁的常用实现是采用自旋锁:
之前的方式是,获取锁失败就进入Blocked状态,线程阻塞等待锁释放,然后由CPU唤醒(这个时间一般都比较长,由用户态切换到内核态)。
自旋锁:
就是一个循环的概念
while(获取lock==false)就进行死循环,但不会让出CPU,线程也不会阻塞,不会切换状态,线程就在CPU上空跑,直到锁被释放,就可以很快速地获取到锁。
4.公平锁和非公平锁
公平锁
获取锁失败的线程进入等待队列,当锁被释放,在队列中等待时间最长的线程首先获取到锁。
非公平锁
阻塞队列的各个线程获取到锁的几率相等,不分先后。
synchronized锁的升级策略
偏向锁 ->轻量级锁 ->重量级锁