目录
01:Executors.newSingleThreadExecutor() //单个线程
02:newFixedThreadPool(int nThreads) //创建一个固定的线程池的大小
03:newCachedThreadPool() //缓存池,可伸缩的, 遇强则强,遇弱则弱
可重入锁概念
什么是可重入锁,不可重入锁呢?"重入"字面意思已经很明显了,就是可以重新进入。可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。比如synchronized内置锁就是可重入的,如果A类有2个synchornized方法method1和method2,那么method1调用method2是允许的。显然重入锁给编程带来了极大的方便。假如内置锁不是可重入的,那么导致的问题是:1个类的synchornized方法不能调用本类其他synchornized方法,也不能调用父类中的synchornized方法。与内置锁对应,JDK提供的显示锁ReentrantLock也是可以重入的。
一个线程获取多少次锁,就必须释放多少次锁。这对于内置锁也是适用的,每一次进入和离开synchornized方法(代码块),就是一次完整的锁获取和释放。
下面这个例子因为主线程2次获取了锁,但是却只释放1次锁,导致线程t永远也不能获取锁。
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock ();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
lock.writeLock().lock();
System.out.println("Run方法");
lock.writeLock().unlock();
}
});
lock.writeLock().lock();
lock.writeLock().lock(); //这里锁了两次
t.start();
Thread.sleep(200);
System.out.println("主线程");
lock.writeLock().unlock(); //然后这里只解锁了一次导致有一个写入锁未释放,所以造成死锁,导致Thread线程没被执行
//lock.writeLock().unlock(); 锁了两次,最后这里就必须释放两次,然后Thread就是执行了
}
}
8.ReadWriteLock--读写锁

由官方文档看到,读可以允许有多个线程进行,但是写只可有一个线程。
Synchronized存在明显的一个性能问题就是读与读之间互斥,简言之就是,我们编程想要实现的最好效果是,
可以做到读和读互不影响,读和写互斥,写和写互斥,提高读写的效率,ReadWriteLock可以做到。
Java并发包中ReadWriteLock是一个接口,主要有两个方法,源码如下:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。
Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容,读写锁之间为互斥。
public class Demo {
public static void main(String[] args) {
MyCache2 myCache = new MyCache2();
//写入5个线程数据
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp);
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
//加锁的
class MyCache2{
private volatile Map<String,Object> map = new HashMap<>();
//读写锁,更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value){
readWriteLock.writeLock().lock(); //用法与lock类似
try { //try里防止代码块
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK"+key);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock(); //和lock一样需要解锁
}
}
//取,读,所有人都可以读
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK" + key);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
/* 使用读写锁可以实现读写互斥
2写入2
2写入OK2
1写入1
1写入OK1
*/
//自定义缓存
class MyCache{ //这种没有用读写锁会造成还没写入完成一个锁的时候被另一个锁写入
private volatile Map<String,Object> map = new HashMap<>();
//存,写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK"+key);
}
//取,读
public void get(String key){
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK"+key);
}
/*
1写入1
4写入4
4写入OK4
5写入5
5写入OK5
*/
}
9.阻塞队列BlockingQueue
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
简述一下其中比较重要的
ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。
DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
队列的一个特性是先进先出,即FIFO;栈是先进后出;所以可以形象的称为:队列——吃了拉,栈——吃了吐。
正常说队列阻塞有两种情况,一种是写入:如果队列满了,就必须阻塞等待;取的时候如果队列是空的就必须阻塞等待生产。
和list,set一样,都是继承自Collection接口;
方式 | 有返回值,抛出异常 | 有返回值,不抛出异常 | 没有返回值,阻塞等待 | 有返回值,超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer()) |
移除 | remove() | poll() | get() | pull() |
检测队首元素 | element() | peek() |
1.add() 和 remove() 方法
// 抛出异常 返回布尔值
public static void test1(){
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add(1));
System.out.println(queue.add(2));
System.out.println(queue.add(3));
// 抛出异常 java.lang.IllegalStateException: Queue full
// System.out.println(queue.add(4));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
// 抛出异常 java.util.NoSuchElementException
// System.out.println(queue.remove());
}
add方法在阻塞队列中添加元素会返回布尔值,如果阻塞队列满了还在继续添加就会抛出异常,队列每次remove会弹出阻塞队列队首元素,并且返回改弹出元素,如果阻塞队列为空继续remove就会抛出异常
2. offer() 和 poll() 方法(无参版)
public static void test2(){
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer(1));
System.out.println(queue.offer(2));
System.out.println(queue.offer(3));
// 不抛出异常,返回布尔值
System.out.println(queue.offer(4));
// 检测队首元素 如果队列为空抛出异常
//System.out.println(queue.element());
// 检测队首元素, 如果队列为空返回null
System.out.println(queue.peek());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
// 不抛出异常, 返回null
System.out.println(queue.poll());
// 抛出异常java.util.NoSuchElementException
//System.out.println(queue.element());
// 如果队首没有元素返回null 不抛出异常
System.out.println(queue.peek());
}
offer() 和 poll()方法在阻塞队列为满或者空时返回布尔值(成功返回true,失败返回false),不会抛出异常。这里附加两个检查队首元素的方法。element()检查队首元素,如果队列为空抛出异常;peek()检测队首元素, 如果队列为空返回null,不抛出异常。
3.put() 和 take() 方法
// 等待 阻塞,一直阻塞 没有返回值
public static void test3() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
queue.put(1);
queue.put(2);
// 阻塞
//queue.put(3);
System.out.println(queue.take());
System.out.println(queue.take());
// 阻塞 等待
// queue.take();
}
put() 和 take() 方法会在队列满或空时一直阻塞等待,直到队列不为满或空时。put()方法没有返回值。
4.offer() 和 poll() 方法(带参版)
public static void test4() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
queue.offer(1);
queue.offer(2);
// 阻塞等待2s,
queue.offer(3, 2, TimeUnit.SECONDS);
System.out.println("==================");
System.out.println(queue.poll());
System.out.println(queue.poll());
queue.poll(3, TimeUnit.SECONDS);
System.out.println("==================");
}
offer() 方法带参数,第一个参数是往队列中添加的元素,第二个参数是时间大小,第三个单位是时间单位(使用TimeUnit工具类)
poll()中第一个参数是时间大小,第二个单位是时间单位(使用TimeUnit工具类)
offer() 和 poll() 方法没有返回值,会设置等待时间,超过等待时间之后就继续执行之后的语句。
10.SynronousQueue
SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即 put 的时候),如果当前没有人想要消费产品(即当前没有线程执行 take),此生产线程必须阻塞,等待一个消费线程调 用 take 操作,take 操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程 称为一次配对过程(当然也可以先 take 后 put,原理是一样的)。
我们用一个简单的代码来验证一下,如下所示:
public class Demo {
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
*/
这个结果T1put之后必须等T2take了,线程才会执行下一个put。
11.线程池(重点)
写在前面:
1.Executors工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
2.Executor 接口对象能执行我们的线程任务。
3.ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法获取任务执行的状态并可以获取任务的返回值。
4.使用ThreadPoolExecutor可以创建自定义线程池。
5.Future表示异步计算的结果,他提供了检查计算是否完成的方法,可以使用get()方法获取计算的结果。
一、三大方法(Executors工具类)
三大方法名以及源码如下
01:Executors.newSingleThreadExecutor() //单个线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//Executors 工具类、3大方法
public class Demo01 {
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(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
运行结果: (10个任务被同一个线程所操作)
02:newFixedThreadPool(int nThreads) //创建一个固定的线程池的大小
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//Executors 工具类、3大方法
public class Demo01 {
public static void main(String[] args) {
//最多5个线程同时执行,从控制台中查看结果
ExecutorService threadpool = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小,(5个线程)
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(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
03:newCachedThreadPool() //缓存池,可伸缩的, 遇强则强,遇弱则弱
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//Executors 工具类、3大方法
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadpool = Executors.newCachedThreadPool(); //缓存池,可伸缩的, 遇强则强,遇弱则弱
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(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
运行结果:(最高同时有10个线程在执行,可伸缩的, 遇强则强,遇弱则弱)
看如上三个方法的源码可知都是使用Executors工具类下的方法,然后根据阿里的开发手册如下,不允许这样写,原因会导致OOM(内存溢出),所以应该使用线程池来做。
需要根据线程池的源码来自定义线程池。
二、线程池的7大参数
int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 线程池最大容量
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle // 拒绝策略)
01:7大参数之源码分析:
(4) 三大方法所公共的 ThreadPoolExecutor() 方法
******7大参数******
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler) { //拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
三、四种拒绝策略:
new ThreadPoolExecutor.AbortPolicy() // 队列满了,还有人进来,不处理这个人的,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和早的竞争,也不会抛出异常!
手动(自定义)创建一个线程池:银行办业务问题
举例,银行共有5个窗口(最大线程数),有常开的两个窗口处理业务(核心线程数);候客区(阻塞队列)有三个位置;当两个常开窗口(核心线程)以及候客区(阻塞队列)的位置都有人后,再来客户会开启另外三个窗口;当5个窗口(最大线程)和候客区(阻塞队列)都有人后,再来客户会启动拒绝策略。当窗口(线程)超过设定时间(超时时间)没有业务办理时(未调用)会关闭窗口(释放线程)。
1).new ThreadPoolExecutor.AbortPolicy() //银行满了,还有人进来,不处理这个人的,抛出异常
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo02 {
public static void main(String[] args) {
//自定义线程池! 工作中只会使用 ThreadPoolExecutor
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, //核心线程池大小
5, //最大核心线程池大小
3, //超时了没有人调用就会释放
TimeUnit.SECONDS, //超时单位
new LinkedBlockingDeque<>(3), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
new ThreadPoolExecutor.AbortPolicy()); //银行满了,还有人进来,不处理这个人的,抛出异常
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(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
从1到9个线程的运行结果(对比):
阻塞队列加上最大线程允许数(Runtime.getRuntime().availableProcessors();)为线程池的大小
2).new ThreadPoolExecutor.CallerRunsPolicy() //哪来的去哪里
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo02 {
public static void main(String[] args) {
//自定义线程池! 工作中只会使用 ThreadPoolExecutor
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, //核心线程池大小
5, //最大核心线程池大小
3, //超时了没有人调用就会释放
TimeUnit.SECONDS, //超时单位
new LinkedBlockingDeque<>(3), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
new ThreadPoolExecutor.CallerRunsPolicy()); //哪来的去哪里!
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(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
运行结果:
3).new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo02 {
public static void main(String[] args) {
//自定义线程池! 工作中只会使用 ThreadPoolExecutor
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, //核心线程池大小
5, //最大核心线程池大小
3, //超时了没有人调用就会释放
TimeUnit.SECONDS, //超时单位
new LinkedBlockingDeque<>(3), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
new ThreadPoolExecutor.DiscardPolicy()); //队列满了,丢掉任务,不会抛出异常!
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(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
运行结果:
4).new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试和最早的竞争,也不会抛出异常
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo02 {
public static void main(String[] args) {
//自定义线程池! 工作中只会使用 ThreadPoolExecutor
ThreadPoolExecutor 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(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
运行结果:
四、工作流程:
1 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。
2 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。
3 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。
4 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。
五、线程池的概念:
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
六、线程池的工作机制
2.1 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
2.1 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
七、使用线程池的原因:
多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。
线程池面试考题:三个方法,7大参数,4中拒绝策略
池化技术:事先准备好一些资源,有人要用就来拿,用完后归还池里。
线程池的好处:
- 降低资源的消耗
- 提高响应的速度
- 方便管理
小结和拓展(面试常问)
问题:池的线程最大值如何去设置!
了解:IO密集型,CPU密集型(调优)
最大线程到底该如何定义
1、CPU 密集型,几核的计算机就设置为几,可以保存CPU的效率最高!
自动获取计算机CPU处理器个数
Runtime.getRuntime().availableProcessors()
2、IO 密集型 —> 判断程序中十分耗IO的线程,根据IO线程数来设定。
比如:15个大型IO线程任务,可以最大线程数为30
另摘自博客【Java面试】第二章:P5级面试_java_wxid的博客-优快云博客_java p5 部分知识点,如下:
线程池就是控制运行的线程数量,处理过程中将任务放到队列,然后在线程创建后启动这些任务,如果线程数量超出了最大数量就排队等候,等其他线程执行完毕再从队列中取出任务执行。
线程池实现原理,七大核心参数,如何合理的配置核心线程数?拒绝策略?JUC并发包:信号灯,循环栅栏,倒计时器
线程池就是控制运行的线程数量,处理过程中将任务放到队列,然后在线程创建后启动这些任务,如果线程数量超出了最大数量就排队等候,等其他线程执行完毕再从队列中取出任务执行。
线程池相当于银行网点,常驻核心数相当于今日当值窗口,线程池能够同时执行的最大线程数相当于银行所有的窗口,任务队列相当于银行的候客区,当今日当值窗口满了,多出来的客户去候客区等待,当候客区满了,银行加开窗口,候客区先来的客户去加班窗口,当银行所有的窗口满了,其他客户在候客区等待,同时拒绝其他客户进入银行。当用户少了,加班的窗口等待时间(相当于多余线程存活的时间)(等待时间的单位相当于unit参数)假设超过一个小时还是没有人来,就取消加班的窗口。
底层在创建线程池的时候有七个参数:核心线程数,同时执行的最大线程数,多余线程存活时间,单位时间秒,任务队列,默认线程工厂,拒绝策略
corePoolSize:核心线程数,如何合理的配置核心线程数?
- 对于CPU密集型任务,由于CPU密集型任务的性质,导致CPU的使用率很高,如果线程池中的核心线程数量过多,会增加上下文切换的次数,带来额外的开销。因此,考虑到CPU密集型任务因为某些原因而暂停,这个时候有额外的线程能确保CPU这个时刻不会浪费,还可以增加一个CPU上下文切换。一般情况下:线程池的核心线程数量等于CPU核心数+1。例如需要大量的计算,视频渲染啊,仿真啊之类的。这个时候CPU就卯足了劲在运行,这个时候切换线程,反而浪费了切换的时间,效率不高。打个比方,你的大脑是CPU,你本来就在一本心思地写作业,多线程这时候就是要你写会作业,然后立刻敲一会代码,然后在P个图,然后在看个视频,然后再切换回作业。emmmm,过程中你还需要切换(收起来作业,拿出电脑,打开VS…)那你的作业怕是要写到挂科。这个时候你就该一门心思地写作业。
- 对于I/O密集型任务,由于I/O密集型任务CPU使用率并不是很高,可以让CPU在等待I/O操作的时去处理别的任务,充分利用CPU。一般情况下:线程的核心线程数等于2*CPU核心数。例如你需要陪小姐姐或者小哥哥聊天,还需要下载一个VS,还需要看博客。打个比方,小姐姐给你发消息了,回一下她,然后呢?她给你回消息肯定需要时间,这个时候你就可以搜索VS的网站,先下安装包,然后一看,哎呦,她还没给你回消息,然后看会自己的博客。小姐姐终于回你了,你回一下她,接着看我的博客,这就是类似于IO密集型。你可以在不同的“不烧脑”的工作之间切换,来达到更高的效率。而不是小姐姐不回我的信息,我就干等,啥都不干,就等,这个效率可想而知,也许,小姐姐根本就不会回复你。
- 对于混合型任务,由于包含2种类型的任务,故混合型任务的线程数与线程时间有关。在某种特定的情况下还可以将任务分为I/O密集型任务和CPU密集型任务,分别让不同的线程池去处理。一般情况下:线程池的核心线程数=(线程等待时间/线程CPU时间+1)*CPU核心数;
并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,我们的项目使用的时redis作为缓存(这类非关系型数据库还是挺好的)。增加服务器是第二步(一般政府项目的首先,因为不用对项目技术做大改动,求一个稳,但前提是资金充足),至于线程池的设置,设置参考 2 。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件(任务时间过长的可以考虑拆分逻辑放入队列等操作)对任务进行拆分和解耦。
maximumPoolsize:同时执行的最大线程数
keepAliveTime:多余线程存活时间,当前线程池数量超过核心线程数时,当前空闲时间达到多余线程存活时间的值的时候,多余空闲线程会被销毁到只剩核心线程数为止
unit:多余线程存活时间的单位
workQueue:任务队列,被提交但尚未被执行的任务
threadFactory:生成线程池的线程工厂
handler:拒绝策略,当队列满了并且工作线程数量大于线程池的最大线程数时,提供拒绝策略。
拒绝策略:
- 第一种拒绝策略:AbortPolicy:超出最大线程数,直接抛出RejectedExecutionException异常阻止系统正常运行。可以感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
- 第二种拒绝策略:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,相当于当线程池无能力处理当前任务时,会将这个任务的执行权交予提交任务的线程来执行,也就是谁提交谁负责,从而降低新任务的流量。(谁调用了你,到达最大线程数时,你回去找调用你的人,然后听从调用你的人安排)(超出的我们能办的给你办,不能办的给你回退 )这样的话提交的任务就不会被丢弃而造成业务损失,如果任务比较耗时,那么这段时间内提交任务的线程也会处于忙碌状态而无法继续提交任务,这样也就减缓了任务的提交速度,这相当于一个负反馈,也有利于线程池中的线程来消化任务。这种策略算是最完善的相对于其他三个。
- 第三拒绝策略:DiscardOldestPolicy:抛弃队列中等待最久的任务,也就是它丢弃的是队列中的头节点,然后把当前任务加入队列中尝试再次提交当前任务
- 第四种拒绝策略:DiscardPolicy:直接丢弃任务,不予任何处理也不抛异常,当任务提交时直接将刚提交的任务丢弃,而且不会给与任何提示通知。
实际创建线程池:
在实际使用的时候,选择线程池的时候尽量不用JDK提供的三种常见的创建方式,因为它的底层队列是Linked这个接近于无界,非常大,这样会堆积大量的请求,从而导致OOM,
阿里巴巴开发手册推荐我们使用ThreadPoolExecutor去创建线程池。
JUC并发包:
CountDownLatch倒计时器:
让一些线程阻塞直到另一些线程完成一系统操作后才被唤醒。一个 CountDownLatch 用给定的计数初始化。await() 方法阻塞,直到由于countDown() 方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的 await() 调用立即返回。 这是一个一次性的现象 - 计数无法重置。
举个例子:我们的API接口响应时间被要求在200ms以内,但是如果一个接口内部依赖多个三方/外部服务,那串行调用接口的响应时间必然很久,所以可使用内部服务并行调用进行优化。假设接口内部依赖了10个外部服务,创建CountDownLatch实例,计数数量为10,有10个线程来完成任务,等待在CountDownLatch上的线程执行完才能继续执行那个响应时间较快的接口。latch.countDown();方法作用是通知CountDownLatch有一个线程已经准备完毕,倒计数器可以减一了。latch.await()方法要求主线程等待所有10个检查任务全部准备好才一起并行执行。
一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检测。只有等到所有的检查完毕后,引擎才能点火。那么在检测环节当然是多个检测项可以同时进行的
Semaphore信号灯:
多个共享资源互斥使用,控制线程并发数量,多个线程抢多个资源。
1、Semaphore信号量作为一种流控手段,可以对特定资源的允许同时访问的操作数量进行控制,例如池化技术(连接池)中的并发数,有界阻塞容器的容量等。
2、Semaphore中包含初始化时固定个数的许可,在进行操作的时候,需要先acquire获取到许可,才可以继续执行任务,如果获取失败,则进入阻塞;处理完成之后需要release释放许可。
3、acquire与release之间的关系:在实现中不包含真正的许可对象,并且Semaphore也不会将许可与线程关联起来,因此在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为是消费一个许可,而release操作是创建一个许可,Semaphore并不受限于它在创建时的初始许可数量。也就是说acquire与release并没有强制的一对一关系,release一次就相当于新增一个许可,许可的数量可能会由于没有与acquire操作一对一而导致超出初始化时设置的许可个数。 举例,有六台车抢三个停车位。
CyclicBarrier循环栅栏:
当多个线程一起执行任务是,一个线程没有完成任务,其他线程都必须进入等待状态,等待这个线程完成任务后,才能再执行其他任务。强调相互等待,一个线程不完成,其他线程全部等待。
创建CyclicBarrier时,它默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量。调用await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞。
例如:一个公司去团建,需要大家全部集合完毕后,才能出发,有一个人员不到,全员都得等待。它的作用就是会让所有线程都等待完成后才会继续下一步行动