JUC并发编程之:简单概述(五)
##不可变类
##享元模式
##并发工具
>线程池
>JUC工具包
>disruptor
>guava
一、不可变类
1.1、日期转换的问题
·下面的代码在运行时,由于SimpleDateFormat不是线程安全的,很大机率出现:
java.lang.NumberFormatException或者出现不正确的日期解析结果
@Slf4j
public class SimpleDateFormatTest {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i <10 ; i++) {
new Thread(()->{
try {
log.debug("parse : {}",sdf.parse("2021-03-09"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
##结果:
java.lang.NumberFormatException: multiple points
java.lang.NumberFormatException: For input string:""
17:06:50.246 [Thread-9] DEBUG x- parse : Tue Mar 09 00:00:00 CST 2021
##我们可以加锁解决,但【对性能有影响】
new Thread(()->{
synchronized(sdf){
try {
log.debug("parse : {}",sdf.parse("2021-03-09"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
1.2、不可变类的使用
@Slf4j
public class DateTimeFormatTest {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i <10 ; i++) {
new Thread(()->{
TemporalAccessor accessor = dtf.parse("2021-03-09");
log.debug("accessor : {}",accessor);
}).start();
}
}
}
1.3、不可变类的设计
##除了上述的DateTimeFormatter是不可变类,String类也是不可变的,以String为例,
说明一下不可变设计的要素:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
private final char value[];//保存字符串
private int hash;//缓存hash码
// ...
}
##一、final的使用
发现该类、类中所有属性都是final的
·属性用final修饰保证了该属性是只读的,不能修改的
·类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
##二、保护性拷贝
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
通过创建副本对象来避免共享的手段称之为:保护性拷贝
二、享元模式
2.1、享元模式
·Flyweight pattern,当需要重用数量有限的同一类对象时
##体现:
在JDK中Boolean Byte Short Integer Long Character等包装类提供了valueOf方法,
例如Long的valueOf会缓存-128~127之间的Long对象,在这个范围之间会重用对象,大于这个范围才会信件Long对象
public static Long valueOf(long l){
final int offset = 128;
if(l >= -128 && l <= 127 ){
return LongCache.cache[(int)l+offset];
}
return new Long(l);
}
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
Byte Short Long缓存的范围都是-128~127
Character缓存范围是0~127
Integer的默认范围是-128~127,最小值不可变,但最大值可以通过调整虚拟机参数来改变
Boolean缓存了TRUE和FALSE
2.2、享元模式-自定义连接池
一个应用,如果每次都重新创建和关闭数据库连接,性能会收到极大影响。这时预先创建好一批连接,
放入连接池。一次请求到达后,从连接池获取连接,使用完毕后在还回连接池,这样既节约了连接的
创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
@Slf4j
public class MyConnectPool {
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool(2);
for (int i = 0; i <5 ; i++) {
new Thread(()->{
//获取连接
Connection conn = pool.gain();
//使用
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放连接
pool.free(conn);
},"t"+i).start();
}
}
}
//连接池
@Slf4j
class ConnectionPool{
//线程池大小
private final int poolSize;
//连接对象数组
private Connection[] connections;
//每个连接对象的使用状况--多个线程取修改数组,
//普通数组线程不安全,因此使用原子数组
//0 连接空闲 1 连接正在使用
private AtomicIntegerArray array;
//初始化线程池大小和连接
public ConnectionPool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.array = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i <poolSize ; i++) {
connections[i] = new MockConnection();
}
}
//获取空闲连接
public Connection gain(){
while (true){
//获取到空闲连接
for (int i = 0; i <poolSize ; i++) {
if(array.get(i)==0){
//更改连接状态为1
if (array.compareAndSet(i,0,1)) {
log.debug("获取连接:{}",i);
return connections[i];
}
}
}
//没有获取到空闲连接
synchronized (this){
try {
log.debug("获取连接等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//归还连接
public void free(Connection conn){
for (int i = 0; i <poolSize ; i++) {
if(connections[i]==conn){
//更改连接状态为0
array.set(i,0);
synchronized (this){
log.debug("归还连接:{}",i);
this.notifyAll();
}
break;
}
}
}
}
//自定义连接
class MockConnection implements Connection{
//内容省略...
}
##结果:
11:08:59.956 [t2] XXX - 获取连接等待
11:08:59.956 [t0] XXX - 获取连接:0
11:08:59.956 [t1] XXX - 获取连接:1
11:08:59.973 [t4] XXX - 获取连接等待
11:08:59.973 [t3] XXX - 获取连接等待
11:09:00.334 [t1] XXX - 归还连接:1
11:09:00.334 [t3] XXX - 获取连接等待
11:09:00.334 [t4] XXX - 获取连接:1
11:09:00.334 [t2] XXX - 获取连接等待
11:09:00.836 [t0] XXX - 归还连接:0
11:09:00.836 [t2] XXX - 获取连接:0
11:09:00.836 [t3] XXX - 获取连接等待
11:09:00.951 [t4] XXX - 归还连接:1
11:09:00.951 [t3] XXX - 获取连接:1
11:09:01.401 [t3] XXX - 归还连接:1
11:09:01.732 [t2] XXX - 归还连接:0
##不足:
1> 连接的动态增长与收缩(连接数的动态修改)
2> 连接保活(可用性检测)
3> 等待超时处理
4> 分布式hash
三、线程池
3.1、自定义线程池
3.1.1、阻塞队列
//阻塞队列
@Slf4j
class BlockingQueue<T>{
//任务队列
private Deque<T> queue = new ArrayDeque<>();
//锁
private ReentrantLock lock = new ReentrantLock();
//生产者条件变量
private Condition fullWaitSet = lock.newCondition();
//消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
//队列容量
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
//阻塞获取--带超时的
public T poll(long timeout, TimeUnit unit){
lock.lock();
try {
long nanos = unit.toNanos(timeout);
while(queue.isEmpty()){
try {
if(nanos<=0){
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
//阻塞获取
public T take(){
lock.lock();
try {
while(queue.isEmpty()){
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
//阻塞添加
public void put(T t){
lock.lock();
try {
while(queue.size()==capacity){
try {
log.debug("等待...新增任务到阻塞队列,task:{}",t);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("新增任务到阻塞队列,task:{}",t);
queue.addLast(t);
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
//获取容量大小
public int size(){
lock.lock();
try {
return queue.size();
}finally {
lock.unlock();
}
}
}
3.1.2、线程池
//线程池
@Slf4j
class ThreadPool{
//任务队列
private BlockingQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers = new HashSet<>();
//核心线程数
private int coreSize;
//任务超时时间
private long timeout;
//超时时间单位
private TimeUnit timeUnit;
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapacity) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
taskQueue = new BlockingQueue<>(queueCapacity);
}
//执行任务
public void execute(Runnable task){
//当任务数没有超过coreSize时,直接执行
//当任务数超过coreSize时,存储到任务队列taskQueue
synchronized (workers){
if(workers.size()<coreSize){
Worker woker = new Worker(task);
log.debug("新增任务,task:{}, worker:{}",task,woker);
workers.add(woker);
woker.start();
}else{
taskQueue.put(task);
}
}
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
//当任务不为空时,执行任务
//当任务为空时,从阻塞队列拿出任务执行
// while (task!=null || (task = taskQueue.take())!=null){
while (task!=null || (task = taskQueue.poll(timeout,timeUnit))!=null){
try{
log.debug("执行任务,task:{}",task);
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
task = null;
}
}
synchronized (this){
log.debug("移除任务,task:{}",this);
workers.remove(this);
}
}
}
}
3.1.3、测试
@Slf4j
public class MyThreadPool {
public static void main(String[] args) {
ThreadPool pool = new ThreadPool(2,1000,TimeUnit.MILLISECONDS,3);
for (int i=0;i<7;i++){
int j = i;
pool.execute(()->{
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("任务内容:{}",j);
});
}
}
}
##测试结果:
[main] DEBUG XXX - 新增任务,task:yyy184f6be2, worker:Thread[Thread-0,5,main]
[main] DEBUG XXX - 新增任务,task:yyy2d8f65a4, worker:Thread[Thread-1,5,main]
[main] DEBUG BQ - 新增任务到阻塞队列,task:yyy646d64ab
[main] DEBUG BQ - 新增任务到阻塞队列,task:yyy59e5ddf
[main] DEBUG BQ - 新增任务到阻塞队列,task:yyy536aaa8d
[main] DEBUG BQ - 等待...新增任务到阻塞队列,task:yyye320068
[Thread-0] DEBUG XXX - 执行任务,task:yyy184f6be2
[Thread-1] DEBUG XXX - 执行任务,task:yyy2d8f65a4
[Thread-0] DEBUG MTP - 任务内容:0
[Thread-0] DEBUG XXX - 执行任务,task:yyy646d64ab
[main] DEBUG BQ - 新增任务到阻塞队列,task:yyye320068
[main] DEBUG BQ - 等待...新增任务到阻塞队列,task:yyy76f2b07d
[Thread-1] DEBUG MTP - 任务内容:1
[Thread-1] DEBUG XXX - 执行任务,task:yyy59e5ddf
[main] DEBUG BQ - 新增任务到阻塞队列,task:yyy76f2b07d
[Thread-0] DEBUG MTP - 任务内容:2
[Thread-0] DEBUG XXX - 执行任务,task:yyy536aaa8d
[Thread-1] DEBUG MTP - 任务内容:3
[Thread-1] DEBUG XXX - 执行任务,task:yyye320068
[Thread-0] DEBUG MTP - 任务内容:4
[Thread-0] DEBUG XXX - 执行任务,task:yyy76f2b07d
[Thread-1] DEBUG MTP - 任务内容:5
[Thread-1] DEBUG XXX - 移除任务,task:Thread[Thread-1,5,main]
[Thread-0] DEBUG MTP - 任务内容:6
[Thread-0] DEBUG XXX - 移除任务,task:Thread[Thread-0,5,main]
3.1.4、优化:新增带超时的阻塞队列添加
##阻塞队列BlockingQueue新增 方法:带超时时间的阻塞队列添加
//带超时时间的阻塞添加
public boolean add(T t,long timeout,TimeUnit unit){
lock.lock();
try {
long nanos = unit.toNanos(timeout);
while(queue.size()==capacity){
try {
if(nanos<0){
return false;
}
log.debug("等待...新增任务到阻塞队列,task:{}",t);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("新增任务到阻塞队列,task:{}",t);
queue.addLast(t);
emptyWaitSet.signal();
return true;
}finally {
lock.unlock();
}
}
3.1.5、优化:拒绝策略
##如果阻塞队列满了怎么办?
·1)死等:上面的put()方法
·2)带超时等待:上面的add()方法
·3)让调用者放弃任务执行
·4)让调用者直接抛出异常
·5)让调用者自己执行任务
##修改方法,将执行任务的接口放开,让调用者自己选择处理方式
拒绝策略接口:
//拒绝策略
@FunctionalInterface
interface rejectPolicy<T>{
void reject(BlockingQueue<T> queue,T task);
}
线程池新增方法:
//线程池
@Slf4j
class ThreadPool{
//决绝策略
private RejectPolicy<Runnable> rejectPolicy;
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapacity,RejectPolicy rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
taskQueue = new BlockingQueue<>(queueCapacity);
this.rejectPolicy = rejectPolicy;
}
//执行任务
public void execute(Runnable task){
//当任务数没有超过coreSize时,直接执行
//当任务数超过coreSize时,存储到任务队列taskQueue
synchronized (workers){
if(workers.size()<coreSize){
Worker woker = new Worker(task);
log.debug("新增任务,task:{}, worker:{}",task,woker);
workers.add(woker);
woker.start();
}else{
//死等
//taskQueue.put(task);
//决绝策略
taskQueue.tryPut(rejectPolicy,task);
}
}
}
}
阻塞队列新增方法:
//阻塞队列
@Slf4j
class BlockingQueue<T>{
//阻塞添加--拒绝策略
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
//队列已满
if(queue.size()==capacity){
rejectPolicy.reject(this,task);
}else{
//队列未满直接新增
queue.addLast(task);
}
}finally {
lock.unlock();
}
}
}
测试:
@Slf4j
public class MyThreadPool {
public static void main(String[] args) {
//ThreadPool pool = new ThreadPool(2,1000,TimeUnit.MILLISECONDS,3);
ThreadPool pool = new ThreadPool(2,1000,TimeUnit.MILLISECONDS,3,(queue,task)->{
//1)死等
//queue.put(task);
//2)带超时的等待
//queue.add(task,1000,TimeUnit.MILLISECONDS);
//3)让调用者放弃任务执行
//log.debug("自动放弃任务,task{}",task);
//4)让调用者直接抛出异常
//throw new RuntimeException("直接抛出异常,task:"+task);
//5)让调用者自己执行任务
task.run();
});
for (int i=0;i<7;i++){
int j = i;
pool.execute(()->{
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("任务内容:{}",j);
});
}
}
}
3.2、ThreadPoolExecutor
3.2.1、线程池状态
·ThreadPoolExecutor使用int的高三位表示线程池状态,低29位表示线程数量
·线程状态和线程数量存储在一个原子变量ctl中,目的是将线程池状态与线程个数合二为一,
这样就可以用一次CAS原子操作进行赋值
状态名 | 高3位 | 接受新任务 | 处理阻塞队列任务 | 说明 |
---|---|---|---|---|
RUNNING | 111 | Y | Y | |
SHUTDOWN | 000 | N | Y | 不会接受新任务,但会处理阻塞队列剩余任务 |
STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列任务 |
TIDYING (整理) | 010 | - | - | 任务执行完毕,活动线程为0,即将进入终结 |
TERMINATED | 011 | - | - | 终结状态 |
3.2.2、构造方法
1>ThreadPoolExecutor自带:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
名称 | 作用 |
---|---|
corePoolSize | 核心线程数目(最多保留的线程数) |
maximumPoolSize | 最大线程数(核心线程 + 救急线程) |
keepAliveTime | 生存时间(针对急救线程) |
unit | 时间单位(针对急救线程) |
workQueue | 阻塞队列 |
threadFactory | 线程工厂(可以在线程创建时起个好名) |
handler | 拒绝策略 |
##工作方式:
·线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程(核心线程)
来执行任务
·当线程数达到corePoolSize并没由线程空闲时,再加入新任务,新加的任务会被加入
workQueue阻塞队列排队,等待空闲的线程(空闲的核心线程)
·如果队列选择了有界队列,那么任务超过了队列大小时,会创建
maximumPoolSize-corePoolSize数目的线程来救急(救急线程)
·如果线程到达maxumumPoolSize仍然有新任务这是会执行拒绝策略。拒绝策略JDK提供了4种
实现,其他框架也提供了实现:
>AbortPolicy让调用者抛出RejectExecutionException异常,这是默认策略
>CallerRunsPolicy让调用者执行任务
>DiscardPolicy放弃本次任务
>DiscardOldestPolicy放弃队列中最早的任务,本任务取而代之
·当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束以节省资源,
这个时间有keepAliveTime和unit来控制
·核心线程执行完任务不会主动的结束自己,仍在运行
2>Executors工具类:
根据上述构造方法,JDK Executors类中提供了众多工厂方法来创建各种用途的线程池:
newFixedThreadPool :
#①、 newFixedThreadPool(int nThreads)
特点:核心线程数 == 最大线程数,没有救急线程被创建,也无需超时
阻塞队列是无边界的,可以放任意数量的任务
【适用于任务量已知,相对耗时的任务】
newCachedThreadPool :
#②、newCachedThreadPool()
特点:核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,
意味着:
全都是救急线程(空闲60s后可以回收)
救急线程可以无限创建
队列采用了SynchronousQueue实现特点是,它没有容量,
它在没有线程来取时任务是放不进去的(现拉现吃)
【使用任务数比较密集,但每个任务执行时间较短】
newSingleThreadExecutor :
#③、newSingleThreadExecutor()
特点:只有1个核心线程,没有救急线程,也无需超时
阻塞队列是无边界的,可以放任意数量的任务
【希望多个任务排队执行】
与自己只创建一个线程执行任务的区别:
自己创建一个单线程串行执行任务,如果任务执行失败或者抛出异常而终止那么没有任何补救措施
而线程池还会充县创建一个线程保证池的正常工作,后续任务仍会照常执行
3.2.3、线程池提交任务
//执行任务---无返回结果
void execute(Runnable command);
//提交任务task,返回值Future获得任务执行结果
<T> Future<T> submit(callable<T> task);
//提交tasks中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
//提交tasks只能够所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit);
//提交tasks只能够所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其他任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
//提交tasks只能够所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其他任务取消
//带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
测试execute:
private static void execute(){
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(()->{
log.debug("execute ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
##结果
14:11:24.523 [pool-1-thread-1] DEBUG - execute ...
测试submit
private static void submit() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<String> future = executorService.submit(()->{
log.debug("submit...");
Thread.sleep(1000);
return "ok";
});
log.debug("future:{}",future.get());
}
##结果
14:12:49.785 [pool-1-thread-1] DEBUG - submit...
14:12:50.787 [main] DEBUG - future:ok
测试invokeAll
private static void invokeAll() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
List<Future<Object>> futureList = executorService.invokeAll(Arrays.asList(
() -> {
log.debug("begin 1");
Thread.sleep(1000);
return "ok 1";
},
() -> {
log.debug("begin 2");
Thread.sleep(3000);
return "ok 2";
},
() -> {
log.debug("begin 3");
Thread.sleep(500);
return "ok 3";
}
));
futureList.forEach(t->{
try {
log.debug("result : {}",t.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
##结果
14:29:57.558 [pool-1-thread-2] DEBUG xxx - begin 2
14:29:57.558 [pool-1-thread-1] DEBUG xxx - begin 1
14:29:58.561 [pool-1-thread-1] DEBUG xxx - begin 3
14:30:00.561 [main] DEBUG xxx - result : ok 1
14:30:00.562 [main] DEBUG xxx - result : ok 2
14:30:00.562 [main] DEBUG xxx - result : ok 3
invokeAny测试
private static void invokeAny(){
ExecutorService executorService = Executors.newFixedThreadPool(2);
String res = executorService.invokeAny(Arrays.asList(
() -> {
log.debug("begin 1");
Thread.sleep(1000);
return "ok 1";
},
() -> {
log.debug("begin 2");
Thread.sleep(3000);
return "ok 2";
},
() -> {
log.debug("begin 3");
Thread.sleep(500);
return "ok 3";
}
));
log.debug("result : {}",res);
}
##结果
14:36:50.198 [pool-1-thread-1] DEBUG xxx - begin 1
14:36:50.198 [pool-1-thread-2] DEBUG xxx - begin 2
14:36:51.200 [pool-1-thread-1] DEBUG xxx - begin 3
14:36:51.200 [main] DEBUG xxx - result : ok 1
3.2.4、关闭线程池
/**
* 线程池状态变为SHUTDOWN
* - 不会接收新任务
* - 但已提交任务会执行完
* - 此方法不会阻塞调用线程的执行
**/
void shutdown();
/**
* 线程池状态变为STOP
* - 不会接收新任务
* - 会将队列中的任务返回
* - 并用interrupt的方式中断正在执行的任务
**/
List<Runnable> shutdownNow();
//其他方法
//只要不是RUNNING状态,此方法就会返回TRUE
boolean isShutdown();
//线程池状态是否是TERMINATED
boolean isTerminated();
//调用shutdown后,由于调用线程并不会等待所有任务结束,
//因此如果它想在线程池TERMINATED后做些事情,可以利用此方法等待
boolean awaitTermination(long timeout,TimeUnit unit);
3.3、设计模式:工作线程
##定义:
·让有限的工作线程来轮流异步处理无限多的任务,也可以将其归类为分工模式。
它的典型实现就是线程池,也体现了经典设计模式中的享元模式
·例如,海底捞服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属
服务员,成本就太高了。
·【不同任务类型应该使用不同的线程池】,这样能够避免饥饿,并能提升效率。
·例如,如果参观的工人纪要招呼客人(task A),又要到后出做饭(task B)显然效率不咋地,
分成服务员(线程池A)与厨师(线程池B)更为合理。
##饥饿:
·固定大小线程池会有饥饿现象(线程数量不足导致的)
>两个工人都是同一个线程池中的两个线程
>他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
>>客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
>>后厨做菜:做就是了
·比如工人A处理了点餐任务,接下来他要等着工人B把菜做好,然后上菜
·但现在同时来了两个客人,这个时候工人A和工人B都去处理点餐了,这时没人做饭,死锁
问题:
@Slf4j
public class ThreadPoolHungry {
public static void main(String[] args) {
//两个服务员--又点餐又做菜
ExecutorService executorService = Executors.newFixedThreadPool(2);
//服务员1--服务客人1
executorService.execute(()->{
log.debug("1号客人点餐:宫保鸡丁");
Future<String> future = executorService.submit(() -> {
log.debug("为1号客人做菜");
return "宫保鸡丁";
});
try {
log.debug("为1号客人上菜:{}",future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
//服务员2--服务客人2
executorService.execute(()->{
log.debug("2号客人点餐:糖醋里脊");
Future<String> future = executorService.submit(() -> {
log.debug("为2号客人做菜");
return "糖醋里脊";
});
try {
log.debug("为2号客人上菜:{}",future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
}
}
##结果:
16:03:12.593 [pool-1-thread-1] DEBUG xxx - 1号客人点餐:宫保鸡丁
16:03:12.593 [pool-1-thread-2] DEBUG xxx - 2号客人点餐:糖醋里脊
解决:
@Slf4j
public class ThreadPoolHungrySolve {
public static void main(String[] args) {
//两个服务员--又点餐又做菜
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
ExecutorService chefPool = Executors.newFixedThreadPool(1);
//服务员1--服务客人1
waiterPool.execute(()->{
log.debug("1号客人点餐:宫保鸡丁");
Future<String> future = chefPool.submit(() -> {
log.debug("为1号客人做菜");
return "宫保鸡丁";
});
try {
log.debug("为1号客人上菜:{}",future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
//服务员2--服务客人2
waiterPool.execute(()->{
log.debug("2号客人点餐:糖醋里脊");
Future<String> future = chefPool.submit(() -> {
log.debug("为2号客人做菜");
return "糖醋里脊";
});
try {
log.debug("为2号客人上菜:{}",future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
}
}
##结果:
16:08:27.927 [pool-1-thread-1] DEBUG xxx - 1号客人点餐:宫保鸡丁
16:08:27.930 [pool-2-thread-1] DEBUG xxx - 为1号客人做菜
16:08:27.930 [pool-1-thread-1] DEBUG xxx - 为1号客人上菜:宫保鸡丁
16:08:27.931 [pool-1-thread-1] DEBUG xxx - 2号客人点餐:糖醋里脊
16:08:27.932 [pool-2-thread-1] DEBUG xxx - 为2号客人做菜
16:08:27.932 [pool-1-thread-1] DEBUG xxx - 为2号客人上菜:糖醋里脊
3.4、线程池创建多少线程合适
·创建线程过少导致程序不能充分地利用资源,容易导致饥饿
·创建线程过多导致更多的线程上下文切换(时间片),占用更多内存
##CPU密集型运算
通常采用【CPU核数+1】能够实现最优的CPU利用率
+1是保证当线程由于也确实故障或其他原因导致暂停时,额外的这个线程就能顶上去,
保证CPU时钟周期不被浪费
##IO密集型运算
CPU不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用CPU资源,当执行IO操作时,远程RPC调用时,包括进行数据库操作时,这时CPU就闲下来了,可以利用多线程提高它的利用率
经验公式如下:
【线程数 = CPU核数 * 期望CPU利用率 * 总时间(CPU利用时间+等待时间)/ CPU计算时间】
3.5、任务调度线程池
3.5.1、Timer的缺点
在【任务调度线程池】功能加入之前,可以使用java.util.Timer来实现定时功能,Timer的
有点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,
同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
@Slf4j
public class TimerScheduleTask {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("任务1");
int i = 1/0;
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("任务2");
}
};
timer.schedule(task1,1000);
timer.schedule(task2,1000);
}
}
##结果:
16:35:18.227 [Timer-0] DEBUG - 任务1
Exception in thread "Timer-0" java.lang.ArithmeticException: / by zero
at com.lee.juc3.TimerScheduleTask$1.run(TimerScheduleTask.java:17)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
##任务1的异常,影响了任务2的执行
3.5.2、ScheduledThreadPoolExecutor
//常用方法Excutors
//任务调度
schedule(任务,初始延时,时间单位);
//以固定的速率执行任务,每隔x个时间间隔1执行一次
//如果任务执行需要3s,时间间隔1是1s,那么两个任务之间的间隔是3s
scheduleAtFixedRate(任务,初始延时,时间间隔1,时间单位);
//以固定的速率执行任务,每隔x个时间间隔2执行一次
//如果任务执行需要3s,时间间隔2是1s,那么两个任务之间的间隔是4s
scheduleWithFixedRate(任务,初始延时,时间间隔2,时间单位);
@Slf4j
public class ScheduledThreadPool {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.schedule(()->{
log.debug("任务1");
int i=1/0;
},1, TimeUnit.SECONDS);
pool.schedule(()->{
log.debug("任务2");
},1, TimeUnit.SECONDS);
}
}
##结果:
16:41:26.084 [pool-1-thread-1] DEBUG - 任务1
16:41:26.087 [pool-1-thread-1] DEBUG - 任务2
##任务1的异常没有影响任务2的执行,但任务1的异常没有打印
正确处理线程池异常:
//自己处理try-catch
pool.schedule(()->{
log.debug("任务1");
try{
int i=1/0;
}catch(Exception e){
log.error("error:{}",e);
}
},1, TimeUnit.SECONDS);
//使用Future
Future<Boolean> f = pool.submit(()->{
log.debug("任务1");
try{
int i=1/0;
}catch(Exception e){
log.error("error:{}",e);
}
return true;
},1, TimeUnit.SECONDS);
//future会自己抛出异常
log.debug("result:{}",f.get());
3.5.3、定时任务应用
##每周四18:00:00定时执行任务
/**
* 定时任务:每周四 18:00:00执行任务
*/
@Slf4j
public class ScheduleTask {
public static void main(String[] args) {
//获取当前时间
LocalDateTime nowTime = LocalDateTime.now();
//获取周四时间
LocalDateTime thursdayTime = nowTime.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
//如果当前时间大于周四获取下周四
if(nowTime.compareTo(thursdayTime)>0){
thursdayTime = thursdayTime.plusWeeks(1);//增加一个星期
}
//获取时间差转化成毫秒
long initialDelay = Duration.between(nowTime, thursdayTime).toMillis();
//每隔1星期执行一次
long period = 1000*60*60*24*7;
//定时任务
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleWithFixedDelay(() -> {
try {
log.debug("定时任务");
} catch (Exception e) {
log.error("error:{}",e);
}
},initialDelay,period, TimeUnit.MILLISECONDS);
}
}
3.6、Fork-Join
##一、概念:
Fork-join是JDK7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的
CPU密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归
相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解
Fork-join在分治的基础上加入了多线程,可以把每个人物的分解和合并交给不同的线程来完成。
Fork-join默认会创建于CPU核心数大小相同的线程池
##二、使用:
提交给Fork-join的任务必须要继承RecursiveTask(有返回值)或RecursiveAction(无返回
值)两个类
@Slf4j
public class ForkJoinTest {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
Integer res = pool.invoke(new AddTask(5));
log.debug("res:{}",res);
//拆分
//AddTask(5)=5+AddTask(4)
//AddTask(4)=4+AddTask(3)
//...
//AddTask(2)=2+AddTask(1)
//AddTask(1)=1
}
}
//计算1+2+3~+N
@Slf4j
class AddTask extends RecursiveTask<Integer>{
private Integer n;
public AddTask(Integer n) {
this.n = n;
}
@Override
protected Integer compute() {
if(n==1){
return 1;
}
AddTask addTask = new AddTask(n - 1);
addTask.fork();//将addTask任务交给新的线程
Integer res = n+addTask.join();//获取任务结果
return res;
}
}