為什么很多大廠喜歡問并發編程呢?因為并發編程是開發人員的一個分水嶺。很多好幾年開發經驗的開發人員可能也沒有實際的并發編程經驗,要么就是在一些沒有挑戰性的中臺實現了所謂的分布式鎖,但是沒有并發量去考驗,要么就是笑著說其實工作中用不上,這些開發人員后面會逐漸被AI淘汰,CURD的東西花這么多錢請你們干嘛呢?為什么不直接請個便宜的應屆生呢?鍛煉一兩年絕對不比這些開發人員差。因此,努力越過分水嶺,往架構組件的能力出發吧。這篇文章將會是你的出發點,這里會詳細介紹JDK 的并發包的原理及使用方法。
J.U.C并發包,即java.util.concurrent包,是JDK的核心工具包,是JDK1.5之后,由 Doug Lea實現并引入。
整個java.util.concurrent包,按照功能可以大致劃分如下:
課程J.U.C,分析所有基于的源碼為Oracle JDK1.8
多線程概念介紹
線程簡述: 線程是進程的執行單元,用來執行代碼。
為什么使用多線程?
多線程有什么用?這里舉例說明:
比如看學習視頻時候:我們在看視頻的同時,還可以聽到聲音,還可以看到廣告以及彈幕,這里至少用到四個線程,當其中一個線程卡死如放不了彈幕不影響播放廣告。
如上圖,要達到并行執行的效果,這里就要用到多線程。
線程調度
計算機通常只有一個CPU時,在任意時刻只能執行一條計算機指令,每一個進程只有獲得CPU的使用權才能執行指令。所謂多進程并發運行,從宏觀上看,其實是各個進程輪流獲得CPU的使用權,分別執行各自的任務。那么,就會有多個線程處于就緒狀態等到CPU,JVM就負責了線程的調度。JVM采用的是搶占式調度,沒有采用分時調度,因此可能造成多線程執行結果的的隨機性。
說明:在單核CPU中,同一個時刻只有一個線程執行,根據CPU時間片算法依次為每個線程服務,這就叫線程調度。
目標: 線程等待和喚醒使用。
介紹
等待和喚醒:通常是兩個線程之間的事情,一個線程等待,另外一個線程負責喚醒
等待和喚醒
Object類:
public final void wait(); // 導致當前線程等待public final native void wait(long timeout) throws InterruptedException;public final native void notify(); // 喚醒正在等待的單個線程public final native void notifyAll(); // 喚醒正在等待的全部線程
注意:wait和notify必須是在同步代碼塊中,使用鎖對象調用
使當前線程阻塞
喚醒正在等待的單個線程
喚醒所有等待(對象的)線程,哪一個線程將會第一個處理取決于操作系統的實現。
因為wait和notify需要使用鎖對象來調用,而任何對象都可以作為鎖,所以放在Object類中。
1、wait()、notify()、notifyAll() 方法是Object的本地final方法,子類無法被重寫。
2、wait() 使當前線程阻塞,前提是必須先獲得鎖,一般配合synchronized 關鍵字使用。
即,一般在 synchronized 同步代碼塊里使用 wait()、notify、notifyAll() 方法。
3、由于 wait()、notify()、notifyAll() 方法在 synchronized 代碼塊執行,說明當前線程一定是獲取了鎖的。當線程執行wait()方法時候,會釋放當前的鎖,然后讓出CPU,進入等待狀態。只有當 notify()/notifyAll() 被執行時候,才會喚醒一個或多個正處于等待狀態的線程,然后繼續往下執行,直到執行完 synchronized 代碼塊的代碼或是中途遇到wait(),再次釋放鎖。也就是說,notify()/notifyAll() 的執行只是喚醒沉睡的線程,而不會立即釋放鎖,鎖的釋放要看代碼塊的具體執行情況。所以在編程中,盡量在使用了 notify()/notifyAll() 后立即退出臨界區,以喚醒其他線程讓其獲得鎖。
4、notify 和 wait 的順序不能錯,如果A線程先執行notify方法,B線程在執行wait方法,那么B線程是無法被喚醒的。
5、notify 和 notifyAll的區別:
代碼:
package cn.itcast.thread;import java.util.concurrent.TimeUnit;/** * 測試 wait()、notify()、notifyAll() */public class Test1 { public static void main(String[] args) { // 創建對象 Object obj = new Object(); // 線程t1 new Thread(() -> { synchronized (obj) { try { System.out.println(Thread.currentThread().getName() + "wait 前"); obj.wait(); // 等待、線程阻塞(釋放鎖) System.out.println(Thread.currentThread().getName() + "wait 后"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1:").start(); // 線程t2 new Thread(() -> { synchronized (obj) { try { System.out.println(Thread.currentThread().getName() + "wait 前"); obj.wait(); // 等待、線程阻塞(釋放鎖) System.out.println(Thread.currentThread().getName() + "wait 后"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t2:").start(); // 線程t3 new Thread(() -> { synchronized (obj) { try { // 休眠2秒 TimeUnit.SECONDS.sleep(2); //目的讓前2個線程進入等待狀態 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("notifyAll 前"); obj.notifyAll(); // 喚醒全部等待的線程,不會釋放鎖 System.out.println("notifyAll 后"); } }, "t3:").start(); }
小結
導致當前線程等待。(釋放鎖,讓出CPU)
喚醒正在等待的單個線程。(不釋放鎖,不讓出CPU)
目標:理解線程6種狀態以及狀態轉換。
線程狀態
線程可以處于以下狀態之一:
Thread.State 枚舉類中進行了定義。
狀態轉換
代碼:
package cn.itcast.thread;/** 測試: 線程6種狀態 */public class Test2 { // 方法1 (RUNNABLE: 運行) public static void test1() { new Thread(() -> { synchronized (Test2.class) { while (true) { } } }, "t1-runnable").start(); //線程運行中 } // 方法2 (TIMED_WAITING : 超時等待) public static void test2() { new Thread(() -> { synchronized (Test2.class) { while (true) { try { Test2.class.wait(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "t2-timed_waiting").start(); //線程超時等待中 } // 方法3 public static void test3() { // (RUNNABLE: 運行) new Thread(() -> { synchronized (Test2.class) { while (true) { } } }, "t3-runnable").start(); // (BLOCKED: 阻塞) new Thread(() -> { synchronized (Test2.class) { //由于上面沒有釋放鎖,被阻塞中 } }, "t4-BLOCKED").start(); } // 方法4 (WAITING : 等待) public static void test4() { new Thread(() -> { synchronized (Test2.class) { while (true) { try { Test2.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "t5-waiting").start(); //線程等待中 } public static void main(String[] args) { //test1(); // 線程運行中 //test2(); // 線程超時等待中 test3(); // 線程阻塞中 //test4(); // 線程等待中 }}
查看進程堆棧
使用jstack可查看指定進程(pid)的堆棧信息,用以分析線程執行狀態:
目標: 學習如何控制線程執行順序、線程讓步、優先級。
join 作用
– 如果為0表示永遠等待,其實是等到線程結束后。
– 傳入指定的時間會調用wait(millis), 時間到鎖釋放,不再等待。
yield 作用
yield 和 sleep 的異同
線程優先級
線程的優先級說明該線程在程序中的重要性。系統會根據優先級決定首先使用哪個線程,但這并不意味著優先級低的線程得不到運行,只是它運行的機率比較小而已,比如垃圾回收機制。
優先級范圍1-10,默認為5,比如設置最高優先級為10:
t1.setPriority(Thread.MAX_PRIORITY);
代碼:
package cn.itcast.thread;/** * join() : 加入線程 * yield() : 線程讓步 * sleep() : 線程休眠 */public class Test3 { public static void main(String[] args) throws InterruptedException { // 線程1 Thread t1 = new Thread(() -> { for (int i = 1; i <= 20; i++) { System.out.println(Thread.currentThread().getName() + i); // 線程讓步 //Thread.yield(); } },"t1:"); // 線程2 Thread t2 = new Thread(() -> { for (int i = 1; i <= 20; i++) { System.out.println(Thread.currentThread().getName() + i); } },"t2:"); // 啟動線程 t1.start(); // 加入線程 t1.join(); // 設置線程優先級 //t1.setPriority(Thread.MIN_PRIORITY); t2.start(); // 設置線程優先級 //t2.setPriority(Thread.MIN_PRIORITY); }}
小結
加入線程,線程調用了join方法,那么就要一直運行到該線程結束,才會運行其他線程. 這樣可以控制線程執行順序。
線程讓步,讓出CPU的時間片盡量切換其他線程去執行。
設置線程優先級,優先級可以設置1-10,數字越大代表優先級越高。
在Java語言中,每個線程都有一個優先級,當線程調控器有機會選擇新的線程時,線程的優先級越高越有可能先被選擇執行。
并發編程的目的是為了讓程序運行得更快,但是,并不是啟動更多的線程就能讓程序最大限度地并發執行。在進行并發編程時,如果希望通過多線程執行任務讓程序運行得更快,會面臨非常多的挑戰,比如死鎖的問題、上下文切換的問題。
描述
鎖是個非常有用的工具,運用場景非常多,因為它使用起來非常簡單,而且易于理解。但同時它也會帶來一些困擾,那就是可能會引起死鎖,一旦產生死鎖,就會造成系統功能不可用。讓我們先來看一段代碼,這段代碼會引起死鎖,使線程t1 和 線程t2 互相等待對方釋放鎖。
演示
什么是死鎖?多線程競爭共享資源,導致線程相互等待,程序無法向下執行。
執行sleep的時候會讓出CPU,但不是釋放鎖。
package cn.itcast.thread;/** 學習死鎖的概念和解決死鎖 */public class DeadLock { // 定義兩個對象作為鎖 private static Object objA = new Object(); private static Object objB = new Object(); public static void main(String[] args) { // 線程1 Thread t1 = new Thread(() -> { // 同步鎖 synchronized (objA){ try { // 線程休眠(讓出CPU,不釋放鎖) Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("AAAAAAA"); // 同步鎖 synchronized (objB){ System.out.println("BBBBBBB"); } } }); // 線程2 Thread t2 = new Thread(() -> { // 同步鎖 synchronized (objB){ System.out.println("CCCCCCC"); // 同步鎖 synchronized (objA){ System.out.println("DDDDDDD"); } } }); t1.start(); t2.start(); }}
上面的代碼只是演示死鎖的場景,在現實中你可能不會寫出這樣的代碼。但是,在一些更為復雜的場景中,你可能會遇到這樣的問題,比如t1拿到鎖之后,因為一些異常情況沒有釋放鎖(死循環)。又或者是t1拿到一個數據庫鎖,釋放鎖的時候拋出了異常,沒釋放掉。一旦出現死鎖,業務是可感知的,因為不能繼續提供服務了。
查看線程執行情況
打開cmd命令dos窗口輸入如下命令
jps命令:查看Java程序進程id信息
jstack命令:查看指定進程堆棧信息
如何避免死鎖?
現在,我們介紹避免死鎖的幾個常見方法:
小結
1.什么是死鎖: 多線程競爭共享資源,導致線程相互等待,程序無法向下執行。
2.死鎖產生的條件
3.如何避免死鎖: 干掉其死鎖產生的條件中一個條件即可。
多線程一定快嗎?
測試代碼
代碼演示串行和并發執行并累加操作的時間,請分析: 下面的代碼并發執行一定比串行執行快嗎?
package cn.itcast.thread;public class Test4 { // 定義變量 private static final long count = 1000000000; public static void main(String[] args) throws InterruptedException { concurrency(); serial(); } // 定義方法1(使用線程) private static void concurrency() throws InterruptedException { long start = System.currentTimeMillis(); // 創建線程 循環累加 Thread thread = new Thread(() -> { int a = 0; for (long i = 0; i < count; i++) { a += 5; } }); // 開啟線程 thread.start(); // 循環累減 int b = 0; for (long i = 0; i < count; i++) { b--; } long time = System.currentTimeMillis() - start; // thread.join(); System.out.println("concurrency :" + time + "ms,b=" + b); } // 定義方法2 (不用線程) private static void serial() { long start = System.currentTimeMillis(); // 循環累加 int a = 0; for (long i = 0; i < count; i++) { a += 5; } // 循環累減 int b = 0; for (long i = 0; i < count; i++) { b--; } long time = System.currentTimeMillis() - start; System.out.println("serial:" + time + "ms,b=" + b ); }}
測試結果
上述問題的答案是“不一定”,測試結果如表所示:
當并發執行累加操作不超過百萬次時,速度會比串行執行累加操作要慢。那么,為什么并發執行的速度會比串行慢呢?這是因為線程有創建和上下文切換的開銷。
上下文切換
例如:這就像我們同時讀兩本書,當我們在讀一本英文的技術書時,發現某個單詞不認識,于是便打開中英文字典,但是在放下英文技術書之前,大腦必須先記住這本書讀到了多少頁的第多少行,等查完單詞之后,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。
如何減少上下文切換
減少上下文切換的方法有無鎖并發編程、CAS算法、使用最少線程和使用協程。
小結
強烈建議多使用JDK并發包提供的并發原子類和工具類來解決并發問題,因為這些類都已經通過了充分的測試和優化,均可解決了上面提到的幾個挑戰。
什么是JMM內存模型?
Java內存模型(即Java Memory Model,簡稱JMM)。Java內存模型跟CPU緩存模型類似,是基于CPU緩存模型來建立的,Java內存模型是標準化的,屏蔽了底層不同計算機的區別。
JMM本身是一種抽象的概念,并不真實存在,它描述的是一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。由于JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存(有些地方稱為棧空間),用于存儲線程私有的數據。而Java內存模型中規定所有共享變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問。
線程對共享變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝的自己的工作內存空間,然后對變量進行操作,操作完成后再將變量寫回主內存,不能直接操作主內存中的變量,工作內存中存儲著主內存中的變量副本拷貝,前面說過,工作內存是每個線程的私有數據區域,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程如下圖:
關于JMM中的主內存和工作內存說明如下:
主要存儲的是Java實例對象,所有線程創建的實例對象都存放在主內存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),當然也包括了共享的類信息、常量、靜態變量。由于是共享數據區域,多條線程對同一個變量進行訪問可能會發現線程安全問題。
主要存儲當前方法的所有本地變量信息(工作內存中存儲著主內存中的變量副本拷貝),每個線程只能訪問自己的工作內存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執行的是同一段代碼,它們也會各自在自己的工作內存中創建屬于當前線程的本地變量,當然也包括了字節碼行號指示器、相關Native方法的信息。注意由于工作內存是每個線程的私有數據,線程間無法相互訪問工作內存,因此存儲在工作內存的數據不存在線程安全問題。
啟動2個線程,線程A讀取主內存的共享變量數據,之后線程B修改共享變量數據,線程A無法感知:
package cn.itcast.thread;public class Test5 { // 定義flag屬性 private static boolean flag = false; public static void main(String[] args) throws InterruptedException { // 創建線程1 new Thread(() -> { long num = 0; while (!flag){ num++; } // 如果沒有打印,說明當前線程無法感知flag的修改 System.out.println("num = " + num); }).start(); // 休眠1000毫秒 Thread.sleep(1000); // 創建線程2 new Thread(() -> { // 修改flag flag = true; System.out.println("flag = " + flag); }).start(); }}
運行效果:沒有任務打印輸出,上面的線程無法感知flag的修改。
Java并發編程三個特性: 可見性、原子性、有序性。
可見性表示的是,如果有線程更新了某一個共享變量的值,則其它線程要能夠立即感知到最新的內容。如果不能保證可見性,則可能出現類似于數據庫中的臟讀情況。
前文介紹JMM的時候也提到了,如果要保證可見性,那么變量被一個線程修改后,需要將其修改后的最新值同步回主存,然后其它線程要讀取該變量時,需要從主存刷新最新的值到本地內存,就這樣通過主存實現可見性。但是將最新值同步回主存的時機是沒有強制要求的,也不知道其它線程什么時候可能會去從主存刷新最新值,所以普通變量在多線程操作時是保證不了可見性的。
這時有一個比較好使的關鍵字:volatile。JMM對它定義了一些特殊的訪問規則,它能保證修改后的最新值能立即同步到主存,同時,每次使用都從主存刷新。所以volatile能夠保證多線程場景下的可見性。
volatile 介紹
在多線程并發編程中synchronized和volatile都扮演著重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起線程上下文的切換和調度。
volatile 使用
package cn.itcast.thread;/** volatile 實現多線程訪問共享成員時的可見性 */public class Test6 { // volatile 實現多線程訪問共享成員時的可見性. private static volatile boolean flag = false; public static void main(String[] args) throws InterruptedException { // 創建線程1 new Thread(() -> { long num = 0; while (!flag){ num++; } // 如果沒有打印,說明當前線程無法感知flag的修改 System.out.println("num = " + num); }).start(); // 休眠1000毫秒 Thread.sleep(1000); // 創建線程2 new Thread(() -> { // 修改flag flag = true; System.out.println("flag = " + flag); }).start(); }}
在計算機中,它表示的是一個操作,可能包含一個或多個步驟,這些步驟要么全部執行成功要么全部執行失敗,并且執行的過程中不能被其它操作打斷,這類似于數據庫中事務的原子性概念。
數據原子操作,Java 內存模型對主內存與工作內存之間的具體交互協議定義了八種原子操作:
1. lock(鎖定): 作用于主內存的變量,把一個變量標記為一條線程獨占狀態。
2. unlock(解鎖): 作用于主內存的變量,把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
3. read(讀取): 作用于主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用。
4. load(載入): 作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
5. use(使用): 作用于工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎。
6. assign(賦值): 作用于工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量。
7. store(存儲): 作用于工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作。
8. write(寫入): 作用于工作內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中。
如果要把一個變量從主內存中復制到工作內存中,就需要按順序地執行read和load操作,如果把變量從工作內存中同步到主內存中,就需要按順序地執行store和write操作。但Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。
對應如下的流程圖:
比如:i = i + 1,就是一個非原子操作,它涉及到獲取i,獲取1,相加,賦值等4個操作,所以在多線程情況下可能會出現并發問題。
比如 i = i+1,我們要保證它的原子性 該怎么做呢?可以通過八種操作中的lock和unlock來達到目的。但是JVM并沒有把lock和unlock操作直接開放給用戶使用,我們的Java代碼中,就是大家所熟知的synchronized關鍵字保證原子性。
代碼:
package cn.itcast.thread;public class Test7 { // 無法在多線程的情況下實現原子自遞增的問題。 private static int count = 0; // 定義累計的方法 public synchronized static void inc(){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public static void main(String[] args) throws InterruptedException { // 循環創建線程 for(int i = 0; i < 1000; i++){ new Thread(() -> { inc(); }).start(); } Thread.sleep(4000); System.out.println("y運行結果:"+count); }}
Java內存模型中,允許編輯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。
從字面上的意思理解,有序就是要保證代碼按照既定的順序依次執行。但是CPU(或編譯器)出于性能優化的目的,在保證不會對程序運行結果產生影響的前提下,代碼的執行順序可能會和我們既定的順序不一致。
示例
int i = 1;
int j = 1;
這兩行代碼互相沒有任何依賴關系,誰先執行還是后執行,對程序運行結果都不會有什么影響。經過指令重排后,可能 int j = 1; 就比int i = 1;先執行了。不同的CPU架構可能支持不同的重排規則,像Load-Load、Load-Store、Store-Store、Store-Load等等。
指令重排的后果在并發的情況下有時會是嚴重的,比如以下代碼:
public void execute(){ int a = 0; int b = 1; int c = a + b;}
這里a=0,b=1兩句可以隨便排序,不影響程序邏輯結果,但c=a+b這句必須在前兩句的后面執行。
從前面那個例子可以看到,重排序在多線程環境下出現的概率還是挺高的,在關鍵字上有volatile和synchronized可以禁用重排序。
可能上面說的比較繞,舉個簡單的例子:
// x、y為非volatile變量// flag為volatile變量 x = 2; //語句1y = 0; //語句2flag = true; //語句3x = 4; //語句4y = -1; //語句5
在面試過程時,經常會被問到各種各樣的鎖,如樂觀鎖、讀寫鎖等等,非常繁多,在此做一個總結。介紹的內容如下:
? 樂觀鎖/悲觀鎖
? 獨享鎖/共享鎖
? 互斥鎖/讀寫鎖
? 可重入鎖
? 公平鎖/非公平鎖
? 分段鎖
? 偏向鎖/輕量級鎖/重量級鎖
? 自旋鎖
樂觀鎖/悲觀鎖
樂觀鎖與悲觀鎖并不是特指某兩種類型的鎖,是人們定義出來的概念或思想,主要是指看待并發同步的角度。
樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS(Compare and Swap 比較并交換)實現的。
悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。比如Java里面的synchronized關鍵字的實現就是悲觀鎖。
悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。
悲觀鎖在Java中的使用,就是利用各種鎖。
樂觀鎖在Java中的使用,是無鎖編程,常常采用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。
獨享鎖/共享鎖
獨享鎖是指該鎖一次只能被一個線程所持有。
共享鎖是指該鎖可被多個線程所持有。
對于Java ReentrantLock而言,其是獨享鎖。但是對于Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
讀鎖的共享鎖可保證并發讀是非常高效的,寫的過程是互斥的。
對于synchronized而言,當然是獨享鎖。
互斥鎖/讀寫鎖
獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。
互斥鎖在Java中的具體實現就是ReentrantLock。
讀寫鎖在Java中的具體實現就是ReadWriteLock。
可重入鎖
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。說的有點抽象,下面會有一個代碼的示例。
對于Java ReetrantLock而言,從名字就可以看出是一個重入鎖,其名字是Reentrant Lock 重新進入鎖。
對于synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。
synchronized void setA() throws Exception{ Thread.sleep(1000); setB();}synchronized void setB() throws Exception{ Thread.sleep(1000);}
上面的代碼就是一個可重入鎖的一個特點。如果不是可重入鎖的話,setB可能不會被當前線程執行,可能造成死鎖。
公平鎖/非公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優先獲取鎖。
對于Java ReetrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在于吞吐量比公平鎖大。
對于synchronized而言,也是一種非公平鎖。
分段鎖
分段鎖其實是一種鎖的設計,并不是具體的一種鎖,對于ConcurrentHashMap而言,其并發的實現就是通過分段鎖的形式來實現高效的并發操作。
我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,它即類似于HashMap(JDK7和JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。
偏向鎖/輕量級鎖/重量級鎖
偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖。降低獲取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
重量級鎖是指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓他申請的線程進入阻塞,性能降低。
自旋鎖
在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。
synchronized是并發編程中接觸的最基本的同步關鍵字,是一種重量級鎖,也是java內置的同步機制,首先我們知道synchronized提供了互斥性和可見性,那么我們可以通過使用它來保證并發的安全。
synchronized三種用法:
當使用synchronized修飾類普通方法時,那么當前加鎖的級別就是實例對象,當多個線程并發訪問該對象的同步方法、同步代碼塊時,會進行同步。
當使用synchronized修飾類靜態方法時,那么當前加鎖的級別就是類,當多個線程并發訪問該類(所有實例對象)的同步方法以及同步代碼塊時,會進行同步。
當使用synchronized修飾代碼塊時,那么當前加鎖的級別就是synchronized(X)中配置的x對象實例,當多個線程并發訪問該對象的同步方法、同步代碼塊以及當前的代碼塊時,會進行同步。
使用同步代碼塊時要注意的是不要使用String類型對象,因為String常量池的存在,所以很容易導致出問題。
synchronized 同步代碼塊解決線程安全問題
什么是線程安全問題: 多線程操作共享數據,導致共享數據出現錯亂
出現線程安全問題的條件:
1.有多個線程
2.有共享數據
3.多線程操作共享數據
代碼:
package cn.itcast.thread;/** 學習使用同步代碼塊解決線程安全問題 */public class Test8 { // 定義票的總數量 private static int ticket = 100; public static void main(String[] args) { Runnable runnable = () ->{ // 循環買票 while (true){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 同步代碼塊(類鎖) synchronized (Test8.class) { if (ticket > 0) { ticket--; System.out.println(Thread.currentThread().getName() + "賣了一張票,剩余:" + ticket); } else { // 票沒了 break; } } } }; // 創建3個線程 Thread t1 = new Thread(runnable, "窗口1"); Thread t2 = new Thread(runnable, "窗口2"); Thread t3 = new Thread(runnable, "窗口3"); t1.start(); t2.start(); t3.start(); }}
小結
任意對象。
多線程并發方法同步代碼塊,需要鎖同一個對象。
同步代碼塊中有鎖的線程進入,無鎖的線程需要等待。
synchronized 同步方法
synchronized 同步方法解決線程安全問題
語法
// 普通同步方法,對當前一個實例對象加鎖,多線程操作同一個對象實例進行同步操作public synchronized void 方法名() { ...}// 靜態同步方法,對類加鎖,多線程操作當前類所有實例對象進行同步操作public static synchronized void 方法名() { ...}
代碼:
package cn.itcast.thread;/** 學習使用同步代碼塊解決線程安全問題 */public class Test9 { // 定義票的總數量 private static int ticket = 100; public static void main(String[] args) { Runnable runnable = () ->{ // 循環買票 while (true){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } sell(); } }; // 創建3個線程 Thread t1 = new Thread(runnable, "窗口1"); Thread t2 = new Thread(runnable, "窗口2"); Thread t3 = new Thread(runnable, "窗口3"); t1.start(); t2.start(); t3.start(); } // 同步方法 private static synchronized void sell() { // 同步代碼塊(類鎖) if (ticket > 0) { ticket--; System.out.println(Thread.currentThread().getName() + "賣了一張票,剩余:" + ticket); } }}
小結
synchronized是通過對象內部的一個叫==監視器鎖==來實現的,但是監視器鎖本質又是依賴底層的操作系統的Mutex(互斥) Lock來實現的,而操系統實現線程之間的切換會造成帶量的CPU資源浪費,這個成本非常的高,狀態之間的轉換需要相對比較長的時間,這就是為什么Synchronized效率低的原因,因此這種依賴于操作系統Mutex Lock所實現的鎖我們稱之為:==重量級鎖==。JDK中對Synchronized做的種種優化,其核心都是為了減少這種重量級鎖的使用,JDK1.5以后,為來減少獲得鎖和釋放鎖所帶來的性能消耗, JDK引入了:”輕量級鎖“和”偏向鎖“進行優化,這個優化自動的無需開發人員介入。
synchronized 屬于最基本的線程通信機制,基于對象監視器實現的。Java中的每個對象都與一個監視器相關聯,一個線程可以鎖定或解鎖。一次只有一個線程可以鎖定監視器。試圖鎖定該監視器的任何其他線程都會被阻塞,直到它們可以獲得該監視器上的鎖定為止。
目標:學習使用ReentrantLock可重入鎖解決線程安全問題
介紹
API方法
用來獲取鎖,如果鎖被其他線程獲取,處于等待狀態。如果采用Lock,必須主動去釋放鎖,并且在發生異常的時候,不會自動釋放鎖。因此一般來說,使用Lock必須早try{}catch{}塊中進行,并且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被釋放,防止死鎖發生。
通過這個這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。
tryLock方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已經由其他線程獲取),則返回false,也就是說這個方法無論如何都會立即返回。在獲取不到鎖的時候,不會再那一直等待。
與tryLock類似,只不過是有等待時間,在等待時間內獲取到鎖返回true,超時返回false。
釋放鎖,一定要在finally塊中釋放。
Lock鎖使用語法
Lock介紹:
Lock實現類:
ReentrantLock
Lock使用標準方式
l.lock(); // 獲得鎖 try { 操作共享資源的代碼 } finally { l.unlock(); // 釋放鎖 }
代碼:
package cn.itcast.thread;import java.util.concurrent.locks.ReentrantLock;/** 學習使用Lock解決線程安全問題 */public class Test10 { // 定義票的總數量 private static int ticket = 100; public static void main(String[] args) { // 創建可重入鎖對象 ReentrantLock lock = new ReentrantLock(); Runnable runnable = () -> { // 循環賣票 while (true) { try { Thread.sleep(10); // 獲得鎖 lock.lock(); if (ticket > 0) { ticket--; System.out.println(Thread.currentThread().getName() + "賣了一張票,剩余:" + ticket); } else { break; } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 釋放鎖 lock.unlock(); } } }; // 創建3個線程 Thread t1 = new Thread(runnable, "窗口1"); Thread t2 = new Thread(runnable, "窗口2"); Thread t3 = new Thread(runnable, "窗口3"); t1.start(); t2.start(); t3.start(); }}
目標:掌握Readwriterlock讀寫鎖分析和場景
ReadWriteLock接口介紹
ReadWriteLock也是一個接口,在它里面只定義了兩個方法:
package java.util.concurrent.locks;public interface ReadWriteLock { Lock readLock(); Lock writeLock();}
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。
ReentrantReadWriteLock介紹
ReentrantReadWriteLock里面提供了很多豐富的方法,不過最主要的有兩個方法:readLock() 和 writeLock()用來獲取讀鎖和寫鎖。
ReentrantReadWriteLock可重入讀寫鎖
package cn.itcast.thread;import java.util.concurrent.locks.ReentrantReadWriteLock;// ReentrantReadWriteLock: 可重入讀寫鎖 (讀鎖是共享鎖,寫鎖是獨享鎖)public class Test11 { // 定義可重入讀寫鎖 private ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); public static void main(String[] args) { final Test11 test = new Test11(); new Thread(() -> { test.get(Thread.currentThread()); }).start(); new Thread(() -> { test.get(Thread.currentThread()); }).start(); } public void get(Thread thread) { // 讀鎖是共享鎖 rw.readLock().lock(); // 寫鎖是獨享鎖 //rw.writeLock().lock(); try { for (int i = 0; i < 50; i++){ System.out.println(thread.getName() + "正在進行讀操作"); } System.out.println(thread.getName() + "讀操作完畢"); }finally { rw.readLock().unlock(); //rw.writeLock().unlock(); } }}
小結
ReentrantReadWriteLock的優勢與應用場景
1. 大大提升了讀操作的效率。
2. 不過要注意的是,如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
3. 如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
4. ReentrantReadWriteLock適合讀多寫少的應用場景
目標:掌握java.util.concurrent.atomic包下原子更新基本類型
介紹
Java從JDK 1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。actomic實現原子性操作采用的是CAS算法保證原子性操作,性能高效。
CAS原理分析:
使用CAS(Compare-And-Swap)比較并交換,操作保證數據原子性
CAS算法是 JDK對并發操作共享數據的支持,包含了3個操作數
第一個操作數:內存值value(V)
第二個操作數:預估值expect(E)
第三個操作數:更新值new(N)
含義:當多線程每個線程執行寫的操作時,每個線程都會讀取主存最新內存值value,并設置預估的值,只有最新內存值與預估值一致的線程,就會將需要更新的值更新到主存中,其他線程就會失敗保證原子性操作;這樣就解決了synchronized排隊導致性能低下的問題。
java.util.concurrent.atomic包的原子類:
原子更新基本類型
類 | 含義 |
AtomicBoolean | 原子更新布爾類型 |
AtomicInteger | 原子更新整型 |
AtomicLong | 原子更新長整型 |
上面3個類提供方法完全一樣,所以我們以AtomicInteger為例進行講解API方法:
方法 | 含義 |
int addAndGet(int delta) | 以原子方式將輸入的數值與實例中的值(AtomicInteger里的value)相加,并返回結果 |
boolean compareAndSet(int expect,int update) | 如果輸入的數值等于預期值,則以原子方式將該值設置為輸入的值 |
int getAndIncrement() | 以原子方式將當前值加1,注意,這里返回的是自增前的值 |
void lazySet(int newValue) | 最終會設置成newValue,使用lazySet設置值后,可能導致其他線程在之后的一小段時間內還是可以讀到舊的值 |
int getAndSet(int newValue) | 以原子方式設置為newValue的值,并返回舊值。 |
int incrementAndGet() | 以原子方式將當前值加1,注意,這里返回的是自增后的值 |
代碼:
package cn.itcast.thread;import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicInteger;public class Test12 implements Runnable { // 定義整型并發原子對象 private static AtomicInteger atomicInteger = new AtomicInteger(0); @Override public void run() { try { // 線程休眠 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 進行原子性操作+1 System.out.println(atomicInteger.incrementAndGet()); } public static void main(String[] args) throws InterruptedException { // 創建List集合 List<Thread> list = new ArrayList<>(); Test12 task = new Test12(); // 開啟多線程進行操作共享變量 for (int i = 0; i <10 ; i++) { Thread thread = new Thread(task); list.add(thread); thread.start(); } for (Thread thread : list) { thread.join(); // 確保所有thread全部運行 } System.out.println("遞增結果:" + atomicInteger.get()); }}
運行效果
目標:掌握java.util.concurrent.atomic包下原子更新數組類型
介紹
通過原子的方式更新數組里的某個元素,Atomic包提供了以下3個類。這幾個類提供的方法幾乎一樣,所以本節僅以AtomicIntegerArray為例進行講解。
類 | 含義 |
AtomicIntegerArray | 原子更新整型數組里的元素 |
AtomicLongArray | 原子更新長整型數組里的元素 |
AtomicReferenceArray | 原子更新引用類型數組里的元素 |
AtomicIntegerArray類:
方法 | 含義 |
int addAndGet(int i,int delta) | 以原子方式將輸入值與數組中索引i的元素相加 |
boolean compareAndSet(int i,int expect,int update) | 如果當前值等于預期值,則以原子方式將數組位置i的元素設置成update值 |
代碼:
package cn.itcast.thread;import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicIntegerArray;public class Test13 implements Runnable{ // 定義數組 private static int[] ints = {0,2,3}; // 定義原子整型數組對象 private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(ints); @Override public void run() { try { // 線程休眠 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 操作普通數組中的第一個元素 +1 ints[0] = ints[0] + 1; // 操作原子數組中的第一個元素 +1 System.out.println(atomicIntegerArray.incrementAndGet(0)); } public static void main(String[] args) throws InterruptedException { // 創建List集合 List<Thread> list = new ArrayList<>(); Test13 task = new Test13(); // 開啟多線程進行操作共享變量 for (int i = 0; i <10 ; i++) { Thread thread = new Thread(task); list.add(thread); thread.start(); } for (Thread thread : list) { thread.join(); // 確保所有thread全部運行 } System.out.println("原子數組操作結果:" + atomicIntegerArray.get(0)); // 10 System.out.println("普通數組操作結果:" + ints[0]); // 不一定 }}
運行效果
注意:
數組value通過構造方法傳遞進去,然后AtomicIntegerArray會將當前數組復制一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組。
目標:使用原子操作更新引用類型數據(也就是原子更新多個變量)
介紹
原子更新基本類型的AtomicInteger,只能更新一個變量,如果要原子更新多個變量,就需要使用這個原子更新引用類型提供的類。Atomic包提供了以下3個類:
類 | 介紹 |
AtomicReference | 原子更新引用類型 |
AtomicReferenceFieldUpdater | 原子更新引用類型里的字段 |
AtomicMarkableReference | 原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法AtomicMarkableReference(V initialRef,boolean initialMark) |
AtomicReference類:
方法 | 介紹 |
boolean compareAndSet(V expect, V update) | 如果是期望值expect與當前內存值一樣,更新為update |
代碼:
package cn.itcast.thread;import java.util.concurrent.atomic.AtomicReference;public class Test14 { public static void main(String[] args) { // 1. 創建一個User對象封裝數據 User user = new User("李小華", 18); // 2. 創建一個原子引用類型AtomicReference操作User類型數據 AtomicReference<User> atomicReference = new AtomicReference<>(); // 3. 將user對象的數據存入原子引用類型對象中 atomicReference.set(user); // 4. 更新原子引用類型存儲的數據 atomicReference.compareAndSet(user, new User("李中華", 20)); // 5. 打印普通user對象數據與原子引用類型對象數據 System.out.println("普通對象數據:"+ user +",對象hashcode: " + user.hashCode()); System.out.println("原子引用類型對象數據:" + atomicReference.get() + ",對象hashcode: " + atomicReference.get().hashCode()); }}
運行效果
目標:掌握CountDownLatch使用(實現等待其他線程處理完才繼續運行當前線程)
介紹
CountDownLatch是一個同步輔助類,也叫倒計數閉鎖,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。用給定的計數初始化 CountDownLatch。這個輔助類可以進行計算遞減,所以在當前計數到達零之前,可以讓現場一直受阻塞。到達0之后,會釋放所有等待的線程,執行后續操作。
CountDownLatch類 | 說明 |
CountDownLatch(int count) | 創建CountDownLatch 實例并設置預定計數次數。 |
void countDown() | 遞減鎖存器的計數,如果計數到達零,則釋放所有等待的線程。如果當前計數大于零,則將計數減少1。 |
void await() | 使當前線程在鎖存器倒計數至零之前一直等待,除非線程被中斷。如果當前的計數為零,則此方法立即返回。 |
CountDownLatch 是通過一個計數器來實現的,計數器的初始值為線程的數量。每當一個線程完成了自己的任務后,計數器的值就會減 1。當計數器值到達 0 時,表示所有的線程已經完成了任務,然后在閉鎖上等待的線程就可以恢復執行任務。
傳統join阻塞案例代碼:
package cn.itcast.thread;public class Test15 { public static void main(String[] args) throws InterruptedException { // 創建線程1 Thread t1 = new Thread(() -> { System.out.println("parser1 finish"); }); // 創建線程2 Thread t2 = new Thread(() -> { System.out.println("parser2 finish"); }); t1.start(); t2.start(); t1.join(); // join阻塞 t2.join(); // join阻塞 System.out.println("join方式: all parser finish"); }}
join阻塞效果:
使用CountDownLatch實現阻塞代碼優化:
package cn.itcast.thread;import java.util.concurrent.CountDownLatch;public class Test16 { // 定義倒計數閉鎖對象 private static CountDownLatch countDownLatch = new CountDownLatch(2); public static void main(String[] args) throws InterruptedException { // 創建線程1 Thread t1 = new Thread(() -> { System.out.println("parser1 finish"); countDownLatch.countDown(); // 計算遞減1 }); // 創建線程2 Thread t2 = new Thread(() -> { System.out.println("parser2 finish"); countDownLatch.countDown(); // 計算遞減1 }); t1.start(); t2.start(); countDownLatch.await(); // 阻塞,計算為0釋放阻塞,運行后面的代碼 System.out.println("join方式: all parser finish"); }}
使用CountDownLatch實現阻塞效果:
CountDownLatch倒計數閉鎖好處:實現線程最大并發執行。
目標:掌握CyclicBarrier的使用
介紹
CyclicBarrier是JDK 1.5的 java.util.concurrent 并發包中提供的一個并發工具類。
CyclicBarrier類 | 說明 |
CyclicBarrier(int parties) | 創建對象,參數表示屏障攔截的線程數量,初始化相互等待的線程數量 |
int await() | 告訴CyclicBarrier自己已經到達了屏障,然后當前線程被阻塞返回值int為達到屏障器的索引: 索引未達到屏障線程數量-1,0表示最后一個達到屏障 |
int getParties() | 獲取 CyclicBarrier 打開屏障的線程數量 |
void reset() | 使CyclicBarrier回歸初始狀態,它做了兩件事。如果有正在等待的線程,則會拋出 BrokenBarrierException 異常,且這些線程停止等待,繼續執行。將是否破損標志位broken置為 false。 |
boolean isBroken() | 獲取是否破損標志位broken的值,此值有以下幾種情況。1.CyclicBarrier初始化時,broken=false表示屏障未破損。 2.如果正在等待的線程被中斷,則broken=true,表示屏障破損。 3.如果正在等待的線程超時, 則broken=true,表示屏障破損。 4.如果有線程調用 CyclicBarrier.reset() 方法,則broken=false,表示屏障回到未破損狀態。 |
int getNumberWaiting() | 獲取達到屏障阻塞等待的線程數 |
CyclicBarrier(int parties,Runnable barrierAction) | 用于所有線程到達屏障時,優先執行barrierAction的線程 |
代碼:
package cn.itcast.thread;import java.util.concurrent.CyclicBarrier;import java.util.concurrent.TimeUnit;public class Test17 { // 定義同步屏障對象 private static CyclicBarrier cb = new CyclicBarrier(2); public static void main(String[] args) { // 創建線程1 new Thread(() -> { try { System.out.println("達到屏障阻塞線程數:" + cb.getNumberWaiting()); cb.await(); // 達到屏障阻塞,+1 System.out.println("運行結束1"); // 不會運行 } catch (Exception e) { e.printStackTrace(); } }).start(); // 創建線程2 new Thread(() -> { try { System.out.println("達到屏障阻塞線程數:" + cb.getNumberWaiting()); cb.await(); // 達到屏障阻塞,+1 System.out.println("運行結束2"); // 不會運行 } catch (Exception e) { e.printStackTrace(); } }).start(); try { TimeUnit.SECONDS.sleep(2); // 會運行,沒有到達屏障,不會阻塞 System.out.println("主線程完成,攔截線程數:" + cb.getParties() + ",達到屏障阻塞線程數:" + cb.getNumberWaiting()); } catch (InterruptedException e) { e.printStackTrace(); } }}
運行效果
由于兩個子線程的調度是由CPU決定的,兩個子線程都有可能先執行,所以會產生兩種輸出:
說明:由于所有線程都達到屏障,所有阻塞線程被釋放,所以阻塞線程為0
如果把new CyclicBarrier(2)修改成new CyclicBarrier(3),則主線程和子線程會永遠等待,因為沒有第三個線程執行await方法,即沒有第三個線程到達屏障,所以之前到達屏障的兩個線程都不會繼續執行。
private static CyclicBarrier cb = new CyclicBarrier(3);
目標:掌握Semaphore的使用
介紹
Semaphore(信號量)限制著訪問某些資源的線程數量,在到達限制的線程數量之前,線程能夠繼續進行資源的訪問,一旦訪問資源的數量到達限制的線程數量,這個時候線程就不能夠再去獲取資源,只有等待有線程退出資源的獲取。
應用場景
比如模擬一個停車場停車信號,假設停車場只有兩個車位,一開始兩個車位都是空的。這時同時來了兩輛車,看門人允許它們進入停車場,然后放下車攔。以后來的車必須在入口等待,直到停車場中有車輛離開。這時,如果有一輛車離開停車場,看門人得知后,打開車攔,放入一輛,如果又離開一輛,則又可以放入一輛,如此往復。
API方法
信號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個 acquire(),然后再獲取該許可。每個 release() 添加一個許可,從而可能釋放一個正在阻塞的獲取者讓其運行。但是,不使用實際的許可對象,Semaphore 只對可用許可的號碼進行計數,并采取相應的行動。
所以一個Semaphore 信號量有且僅有 3 種操作,且它們全部是原子的。
方法 | 說明 |
Semaphore(int permits) | permits是允許同時運行的線程數目,創建指定數據線程的信號量 |
Semaphore(int permits, boolean fair) | permits是允許同時運行的線程數目,創建指定數據線程的信號量;fair指定是公平模式還是非公平模式,默認非公平模式 |
void acquire() | 方法阻塞,直到申請獲取到許可證才可以運行當前線程 |
void release() | 釋放當前線程一個阻塞的 acquire() 方法,方法增加一個許可證 |
intavailablePermits() | 返回此信號量中當前可用的許可證數 |
intgetQueueLength() | 返回正在等待獲取許可證的線程數 |
booleanhasQueuedThreads() | 是否有線程正在等待獲取許可證 |
void reducePermits(int reduction) | 減少reduction個許可證,是個protected方法 |
Collection getQueuedThreads() | 返回所有等待獲取許可證的線程集合,是個protected方法 |
代碼:
package cn.itcast.thread;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;public class Test18 { public static void main(String[] args) { // 1. 創建信號量對象控制并發線程數量,設置許可數5個(同時運行5個線程) Semaphore semaphore = new Semaphore(5, true); // 2. 循環運行10個線程(會看到每次只允許5個線程) for (int i = 0; i < 10; i++) { new Thread(() -> { try { // 2.1 申請獲取許可 semaphore.acquire(); // 2.2 運行業務 System.out.println(Thread.currentThread().getName() + "車,進入停車場"); TimeUnit.SECONDS.sleep(3);// 讓當前線程休眠(讓線程多運行一會,方便觀察效果) System.out.println(Thread.currentThread().getName() + "車,離開停車場"); // 2.3 釋放阻塞,增加一個許可(讓下一個阻塞的線程運行) semaphore.release(); } catch (Exception e) { e.printStackTrace(); } }).start(); } }}
運行效果
特點
Semaphore 在計數器不為 0 的時候對線程就放行,一旦達到 0,那么所有請求資源的新線程都會被阻塞,包括增加請求到許可的線程,Semaphore 是不可重入的。
Semaphore 有兩種模式,公平模式 和 非公平模式(默認使用)
Semaphore semaphore = new Semaphore(許可數, true); // 公平模式
Semaphore semaphore = new Semaphore(許可數, false); // 非公平模式,(默認)
小結
– 初始化許可集
– 增加許可,release()方法釋放一個阻塞,增加一個許可。
– 獲取許可,acquire()方法獲取許可,再獲取許可前處于阻塞等待。
目標:掌握Exchanger的使用
介紹
Exchanger(交換者)是一個用于線程間協作的工具類,可以用于進行線程間的數據交換。
交換數據原理
API方法
方法 | 說明 |
V exchange(V x) | 用于進行線程間的數據交換 |
V exchange(V x, long timeout, TimeUnit unit) | 設置交換數據并等待超時時間 |
代碼:
package cn.itcast.thread;import java.util.concurrent.Exchanger;public class Test19 { public static void main(String[] args) { // 1. 創建交換數據對象,并設置傳輸數據的類型 Exchanger<String> exchanger = new Exchanger<>(); // 2. 啟動2個線程進行交換數據 // 創建線程1 new Thread(() -> { // 2.1 定義交換的數據 String girl1 = "【柳巖】"; System.out.println(Thread.currentThread().getName() + "說:我的女友 " + girl1); System.out.println(Thread.currentThread().getName() + "說:等待線程2交換數據"); // 2.2 將數據交換給線程2,并拿到線程2的數據 try { String b = exchanger.exchange(girl1);//注意:如果線程2沒有到達同步點,當前線程會被阻塞一直等到 //成功獲取線程2的數據后 System.out.println(Thread.currentThread().getName() + "說:我拿到了 " + b); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 創建線程2 new Thread(()->{ // 2.3 定義交換的數據 String girl2 = "【楊冪】"; System.out.println(Thread.currentThread().getName()+"說:我的女友 " + girl2); System.out.println(Thread.currentThread().getName()+"說:等待線程1交換數據"); // 2.4 將數據交換給線程1,并拿到線程1的數據 try { String a = exchanger.exchange(girl2); // 成功獲取線程1的數據后 System.out.println(Thread.currentThread().getName()+"說:我拿到了 " + a); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); }}
運行效果
注意:如果兩個線程有一個沒有執行exchange()方法,則會一直等待,如果擔心有特殊情況發生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)設置最大等待時長。
整篇文章內容較多,這里做個總結。1~7小節講了并發編程的核心基礎概念;在對并發有了一定的基礎了解后,8~9小節講了JVM對并發問題的設計——JMM;從10小節開始詳細介紹了JDK 并發包里面的常用工具。這里只是一個入門級別的了解,但是萬丈高樓平地起,這些基礎是后面提升必不可缺的知識,希望大家可以掌握它。
蔡柱梁,51CTO社區編輯,從事Java后端開發8年,做過傳統項目廣電BOSS系統,后投身互聯網電商,負責過訂單,TMS,中間件等。
本文鏈接:http://www.www897cc.com/showinfo-26-51249-0.html看完后,你再也不用怕面試問并發編程啦
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
下一篇: JS問題:項目中如何區分使用防抖或節流?