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;
}
}