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