Concurrent包第一部分

**

一、Concurrent概述

**
  Concurrent包是jdk5中開始提供的一套併發編程包,其中包含了大量和多線程開發相關的工具類,大大的簡化了java的多線程開發,在高併發及分佈式場景下應用廣泛。Concurrent是java.util下的包。

二、BlockingQueue 阻塞式隊列

1、概述
  阻塞式隊列是一種隊列數據結構,和其他隊列比起來,多了阻塞機制,從而可以在多個線程之間進行存取隊列的操作,而不會有線程併發安全問題.所以稱之爲阻塞式隊列。
  可以簡單的理解爲,阻塞式隊列是專門用來在多個線程間通過隊列共享數據。
在這裏插入圖片描述

` 在阻塞式隊列中,(1)如果隊列滿了,仍然有線程向其中寫入數據,則這次寫入操作會被阻塞住,直到有另外的線程從隊列中消費了數據,隊列有了空間,阻塞纔會被放開,寫入操作纔可以執行;(2)同理,如果隊列是空的,仍然有線程從隊列中獲取數據,則讀取操作將會被阻塞住,直到有另外的線程向隊列中寫入了數據,隊列不再爲空,阻塞纔會被放開,讀取操作纔可以執行。
  可以發現:阻塞式隊列通過阻塞機制,協調了多個線程在一個隊列上的讀寫操作。

2、繼承結構
|java.util.concurrent
|接口 BlockingQueue
  |java.util.concurrent
  |類 ArrayBlockingQueue
  |java.util.concurrent
  |類 LinkedBlockingQueue

其中,
  ArrayBlockingQueue是一個有界的阻塞隊列,底層是數組。有界也就意味着,它不能存儲無限多的元素。它有一個同一時間能夠存儲元素數量的上限。你可以在對其初始化的時候設定這個上限,但之後就無法對這個上限進行修改了。其內部以FIFO(先進先出)的順序對元素進行存儲。
  LinkedBlockingQueue 底層是鏈表,可以在創建時指定大小,也可以不指定,則將使用Integer.MAX_VALUE作爲上限。
  DelayQueue對元素進行持有直到一個特定的延遲到期,注意其中的元素必須實現Delayed接口。DelayQueue將會在每個元素的getDelay()方法返回的值的時間段之後才釋放掉該元素。如果返回的是0或者負數,延遲將被認爲過期,該元素將會在DelayQueue的下一次take被調用時被釋放。
  SynchronousQueue是一個特殊的隊列,它的內部同時只能夠容納單個元素。如果該隊列已有一元素的話,試圖向隊列中插入一個新元素的線程將會阻塞,直到另一個線程將該元素從隊列中抽走。同樣,如果該隊列爲空,試圖向隊列中抽取一個元素的線程將會阻塞,直到另一個線程向隊列中插入了一條新的元素。

3、重要方法

拋出異常 特殊值 阻塞 超時
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
檢查 element() peek() 不可用 不可用

上表解釋:
  在BlockingQueue中的插入、移除和檢查數據有四組方法,這四組方法在處理隊列滿時的插入操作和隊列空時的獲取操作會有不同的處理機制。
分別爲:
   拋出異常
   返回特殊值
   產生阻塞 (阻塞具有超時時間,一旦超過指定時間,阻塞自動放開)
 在使用BlockingQueue過程中 可以根據需要選擇對應的方法。

4、以put()、take()爲例:

 	public class BlockingQueue1 {
        public static void main(String[] args) throws Exception {
                //1.創建阻塞式隊列
                BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
                //2.存入數據
                new Thread(new Producer(queue)).start();
                new Thread(new Consumer(queue)).start();
        }
}
 //消費者
class Consumer implements Runnable{
        private BlockingQueue<String> queue = null;  
        public Consumer(BlockingQueue<String> queue) {
                this.queue = queue;
        }
        @Override
        public void run() {
                try {
                        while(true){
                                      Thread.sleep(2000);//2秒
                                String s = queue.take();//移除
                                System.out.println("消費者消費了"+s);
                        }
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }
}
 //生產者
class Producer implements Runnable{
        private BlockingQueue<String> queue = null;  
        public Producer(BlockingQueue<String> queue) {
                this.queue = queue;
        }
        @Override
        public void run() {
                try {
                        int i = 0;
                        while(true){
                                Thread.sleep(5000);//5秒
                                int n = ++i;
                                queue.put("a"+n);//插入
                                System.out.println("生產者生產了a"+n);
                        }
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }
}

三、ConcurrentMap 同步map

1、ConcurrentMap概述
ConcurrentMap是一個線程安全的Map,可以防止多線程併發安全問題。

2、ConcurrentMap與HashTable的比較:
HashTable也是線程安全的,但是ConcurrentMap性能要比HashTable好的多,所以推薦使用ConcurrentMap。原因如下:
(1)ConCurrentMap的鎖更加精細
  HashTable加鎖是鎖在整個HashTable上,一個線程操作時其他線程無法操作,性能比較低;ConcurrentMap加鎖是將鎖加在數據分段(桶)上,只鎖正在操作的部分數據,所以效率高。
(2)ConCurrentMap引入了讀寫鎖機制
  在多線程併發操作的過程中,多個併發的讀其實不需要隔離,但只要有任意一個寫操作,就必須隔離。HashTable沒有考慮一點,無論什麼類型的操作,直接在整個HashTable上加鎖。ConcurrentMap則區分了讀寫操作,讀的時候加讀鎖,寫的時候加寫鎖,讀鎖和讀鎖可以共存,寫鎖和任意鎖都不能共存,從而實現了在多個讀的過程中不會隔離 提高了效率。
  補充:
  Hashtable的最大特點是散列,注意他不是唯一的,只是重複的概率極低。

3、ConcurrentMap的繼承結構
java.util.concurrent
接口 ConcurrentMap<K,V>
  java.util.concurrent
  類 ConcurrentHashMap<K,V>
  
4、涉及代碼

ConcurrentMap<String,String>map = new ConcurrentHashMap<>();//創建concurrentMap
map.put("name", "zs");//存入數據
map.put("addr","bj");
System.out.println(map.get("name"));//取出數據
System.out.println(map.get("addr"));

四、CountDownLatch 閉鎖

1、閉鎖概述
java.util.concurrent.CountDownLatch是一個併發構造,實現協調某個線程阻塞直到其他若干線程執行達到一定條件才放開阻塞繼續執行的效果。

2、重要的API
(1)構造方法CountDownLatch(int count) 在構造的過程中直接傳入一個數字作爲閉鎖的計數器的初始值。在閉鎖上調用此方法,可以阻塞當前線程,阻塞到CountDownLatch中的計數器count值變爲0,自動放開阻塞。
(2).await() 調用此方法可以將閉鎖中的計數器數值-1,如果減到零,await的阻塞會自動放開
(3)countDown() 遞減鎖存器的計數,如果計數到達零,則釋放所有等待的線程。

3、代碼
以做飯爲例,需要先執行完買米買菜的線程才能執行做飯的線程。

public class CountDownLatch{
        public static void main(String[] args) {
                //1.創建閉鎖 計數器初始值設置爲3
                CountDownLatch cdl = new CountDownLatch(2);
                   //2.創建線程傳入閉鎖,並執行線程
                new Thread(new MaiMi(cdl)).start();
                new Thread(new MaiCai(cdl)).start();
                new Thread(new ZuoFan(cdl)).start();
        }
}
 
class ZuoFan implements Runnable{
        private CountDownLatch cdl = null;      
        public ZuoFan(CountDownLatch cdl) {
                this.cdl = cdl;
        }
        @Override
        public void run() {
                try {
                        //--調用await等待 達到執行條件
                        cdl.await();
                        System.out.println("開始做飯...");
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
        }
}
class MaiMi implements Runnable{
        private CountDownLatch cdl = null;
        public MaiMi(CountDownLatch cdl) {
                this.cdl = cdl;
        }
        @Override
        public void run() {
                try {
                        Thread.sleep(2000);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
                System.out.println("米買回來了...");
                //--在閉鎖上-1
                cdl.countDown();
        }
}
class MaiCai implements Runnable{
        private CountDownLatch cdl = null;
        public MaiCai(CountDownLatch cdl) {
                this.cdl = cdl;
        }
        @Override
        public void run() {
                try {
                        Thread.sleep(5000);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
                System.out.println("菜買回來了...");
                cdl.countDown();
        }
}

五、ExecutorServcie 執行器服務

1、ExecutorServcie 概述
  ExecutorService是Concurrent包下提供的一個接口,ExecutorService實現就是一個線程池實現。
  所謂的池就是用來重用對象的一個集合,可以減少對象的創建和銷燬,提高效率。
  而線程本身就是一個重量級的對象,線程的創建和銷燬都是非常耗費資源和時間,所以如果需要頻繁使用大量線程,不建議每次都創建線程銷燬線程,所以利用線程池的機制,實現線程對象的共享,提升程序的效率。

2、ExecutorService用法—通過ThreadPoolExecutor實現類創建線程池
(1)創建

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, 
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, 
RejectedExecutionHandler handler) 

用給定的初始參數和默認的線程工廠創建新的 ThreadPoolExecutor。
(2)各個參數的意義
  int corePoolSize—核心池的大小,就是正式線程的數量和
  int maximumPoolSize—最大池大小,正式線程數量和臨時線程的總數
  long keepAliveTime—空閒的多餘線程保持時間,臨時線程不執行能夠存活的時間
  TimeUnit unit,—時間單位
  BlockingQueue workQueue—WQ(n)—阻塞式隊列,正式線程都忙時將線程放到WQ中,WQ大小是n。
  RejectedExecutionHandler handler—拒絕服務助手,當正式線程和臨時線程都在執行,而且WQ中也是滿的時候,就由他來處理後續來的請求。
(3)線程池的工作方式
  a.在線程池剛創建出來時,線程池中沒有任何線程,當有任務提交過來時,如果線程池中管理的線程的數量小於corePoolSize,則無論是否有閒置的線程都會創建新的線程來使用.而當線程池中管理的線程的數量達到了corePoolSize,再有新任務過來時,會複用閒置的線程.
  b.當所有的核心池大小中的線程都在忙碌,則再有任務提交,會存入workQueue中,進行排隊,當核心池大小中的線程閒置後,會自動從workQueue獲取任務執行
  c.而當所有的核心池大小中的線程都在忙碌,workQueue也滿了,則會再去創建新的臨時線程來處理提交的任務,但是,無論如何,總的線程數量,不允許超過maximumPoolSize
  d.而當所有的核心池大小中的線程都在忙碌,workQueue也滿了,也創建了達到了maximumPoolSize的臨時線程,再有任務提交,此時會交予RJHandler來拒絕該任務
  e.當任務高峯過去,workQueue中的任務也都執行完成,線程也依次閒置了下來,則在此時,會將閒置時間超過keepAliveTime(單位爲unit)時長的線程關閉掉,但是關閉時會至少保證線程池中管理的線程的數量 不少於corePoolSize個

3、ExecutorService用法—通過Executors工具類的靜態方法創建線程池
(1)重要的三種API

a. newCachedThreadPool()

創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。

corePoolSize=0
maximumPoolSize = Integer.MaxValue
keepAliveTime = 60
TimeUnit = Seconds

特點:擅長處理大量短任務的線程池。

b.newCachedThreadPool()
  創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。

corePoolSize=1
maximumPoolSize = 1
workQueue = new LinkedBlockingQueue<Runnable>())

特點:實現使用單一線程處理任務,多個任務在無界的阻塞式隊列中排隊等待處理
  
c.newFixedThreadPool(int nThreads)
  創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運
行這些線程

corePoolSize=nTHreads
maximumPoolSize = nTHreads
workQueue = new LinkedBlockingQueue<Runnable>())

特點:可以實現使用指定數量的線程處理任務,多個任務在無界的阻塞式隊列中排隊等待處理。
  
4、創造線程池代碼

public class ExecutorService {
        public static void main(String[] args) {
                //1.手動創建線程池
                ExecutorService s1 = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5)
                                , new RejectedExecutionHandler() {
                        @Override
                        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                System.err.println("拒絕了..["+r+"]");
                        }
                });
                
                //2.利用工具類的靜態方法快速創建線程池
                ExecutorService s2 = Executors.newCachedThreadPool();
                ExecutorService s3 = Executors.newSingleThreadExecutor();
                ExecutorService s4 = Executors.newFixedThreadPool(5);
        
        }
}

手動創建線程池的好壞:
 可以自定義,但是設置的參數不一定是運行效果最優的。

5、向線程池中提交任務
(1)execute(Runnable)
  最普通的提交任務的方法,直接傳入一個Runnable接口的實現類對象,即可要求線程池取執行這個任務,這種方式提交的任務無法監控線程的執行 也無法在線程內向調用者返回返回值.
 實例代碼:

s.execute(new Runnable() {
        @Override
        public void run() {
                for(int i=0;i<10;i++){
                        System.out.println("run.."+i);
                }
        }
});

(2)submit(Runnable)
  向線程池提交一個 Runnable 任務用於執行,並返回一個表示該任務的 Future,可以通過Future對象的get()方法來檢測線程是否執行結束,如果線程未執行結束get()方法將會阻塞。

   Future<?> future = s.submit(new Runnable() {
            @Override
            public void run() {
                    try {
                            for(int i=0;i<10;i++){
                                    System.out.println("run.."+i);
                            }
                            Thread.sleep(5000);
                            System.out.println("Runnable結束了..");
                    } catch (Exception e) {
                            e.printStackTrace();
                    }
            }
    });
    future.get();
    System.out.println("main線程結束...");

(3)submit(Callable)
  和上面submit(Runnable)方法非常類似,只不過這個方法傳入的是Callable接口的實現類,Callable接口功能和Runnable接口基本一致,唯一的不同在於,內部的方法叫call,且可以返回返回值,這個返回值可以通過Future對象通過get()方法得到

Future<String> future = s.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
                System.out.println("子線程開始執行了..");
                Thread.sleep(2000);
                System.out.println("子線程執行結束了..");
                return "111";
        }
});
String str = future.get();
System.out.println(str);

(4)invokeAny(Collection<? extends Callable<T>> tasks)
可以接收若干Callable組成的集合,此方法將會自動從中選擇任意一個執行

List<Callable> list = new ArrayList<>();
list.add(new Callable<String>() {
        @Override
        public String call() throws Exception {
                Thread.sleep(2000);
                return "111";
        }
});
list.add(new Callable<String>() {
        @Override
        public String call() throws Exception {
                Thread.sleep(2000);
                return "222";
        }
});
String str = s.invokeAny((Collection<? extends Callable<String>>) list);
System.out.println(str);

(5)invokeAll(Collection<? extends Callable<T>> tasks)

可以接收若干Callable組成的集合,此方法將會執行所有的Callable將結果組成集合返回

List<Callable> list = new ArrayList<>();
list.add(new Callable<String>() {
        @Override
        public String call() throws Exception {
                Thread.sleep(2000);
                return "111";
        }
});
list.add(new Callable<String>() {
        @Override
        public String call() throws Exception {
                Thread.sleep(2000);
                return "222";
        }
});
List<Future<String>> rs = s.invokeAll((Collection<? extends Callable<String>>) list);
for(Future<String> f : rs){
        System.out.println(f.get());
}

6、關閉線程池
  線程池中維護了大量線程,很耗費資源,所以當使用線程池結束時,應該手動關閉線程池,釋放資源。雖然放在main函數中的線程運行完了,但是new的線程運行完放在線程池中了,並沒有銷燬,所以進程並不會停止。 
  
重要的API:
  (1)shutdown()
  即使調用也不會立即關閉所有線程,而是不再接收新的任務,之前已經提交但尚未完成執行的線程仍然會繼續執行,直到所有的任務都執行完,線程池關閉所有線程,退出.
  (2)shotdownNow()
  調用此方法時,會立即關閉所有線程,退出線程池,這種方式雖然可以立即推出線程池,但是正在執行的線程有可能被意外的中斷,造成意想不到的問題。

六、Lock

1、Lock鎖概述
java.util.concurrent.locks.Lock 是一個類似於synchronized 塊的線程同步機制.但是 Lock比 synchronized 塊更加靈活。Lock是個接口,有個實現類是ReentrantLock

2、重要方法

方法 功能
ReentrantLock() 創建一個 ReentrantLock 的實例。
ReentrantLock(boolean fair) //先來的先得資源 創建一個具有給定公平策略的 ReentrantLock。
lock() 獲取鎖
unlock() 試圖釋放鎖

3、lock和syncronized的區別
(1)lock本身就是鎖,不需要syncronized尋找指定鎖對象。
(2)lock可以配置公平策略,實現線程按照先後順序獲取鎖。
(3)提供了trylock方法 可以試圖獲取鎖,獲取到或獲取不到時,返回不同的返回值 讓程序可以靈活處理。
(4)lock()和unlock()可以在不同的方法中執行,可以實現同一個線程在上一個方法中lock()在後續的其他方法中unlock(),比syncronized靈活的多。

4、讀寫鎖
  對於多線程併發安全問題,其實只在涉及到併發寫的時候纔會發生,多個併發的讀並不會有線程安全問題,所以在Concurrent包中提供了讀寫鎖的機制,可以實現,分讀鎖和寫鎖來進行併發控制.多個讀鎖可以共存,而寫鎖和任意鎖都不可共存,從而實現多個併發讀並行執行提升效率,而任意時刻寫都進行隔離,保證安全.這是一種非常高效而精細的鎖機制。

(1)繼承結構
  java.util.concurrent.locks
  接口 ReadWriteLock
    java.util.concurrent.locks
    類 ReentrantReadWriteLock
  
(2)重要的API

方法 功能
ReentrantReadWriteLock() 使用默認(非公平)的排序屬性創建一個新的 ReentrantReadWriteLock
ReentrantReadWriteLock(boolean fair) 使用給定的公平策略創建一個新的 ReentrantReadWriteLock
ReentrantReadWriteLock.ReadLock readLock() 返回用於讀取操作的鎖。
ReentrantReadWriteLock.WriteLock writeLock() 返回用於寫入操作的鎖。

補充:
  併發安全問題的三個條件:共享資源+寫操作+多線程操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章