(上))
1、了解線程1.1 線程概念1. 進(jìn)程操作系統(tǒng)資源分配的最小單位。打開微信、瀏覽器、IDEA、JVM每一個(gè)都是獨(dú)立進(jìn)程進(jìn)程擁有獨(dú)立內(nèi)存空間、文件句柄、CPU 時(shí)間片、堆內(nèi)存進(jìn)程之間完全隔離進(jìn)程切換開銷極大操作系統(tǒng)要保存整套上下文2. 線程進(jìn)程內(nèi)部獨(dú)立執(zhí)行的最小調(diào)度單位也叫輕量級(jí)進(jìn)程。一個(gè)進(jìn)程至少包含 1 個(gè)主線程同一個(gè)進(jìn)程內(nèi)所有線程共享進(jìn)程的堆內(nèi)存、方法區(qū)、文件資源只私有自己的棧內(nèi)存CPU 只調(diào)度線程不直接調(diào)度進(jìn)程線程切換成本遠(yuǎn)低于進(jìn)程一句話口訣進(jìn)程是房子線程是房子里干活的人房子資源所有人共用每個(gè)人各自有自己的工作臺(tái)私有棧。1.2 創(chuàng)建線程方法1MyThread 是Demo1 的非靜態(tài)內(nèi)部類繼承 Thread 重寫 runrun 為線程執(zhí)行體。非靜態(tài)內(nèi)部類依賴外部類實(shí)例main靜態(tài)方法里直接new MyThread()編譯報(bào)錯(cuò)。啟動(dòng)線程必須調(diào)用start()自動(dòng)執(zhí)行 run直接調(diào)用 run 只是普通方法。class Demo1 { class MyThread extends Thread{ Override public void run(){ System.out.println(hello my thread); } } } public static void main(String[] args) { MyThread myThreadnew MyThread(); myThread.start(); } }方法2采用實(shí)現(xiàn)Runnable接口的方式定義線程任務(wù)MyThread重寫run()放置線程執(zhí)行代碼。MyThread加static成為靜態(tài)內(nèi)部類可在靜態(tài)main方法直接實(shí)例化。將Runnable任務(wù)對(duì)象傳入Thread構(gòu)造器調(diào)用start()開啟新線程執(zhí)行run方法。該方式規(guī)避單繼承限制任務(wù)與線程解耦開發(fā)更常用。package practice.thread; public class Demo2 { static class MyThread implements Runnable{ Override public void run(){ System.out.println(hello my thread); } } public static void main(String[] args) { Thread threadnew Thread(new MyThread()); thread.start(); } }方法3重要使用Lambda表達(dá)式簡(jiǎn)化Runnable接口省去單獨(dú)創(chuàng)建類直接填寫線程執(zhí)行任務(wù)。把Lambda任務(wù)傳入Thread構(gòu)造方法創(chuàng)建線程對(duì)象調(diào)用start()啟動(dòng)子線程打印一句話后線程結(jié)束。屬于實(shí)現(xiàn)Runnable方式的極簡(jiǎn)寫法代碼最簡(jiǎn)潔日常開發(fā)高頻使用。public class Demo3 { public static void main(String[] args) { Thread threadnew Thread(()-{ System.out.println(hello my thread); }); thread.start(); } }2、Thread類及其方法2.1 常見構(gòu)造方法構(gòu)造方法說明Thread()創(chuàng)建線程對(duì)象使用默認(rèn)名稱Thread(Runnable target)傳入 Runnable 任務(wù)創(chuàng)建線程對(duì)象Thread(String name)創(chuàng)建線程對(duì)象并自定義線程名稱Thread(Runnable target, String name)傳入 Runnable 任務(wù)同時(shí)自定義線程名稱2.2 常見屬性屬性獲取方法簡(jiǎn)要說明IDgetId()獲取線程唯一標(biāo)識(shí) ID不可修改名稱getName()獲取線程名可通過setName()修改狀態(tài)getState()返回線程 6 種生命周期狀態(tài)NEW/RUNNABLE 等優(yōu)先級(jí)getPriority()獲取線程優(yōu)先級(jí)1~10影響 OS 調(diào)度概率是否后臺(tái)線程isDaemon()判斷是否為守護(hù)線程主線程退出守護(hù)線程自動(dòng)終止是否存活isAlive()判斷線程是否處于運(yùn)行 / 未消亡狀態(tài)是否被中斷isInterrupted()檢測(cè)線程中斷標(biāo)記不清除標(biāo)記2.3 啟動(dòng)線程執(zhí)行線程的start方法是調(diào)用系統(tǒng)API真正在操作系統(tǒng)內(nèi)創(chuàng)建一個(gè)線程這個(gè)新線程會(huì)以run作為入口方法執(zhí)行run里的邏輯→run方法不顯式調(diào)用創(chuàng)建新線程自動(dòng)執(zhí)行的注如果是執(zhí)行run方法不創(chuàng)建新線程2.4 中斷線程方法完整說明public void interrupt()給目標(biāo)線程打上中斷標(biāo)記若線程處于sleep()/wait()/join()阻塞狀態(tài)直接拋出InterruptedException并自動(dòng)清除中斷標(biāo)記public static boolean interrupted()靜態(tài)方法檢測(cè)當(dāng)前執(zhí)行線程的中斷標(biāo)記檢測(cè)后立即清空標(biāo)記位public boolean isInterrupted()實(shí)例方法檢測(cè)調(diào)用該方法的線程對(duì)象的中斷標(biāo)記只查詢、不會(huì)清除標(biāo)記位public class Demo6 { public static void main(String[] args) throws InterruptedException { Thread tnew Thread(()-{ //currentThread()是在start方法之后執(zhí)行的 //并且在t線程中執(zhí)行返回t線程對(duì)象 while (!Thread.currentThread().isInterrupted()) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } }); t.start(); Thread.sleep(2000); t.interrupt(); System.out.println(線程中斷); } }結(jié)果注意任務(wù)內(nèi)部無法直接訪問外部變量t做通用判斷Thread.currentThread()自動(dòng)獲取正在執(zhí)行這段代碼的當(dāng)前線程通用性強(qiáng)同一個(gè)任務(wù)可以綁定任意線程運(yùn)行。降低代碼耦合線程執(zhí)行邏輯不依賴外部線程引用是線程內(nèi)部檢測(cè)自身中斷標(biāo)記的標(biāo)準(zhǔn)規(guī)范寫法。如果直接寫t.isInterrupted()語法能跑但任務(wù)被寫死只能給t線程使用無法復(fù)用。2.5 等待線程join() 作用讓當(dāng)前線程阻塞插隊(duì)等待另一個(gè)線程執(zhí)行完成實(shí)現(xiàn)線程執(zhí)行順序控制。方法說明public void join()無限阻塞直到調(diào)用該方法的線程執(zhí)行完畢當(dāng)前線程才繼續(xù)往下運(yùn)行public void join(long millis)限時(shí)等待最多阻塞millis毫秒超時(shí)無論目標(biāo)線程是否結(jié)束當(dāng)前線程都會(huì)恢復(fù)執(zhí)行public void join(long millis, int nanos)高精度限時(shí)等待毫秒 納秒組合設(shè)置等待時(shí)長(zhǎng)極少使用package practice.thread; public class Demo4 { public static void main(String[] args) throws InterruptedException{ Thread threadnew Thread(()-{ for(int i0;i10;i){ System.out.println(hello my threadi); } }); thread.start(); // 等待線程執(zhí)行3秒 thread.join(3000); for(int i0;i10;i){ System.out.println(hello maini); } } }結(jié)果不加Join vs 加Join2.6 獲取當(dāng)前線程引用返回當(dāng)前線程對(duì)象的引用public class Demo5 { public static void main(String[] args) { Thread threadThread.currentThread(); thread.setName(main thread); System.out.println(thread.getName()); } }2.7 休眠線程我們熟悉的sleeppublic class Demo6 { public static void main(String[] args) { Thread tnew Thread(()-{ long startTimeSystem.currentTimeMillis(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } long endTimeSystem.currentTimeMillis(); System.out.println(hello my thread (endTime-startTime)s); }); t.start(); } }結(jié)果2.8 前后臺(tái)線程及區(qū)別1. 前臺(tái)線程默認(rèn)所有新建的線程都是前臺(tái)JVM 退出規(guī)則只要任意一個(gè)前臺(tái)線程還在運(yùn)行JVM 進(jìn)程就不會(huì)終止。必須所有前臺(tái)線程全部執(zhí)行完畢JVM 才會(huì)正常關(guān)閉。2. 守護(hù)線程后臺(tái)線程setDaemon(true) 設(shè)置當(dāng)所有前臺(tái)線程全部執(zhí)行完畢無論守護(hù)線程有沒有跑完JVM 直接強(qiáng)制退出守護(hù)線程被立刻粗暴終止。public class Demo4 { public static void main(String[] args) { Thread t new Thread(()-{ while(true){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } },我的線程); System.out.println(是否是后臺(tái)線程 t.isDaemon()); t.setDaemon(true);// 設(shè)置為后臺(tái)線程 System.out.println(是否設(shè)置為后臺(tái)線程 t.isDaemon()); System.out.println(已設(shè)置為后臺(tái)線程); t.start(); for (int i 0; i 10; i) { System.out.println(hello main); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }結(jié)果原本線程t是無限循環(huán)將其設(shè)置為后臺(tái)線程后線程t隨著主線程結(jié)束而結(jié)束3、線程的狀態(tài)3.1 線程狀態(tài)情況狀態(tài)簡(jiǎn)寫核心說明NEW新建線程對(duì)象已創(chuàng)建未調(diào)用start()未啟動(dòng)RUNNABLE就緒 / 運(yùn)行正在 CPU 執(zhí)行或就緒排隊(duì)爭(zhēng)搶 CPU 時(shí)間片BLOCKED阻塞競(jìng)爭(zhēng)synchronized同步鎖失敗阻塞排隊(duì)等鎖WAITING無限等待無期限掛起需其他線程主動(dòng)喚醒才能恢復(fù)TIMED_WAITING限時(shí)等待設(shè)置等待時(shí)長(zhǎng)超時(shí)自動(dòng)蘇醒也可提前喚醒TERMINATED終止run()執(zhí)行完畢或異常退出線程生命周期結(jié)束3.2 狀態(tài)的轉(zhuǎn)移簡(jiǎn)單示例public class Demo7 { public static void main(String[] args) { Thread tnew Thread(()-{ System.out.println(hello my thread); }); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); } }結(jié)果4、多線程的安全問題4.1 不安全的情況public class Demo9 { public static int count 0; public static void main(String[] args) throws InterruptedException { Thread t1new Thread(()-{ for(int i0;i50000;i){ count; } }); Thread t2new Thread(()-{ for(int i0;i50000;i){ count; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }結(jié)果按照預(yù)期來講結(jié)果應(yīng)該是100000但是為什么是小于100000呢因?yàn)閏ount不是原子操作其指令可拆解成1. 讀取 count 當(dāng)前值load 2. 執(zhí)行數(shù)值 1 運(yùn)算add 3. 把新值寫回 count 變量store這三步中間線程可以被 CPU 調(diào)度打斷就會(huì)發(fā)生覆蓋丟失。假設(shè) count 10 t1 讀到 count10正要 1 CPU 切換到 t2t2 也讀到 count10 t1 計(jì)算得到 11寫回 count11 t2 計(jì)算得到 11寫回 count11循環(huán) 5 萬次大量這種沖突疊加最終結(jié)果必然 100000。解決方法count外加鎖4.2 不安全的原因1. 線程調(diào)度是隨機(jī)的操作系統(tǒng)采用搶占式調(diào)度線程獲取 CPU 執(zhí)行權(quán)、被掛起的時(shí)機(jī)完全隨機(jī)不可預(yù)測(cè)。原本單線程串行執(zhí)行的代碼在多線程下會(huì)被任意切割、穿插執(zhí)行所有并發(fā)安全問題的底層根源都來源于此。編寫并發(fā)代碼時(shí)必須兼容所有調(diào)度時(shí)序才能保證最終結(jié)果準(zhǔn)確。2. 多個(gè)線程同時(shí)修改一個(gè)變量多個(gè)線程同時(shí)讀寫堆中靜態(tài)變量、成員變量等公共資源共享變量?jī)H有一份存儲(chǔ)在主內(nèi)存中直接產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)。若線程僅操作各自的局部私有變量不存在資源爭(zhēng)搶天然不會(huì)發(fā)生線程安全問題。3. 修改操作不是原子的原子操作指不可被 CPU 中斷、一次性執(zhí)行完成的指令。count 10 單純賦值屬于原子操作count 自增是復(fù)合操作底層拆分為三步讀取變量值 → 數(shù)值自增運(yùn)算 → 結(jié)果寫回變量。在線程切換時(shí)極易被打斷出現(xiàn)兩次自增只生效一次的丟失更新問題最終統(tǒng)計(jì)值小于預(yù)期。4. 內(nèi)存可見性1JMM 內(nèi)存模型基礎(chǔ)Java 內(nèi)存模型JMM為屏蔽不同 CPU、操作系統(tǒng)緩存架構(gòu)差異做了兩層抽象主內(nèi)存對(duì)應(yīng) JVM 堆存放所有線程共享變量訪問速度慢工作內(nèi)存線程私有抽象空間底層為 CPU 寄存器 L1/L2/L3 高速緩存讀寫速度極快。線程不會(huì)直接操作主內(nèi)存會(huì)先將變量拷貝到私有緩存運(yùn)算再同步回主內(nèi)存。2可見性異常產(chǎn)生過程JIT 即時(shí)編譯器遵循as-if-serial規(guī)則僅保證單線程運(yùn)行結(jié)果正確會(huì)對(duì)高頻只讀代碼做性能優(yōu)化。以空自旋while (flag 0)為例JIT 判定該循環(huán)只讀取不修改flag僅第一次從主內(nèi)存加載值存入 CPU 緩存后續(xù)循環(huán)直接復(fù)用緩存舊數(shù)據(jù)不再訪問主內(nèi)存。當(dāng)t2修改flag并更新至主內(nèi)存后t1依舊讀取過期緩存值導(dǎo)致無限死循環(huán)無法退出。package Thread; import java.util.Scanner; public class Demo15 { // 未加volatile觸發(fā)可見性問題 public static int flag 0; public static void main(String[] args) throws InterruptedException { Thread t1 new Thread(() - { while (flag 0) { // 空循環(huán)極易被JIT優(yōu)化 } System.out.println(t1 結(jié)束); }); Thread t2 new Thread(() - { Scanner sc new Scanner(System.in); System.out.println(請(qǐng)輸入flag的值); flag sc.nextInt(); System.out.println(t2 結(jié)束); }); t1.start(); t2.start(); t1.join(); t2.join(); } }3volatile 解決可見性及局限性1核心作用禁止 JIT 對(duì)變量做本地緩存優(yōu)化強(qiáng)制每次讀寫都直接操作主內(nèi)存通過內(nèi)存屏障禁止指令重排序保障執(zhí)行有序性。2重要局限volatile只保證可見性、有序性不具備原子性無法解決count這類復(fù)合運(yùn)算并發(fā)問題僅適用于單線程寫入、多線程讀取的場(chǎng)景多線程同時(shí)修改共享變量必須使用synchronized或Lock鎖。4volatile 與 synchronized 簡(jiǎn)要對(duì)比volatile輕量級(jí)無鎖解決可見性與有序性不保證原子性synchronized重量級(jí)互斥鎖同時(shí)保證原子性、可見性、有序性會(huì)阻塞競(jìng)爭(zhēng)線程。5. 指令重排序1重排序定義編譯器、CPU 為提升執(zhí)行效率在單線程運(yùn)行結(jié)果不變的前提下打亂代碼原有書寫順序重新編排指令屬于正常性能優(yōu)化。單線程環(huán)境無影響多線程并發(fā)時(shí)會(huì)破壞業(yè)務(wù)邏輯。2經(jīng)典風(fēng)險(xiǎn)案例對(duì)象初始化重排序?qū)ο髣?chuàng)建邏輯理想有序三步分配堆內(nèi)存空間執(zhí)行構(gòu)造方法完成對(duì)象初始化將內(nèi)存地址賦值給引用變量指令重排序可能打亂為分配內(nèi)存 → 賦值引用地址 → 執(zhí)行構(gòu)造方法初始化。其他線程若拿到未初始化完畢的對(duì)象引用會(huì)觸發(fā)空指針、成員變量默認(rèn)值異常等嚴(yán)重 bug。3約束方案volatile添加內(nèi)存屏障禁止變量讀寫前后指令重排synchronized同步代碼塊鎖保證臨界區(qū)指令串行執(zhí)行規(guī)避重排序干擾。4.3 解決這個(gè)問題5、synchronized關(guān)鍵字5.1 該關(guān)鍵字的特性1. 互斥synchronized會(huì)起到互斥效果,某個(gè)線程執(zhí)?到某個(gè)對(duì)象的synchronized中時(shí),其他線程如果也執(zhí)? 到同?個(gè)對(duì)象synchronized就會(huì)阻塞等待進(jìn)?synchronized修飾的代碼塊,相當(dāng)于加鎖退出synchronized修飾的代碼塊,相當(dāng)于解鎖2. 可重入同一個(gè)線程已經(jīng)拿到了某個(gè)對(duì)象的鎖之后可以再次重復(fù)獲取這把鎖不會(huì)自己阻塞自己這就是鎖的可重入機(jī)制。5.2 簡(jiǎn)要案例比如前面內(nèi)容涉及的count數(shù)小于100000的情況可以通過加鎖解決public class Demo12 { static class Counter{ private static int count 0; Object lockernew Object(); public void add(){ synchronized(locker){ count; } } } public static void main(String[] args) throws InterruptedException { Counter counternew Counter(); Thread t1new Thread(()-{ for(int i0;i50000;i){ counter.add(); } }); Thread t2new Thread(()-{ for(int i0;i50000;i){ counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }結(jié)果