JUC併發編程之:簡單概述(五)

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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章