Java多线程详解[狂神说Java]
文章目錄
- 多線程01: 線程的生命周期
- 多線程02:創(chuàng)建線程:繼承Thread類
- 案例:下載圖片
- 多線程03: 創(chuàng)建線程:實現(xiàn)Runnable接口
- 案例:多線程操作同一資源對象
- 案例:龜兔賽跑
- 多線程04:創(chuàng)建線程:實現(xiàn)Callable接口
- 多線程05:lambda表達式
- 多線程06:線程狀態(tài)
- 多線程07:線程的優(yōu)先級
- 多線程08:守護線程(daemon)
- 多線程09:線程同步
- 多線程10:死鎖
- 多線程11:線程協(xié)作
- 參考資料
Java支持多線程編程。線程的概念是什么呢?一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發(fā)多個線程,每條線程并行執(zhí)行不同的任務。
那什么是進程呢? 進程是操作系統(tǒng)分配資源的最小單位,一個進程包括由操作系統(tǒng)分配的內(nèi)存空間、一個或多個線程。注意,線程不能單獨存在,它必須是進程的一部分。一個進程一直運行,直到所有的非守護線程都結束運行后才能結束。注:守護線程(Daemon,發(fā)音:英 [?di?m?n] )
多線程能滿足程序員編寫高效率的程序來達到充分利用cpu的目的。
多線程是多任務的一種特別形式,但多線程使用了更小的資源開銷。
多線程01: 線程的生命周期
線程是一個動態(tài)執(zhí)行的過程,它有一個從產(chǎn)生到死亡的過程。
下圖顯示了一個線程完整的生命周期:
來源:菜鳥教程
線程的五大狀態(tài)
-
新建狀態(tài)
使用new關鍵字和Thread類或其子類建立一個線程對象后,該線程對象就處于新建狀態(tài)。它保持這個狀態(tài)直到程序start()這個線程。 -
就緒狀態(tài)
當線程對象調用了start()方法之后,該線程就進入了就緒狀態(tài)。就緒狀態(tài)的線程處于就緒隊列中,要等待JVM里線程調度器的調度。 -
運行狀態(tài)
如果就緒狀態(tài)的線程獲取CPU的資源,就可以執(zhí)行run()方法,此時線程便處于運行狀態(tài)。處于運行狀態(tài)的線程有多種變化方式,它可以變?yōu)樽枞麪顟B(tài)、就緒狀態(tài)和死亡狀態(tài)。 -
阻塞狀態(tài)
如果一個線程執(zhí)行了sleep(睡眠)、suspend(掛起)等方法,失去所占有的資源后,該線程就從運行狀態(tài)轉為阻塞狀態(tài)。在睡眠時間已到或獲得設備資源后可以重新進入就緒狀態(tài)。可以分為三種:
1 . 等待阻塞:運行狀態(tài)下的線程執(zhí)行wait()方法,使線程進入到等待隊列。
2 . 同步阻塞:線程在獲得synchronized同步鎖失敗(因為同步鎖被其他線程占用)。
3 . 其他阻塞:通過調用線程的sleep() 或join()發(fā)出I/O請求時,線程就會進入阻塞狀態(tài)。當sleep()狀態(tài)超時, 或join()等待線程終止或超時,或者I/O處理完畢,線程重新轉入就緒狀態(tài)。 -
死亡狀態(tài)
一個運行狀態(tài)的線程 完成任務或其他終止條件發(fā)生時,該線程就切換到終止狀態(tài)。
多線程02:創(chuàng)建線程:繼承Thread類
Java提供了三種創(chuàng)建線程的方法:
輔助幫助文檔:Class Thread
來源:Java Platform SE 8
Thread
三個步驟:1.自定義線程類Thread類;2.重寫run()方法,編寫線程結構體;3.創(chuàng)建線程對象,調用start()方法啟動線程。
線程不一定立即執(zhí)行,CPU安排調度。
package lishizheng.demo01;//創(chuàng)建線程方式1:繼承Thread類,重寫run方法,調用start開啟線程 public class TestThread1 extends Thread {@Overridepublic void run() {//run方法線程體for (int i = 0; i < 20; i++) {System.out.println("我在看代碼:" + i);}}public static void main(String[] args) {//main線程,主線程//創(chuàng)建一個線程對象TestThread1 testThread1 = new TestThread1();//調用start方法開啟線程testThread1.start();for (int i = 0; i < 1000; i++) {System.out.println("我在學習多線程: " + i);}} }案例:下載圖片
首先下載名為commons-io的jar包:Download Apache Commons IO
解壓之后如下圖:
復制上圖中的jar包,在IDEA項目中新建文件夾lib,然后選中它,然后Ctrl+V,將jar包導入項目。
導入后,右擊lib文件夾,選擇Add As Library
彈出如下界面:點擊OK即可。
代碼如下:
package lishizheng.demo01;import org.apache.commons.io.FileUtils;import java.io.File; import java.io.IOException; import java.net.URL;//練習Thread,實現(xiàn)多線程下載圖片 public class TestThread2 extends Thread {private String url; //網(wǎng)絡圖片地址private String name; //保存的文件名public TestThread2(String url, String name){this.url = url;this.name = name;}//下載圖片線程的執(zhí)行體@Overridepublic void run() {WebDownLoader webDownLoader = new WebDownLoader();webDownLoader.downloader(url,name);System.out.println("已下載文件名為:" + name + "的圖片");}public static void main(String[] args) {//確保圖片ulr是正確的TestThread2 t1 = new TestThread2("https://img-blog.csdnimg.cn/20210210194321356.png","我的博客圖片01.png");TestThread2 t2 = new TestThread2("https://img-blog.csdnimg.cn/20210228094648502.png","我的博客圖片02.png");TestThread2 t3 = new TestThread2("https://img-blog.csdnimg.cn/20210303104522231.png","我的博客圖片03.png");t1.start(); //啟動線程t2.start();t3.start();} }//下載器 class WebDownLoader{//下載方法public void downloader(String url, String name){try {FileUtils.copyURLToFile(new URL(url), new File(name)); //調用FileUtils類中的copyURLToFile方法實現(xiàn)下載} catch (IOException e) {e.printStackTrace();System.out.println("IO異常:downloader方法出現(xiàn)問題");}} } /* 執(zhí)行結果: 已下載文件名為:我的博客圖片01.png的圖片 已下載文件名為:我的博客圖片02.png的圖片 已下載文件名為:我的博客圖片03.png的圖片Process finished with exit code 0*/多線程03: 創(chuàng)建線程:實現(xiàn)Runnable接口
實現(xiàn)Runnable步驟:1.定義MyRunnable類實現(xiàn)Runnable接口;2.實現(xiàn)run()方法,編寫線程執(zhí)行體;3.創(chuàng)建線程對象,調用start()方法啟動線程。
創(chuàng)建一個線程,最簡單的方法是創(chuàng)建一個實現(xiàn) Runnable 接口的類。如下:
public class TestThread3 implements Runnable{}在創(chuàng)建一個實現(xiàn) Runnable 接口的類之后可以在類中實例化一個線程對象:
//創(chuàng)建Runnable接口的實現(xiàn)類對象TestThread3 testThread3 = new TestThread3();//創(chuàng)建線程對象,通過線程對象來開啟我們的線程,代理Thread thread = new Thread(testThread3);新線程創(chuàng)建之后,許喲啊調用它的 start() 方法它才會運行:
thread.start();創(chuàng)建對象并調用start方法可以用一句代碼實現(xiàn):
new Thread(testThread3).start();//等價寫法全部代碼舉例如下:
package lishizheng.demo01;//創(chuàng)建線程方式2:實現(xiàn)Runnable接口,重寫run方法,執(zhí)行線程需要丟入實現(xiàn)Runnable接口的實現(xiàn)類 public class TestThread3 implements Runnable {@Overridepublic void run() {//run方法線程體for (int i = 0; i < 20; i++) {System.out.println("我在看代碼:" + i);}}public static void main(String[] args) {//創(chuàng)建Runnable接口的實現(xiàn)類對象TestThread3 testThread3 = new TestThread3();//創(chuàng)建線程對象,通過線程對象來開啟我們的線程,代理Thread thread = new Thread(testThread3);thread.start();//等價寫法:// new Thread(testThread3).start();for (int i = 0; i < 1000; i++) {System.out.println("我在學習多線程: " + i);}} }/* 運行代碼其中一部分:可以發(fā)現(xiàn)線程的執(zhí)行順序是由CPU來調度的!!!并不完全按照代碼書寫的順序。 我在學習多線程: 0 我在看代碼:0 我在學習多線程: 1 我在學習多線程: 2 我在學習多線程: 3 我在學習多線程: 4 我在學習多線程: 5 我在學習多線程: 6 我在學習多線程: 7 我在學習多線程: 8 我在學習多線程: 9 我在學習多線程: 10 我在學習多線程: 11 我在學習多線程: 12 我在學習多線程: 13 我在學習多線程: 14 我在學習多線程: 15 我在看代碼:1 我在學習多線程: 16 我在看代碼:2 我在學習多線程: 17 我在看代碼:3 我在學習多線程: 18*/小結:
- 繼承Thread類
- 實現(xiàn)Runnable接口
案例:多線程操作同一資源對象
下面的代碼實現(xiàn)搶票的功能:共有10張火車票,三個人來搶。目的是學習并體會多線程對同一個資源對象操作的情況。
補充Thread.currentThread().getName()獲取線程的名字
package lishizheng.demo01;//多個線程同時操作同一個對象 //買火車票的例子//發(fā)現(xiàn)問題:多個線程操作同一個資源的情況下,線程不安全,數(shù)據(jù)紊亂 public class TestThread4 implements Runnable {private int ticketNums = 10;@Override//重寫run方法public void run() {while (true){if(ticketNums <= 0) break;System.out.println( Thread.currentThread().getName() + "拿到了第 " + ticketNums-- +"張票");}}public static void main(String[] args) {TestThread4 testThread4 = new TestThread4();// 一個對象,三個線程 new Thread(testThread4, "小明").start();new Thread(testThread4,"老師").start();new Thread(testThread4,"黃牛").start();} } /* 輸出結果如下: 黃牛拿到了第 10張票 老師拿到了第 8張票 小明拿到了第 9張票 老師拿到了第 6張票 黃牛拿到了第 7張票 老師拿到了第 4張票 小明拿到了第 5張票 老師拿到了第 2張票 黃牛拿到了第 3張票 小明拿到了第 1張票Process finished with exit code 0*/案例:龜兔賽跑
模擬龜兔賽跑,兩者在同一條跑道,先跑到100步者為勝利者。
用意:體會多線程競爭資源。
下面使用到下圖中的兩個方法:
來源:菜鳥教程
多線程04:創(chuàng)建線程:實現(xiàn)Callable接口
實現(xiàn)Callable接口的步驟:
利用Callable修改上面下載圖片案例
package lishizheng.demo02;import org.apache.commons.io.FileUtils;import java.io.File; import java.io.IOException; import java.net.URL; import java.util.concurrent.*;//線程創(chuàng)建3:實現(xiàn)Callable接口 /* * * */ public class TestCallable implements Callable<Boolean> {private String url; //網(wǎng)絡圖片地址private String name; //保存的文件名public TestCallable(String url, String name){this.url = url;this.name = name;}//下載圖片線程的執(zhí)行體@Overridepublic Boolean call() {WebDownLoader webDownLoader = new WebDownLoader();webDownLoader.downloader(url,name);System.out.println("已下載文件名為:" + name + "的圖片");return true;}public static void main(String[] args) throws ExecutionException, InterruptedException {//確保圖片ulr是正確的TestCallable t2 = new TestCallable("https://img-blog.csdnimg.cn/20210228094648502.png","我的博客圖片02.png");TestCallable t3 = new TestCallable("https://img-blog.csdnimg.cn/20210303104522231.png","我的博客圖片03.png");TestCallable t1 = new TestCallable("https://img-blog.csdnimg.cn/20210210194321356.png","我的博客圖片01.png");//創(chuàng)建執(zhí)行服務ExecutorService ser = Executors.newFixedThreadPool(3);//提交執(zhí)行Future<Boolean> r1 = ser.submit(t1);Future<Boolean> r2 = ser.submit(t2);Future<Boolean> r3 = ser.submit(t3);//獲取結果boolean res1 = r1.get();boolean res2 = r2.get();boolean res3 = r3.get();//關閉服務ser.shutdown();} }//下載器 class WebDownLoader{//下載方法public void downloader(String url, String name){try {FileUtils.copyURLToFile(new URL(url), new File(name)); //調用FileUtils類中的copyURLToFile方法實現(xiàn)下載} catch (IOException e) {e.printStackTrace();System.out.println("IO異常:downloader方法出現(xiàn)問題");}} }/* 已下載文件名為:我的博客圖片01.png的圖片 已下載文件名為:我的博客圖片03.png的圖片 已下載文件名為:我的博客圖片02.png的圖片Process finished with exit code 0 */多線程05:lambda表達式
Lambda 表達式,也可稱為閉包,它是推動 Java 8 發(fā)布的最重要新特性。
Lambda 允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞進方法中)。
使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。
語法:
(parameters) -> expression 或 (parameters) -> {statements;}lambda表達式的重要特征:
可選類型聲明
不需要聲明參數(shù)類型,編譯器可以統(tǒng)一識別參數(shù)值。
可選的參數(shù)圓括號
一個參數(shù)無需定義圓括號,但多個參數(shù)需要定義圓括號
可選的大括號
如果主體包含了一個語句,就不需要使用大括號
可選的返回關鍵字
如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指明表達式返回了一個數(shù)值。
函數(shù)式接口(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口。
對于函數(shù)式接口我們可以使用lambda表達式來創(chuàng)建該接口的對象。
//1.定義一個函數(shù)式接口 interface ILike{void lambda(); }//6.用lambda簡化ILike like5 = ()-> {System.out.println("I like Lambda5");};like5.lambda();/* 輸出:I like Lambda5 */下面是推導lambda表達式的過程,一步一步化簡,思路是先從用類實現(xiàn)接口,到靜態(tài)內(nèi)部類,到局部內(nèi)部類,再到匿名內(nèi)部類,一步步簡化,到最后是lambda表達式。
package lishizheng.demo03;//推導lambda表達式 public class TestLambda {//3.靜態(tài)內(nèi)部類static class Like2 implements ILike{@Overridepublic void lambda() {System.out.println("I like Lambda2");}}public static void main(String[] args) {Like like = new Like();like.lambda();Like2 like2 = new Like2();like2.lambda();//4. 局部內(nèi)部類class Like3 implements ILike{@Overridepublic void lambda() {System.out.println("I like Lambda3");}}Like3 like3 = new Like3();like3.lambda();//5.匿名內(nèi)部類,沒有類的名稱,必須借助接口或者父類ILike like4 = new ILike() {@Overridepublic void lambda() {System.out.println("I like Lambda4");}};like4.lambda();//6.用lambda簡化ILike like5 = ()-> {System.out.println("I like Lambda5");};like5.lambda();} }//1.定義一個函數(shù)式接口 interface ILike{void lambda(); }//2.實現(xiàn)類 class Like implements ILike{@Overridepublic void lambda() {System.out.println("I like Lambda");} }/* 輸出結果: I like Lambda I like Lambda2 I like Lambda3 I like Lambda4 I like Lambda5Process finished with exit code 0 */靜態(tài)代理模式
//真實對象和代理對象到實現(xiàn)同一個接口
//代理對象要代理真實對象
好處:代理對象可以做很多真實對象做不了的事情 ;真實對象專注于自己的事情
舉例:下面通過婚慶公司代理你來組織婚禮來說明一下靜態(tài)代理模式的功能。結婚之前要布置現(xiàn)場,然后主人結婚,結婚之后收尾款,這些方法都由婚慶公司對象來調用。
package lishizheng.demo04;//靜態(tài)代理模式//真實對象和代理對象到實現(xiàn)同一個接口 //代理對象到代理真實對象//好處:代理對象可以做很多真實對象做不了的事情 ;真實對象專注于自己的事情public class StaticProxy {public static void main(String[] args) {You you = new You();WeddingCompany weddingCompany = new WeddingCompany(you);weddingCompany.happyMarry();} }interface Marry{void happyMarry(); }class You implements Marry{@Overridepublic void happyMarry() {System.out.println("結婚,超開心");} }//代理角色 class WeddingCompany implements Marry{//代理誰? 真實目標角色private Marry target;public WeddingCompany(Marry target) {this.target = target;}@Overridepublic void happyMarry() {before();this.target.happyMarry();//真實對象after();}private void after() {System.out.println("結婚之后收尾款");}private void before() {System.out.println("結婚之前布置現(xiàn)場");} } /* 輸出結果:結婚之前布置現(xiàn)場 結婚,超開心 結婚之后收尾款Process finished with exit code 0 */和多線程有什么關系呢?
復習Thread調用方法的時候,原理是一樣的,它本身是Runnable接口的代理。 下面通過婚慶公司和線程進行對比。
public class StaticProxy {public static void main(String[] args) {You you = new You();//Thread代理真實的Runnable接口,new Thread(() -> System.out.println("我愛你") ).start();new WeddingCompany(you).happyMarry();} }多線程06:線程狀態(tài)
停止線程stop
- 不推薦使用JDK提供的stop方法,destroy方法
- 推薦讓線程自己停止下來,建議使用標志位進行終止變量:當flag == false時,線程終止。
舉例如下: 測試主線程和teststop線程的執(zhí)行過程,運行期間讓teststop停止。
package lishizheng.demo05;//測試stop //1.建議線程正常停止:利用此時,不建議死循環(huán) //2.建議使用標志位 public class TestStop implements Runnable {// 1.設置一個標志位private boolean flag = true;@Overridepublic void run() {int i = 0;while (flag){System.out.println("run...Thread: " + i++);}}//2.設置公開的方法停止線程public void stop(){this.flag = false;}public static void main(String[] args) {TestStop testStop = new TestStop();new Thread(testStop).start();for (int i = 0; i < 30; i++) {System.out.println("main " +i);if( i == 20){//調用stop方法切換線程標志位,讓線程停止testStop.stop();System.out.println("線程已停止");}}} }/* 輸出結果: main 0 run...Thread: 0 main 1 run...Thread: 1 main 2 run...Thread: 2 main 3 run...Thread: 3 main 4 main 5 run...Thread: 4 main 6 run...Thread: 5 main 7 main 8 main 9 main 10 main 11 main 12 main 13 main 14 main 15 main 16 main 17 main 18 main 19 main 20 run...Thread: 6 線程已停止 main 21 main 22 main 23 main 24 main 25 main 26 main 27 main 28 main 29Process finished with exit code 0*/線程休眠sleep
- sleep指定當前線程阻塞的毫秒數(shù),時間到后線程進入就緒狀態(tài)
- sleep存在異常InterruptedException,要拋出
- sleep可以模擬網(wǎng)絡延遲,倒計時等
- 每個對象都有一個鎖,sleep不會釋放鎖。
舉例:sleep模擬網(wǎng)絡延時,發(fā)現(xiàn)代碼的漏洞:這里是多線程操作同一個對象,造成線程不安全。通過sleep延時可以發(fā)現(xiàn)程序的執(zhí)行過程并不是我們預計的那樣。
package lishizheng.demo05;//模擬網(wǎng)絡延時:放大問題的可能性,容易發(fā)現(xiàn)問題 public class TestSleep implements Runnable{private int ticketNums = 10;@Overridepublic void run() {while (true){if(ticketNums <= 0) break;try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println( Thread.currentThread().getName() + "拿到了第 " + ticketNums-- +"張票");}}public static void main(String[] args) {//線程不安全:多個線程操作同一個對象TestSleep testThread4 = new TestSleep();new Thread(testThread4, "小明").start();new Thread(testThread4,"老師").start();new Thread(testThread4,"黃牛").start();} }/* 運行結果: 黃牛拿到了第 9張票 老師拿到了第 8張票 小明拿到了第 10張票 老師拿到了第 7張票 小明拿到了第 7張票 黃牛拿到了第 6張票 黃牛拿到了第 5張票 小明拿到了第 4張票 老師拿到了第 5張票 小明拿到了第 3張票 黃牛拿到了第 1張票 老師拿到了第 2張票Process finished with exit code 0 */舉例
模擬倒計時和模擬獲取系統(tǒng)時間
線程禮讓yield
線程禮讓,讓當前正在執(zhí)行的線程暫停,但不阻塞;將線程從運行狀態(tài)轉為就緒狀態(tài)。讓CPU重新調度,禮讓不一定成功,看CPU心情。
測試:
package lishizheng.demo05;//測試禮讓線程 //禮讓不一定成功,看CPU心情 public class TestYield {public static void main(String[] args) {MyYield myYield = new MyYield();new Thread(myYield,"a").start();new Thread(myYield,"b").start();} }class MyYield implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"線程開始執(zhí)行");Thread.yield();System.out.println(Thread.currentThread().getName()+"線程停止執(zhí)行");} } /*a線程開始執(zhí)行 b線程開始執(zhí)行 a線程停止執(zhí)行 b線程停止執(zhí)行Process finished with exit code 0 */join
join相當于線程插隊,執(zhí)行完插隊線程才能繼續(xù)執(zhí)行別的線程。
測試:
觀察線程狀態(tài):
多線程07:線程的優(yōu)先級
每一個 Java 線程都有一個優(yōu)先級,這樣有助于操作系統(tǒng)確定線程的調度順序。
Java 線程的優(yōu)先級是一個整數(shù),其取值范圍是 1 ~10(Thread.MIN_PRIORITY ~Thread.MAX_PRIORITY )。
默認情況下,每一個線程都會分配一個優(yōu)先級 NORM_PRIORITY(5)。
具有較高優(yōu)先級的線程對程序更重要,并且應該在低優(yōu)先級的線程之前分配處理器資源。但是,線程優(yōu)先級不能保證線程執(zhí)行的順序,而且非常依賴于平臺。 優(yōu)先級高的線程,先執(zhí)行的概率大。
測試線程優(yōu)先級
package lishizheng.demo05;public class TestPriority {public static void main(String[] args) {//主線程默認優(yōu)先級System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());MyPriority myPriority = new MyPriority();Thread t1 = new Thread(myPriority);Thread t2 = new Thread(myPriority);Thread t3 = new Thread(myPriority);Thread t4 = new Thread(myPriority);Thread t5 = new Thread(myPriority);//先設置優(yōu)先級t1.start();t2.setPriority(1);t2.start();t3.setPriority(4);t3.start();t4.setPriority(Thread.MAX_PRIORITY);t4.start();} }class MyPriority implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());} }/* main--->5 Thread-0--->5 Thread-2--->4 Thread-3--->10 Thread-1--->1Process finished with exit code 0 */多線程08:守護線程(daemon)
守護線程是指為其他線程服務的線程。在JVM中,所有非守護線程都執(zhí)行完畢后,無論有沒有守護線程,虛擬機都會自動退出。
線程分為用戶線程和守護線程,虛擬機必須確保用戶線程執(zhí)行完畢,虛擬機不用等待守護線程執(zhí)行完畢,守護線程比如監(jiān)控內(nèi)存、垃圾回收等。
測試用例:守護線程thread.setDaemon(true);//默認是false,表示是用戶線程會在JVM結束后接著運行一段時間。
package lishizheng.demo05;//測試守護線程 public class TestDaemon {public static void main(String[] args) {God god = new God();You you = new You();Thread thread = new Thread(god);thread.setDaemon(true);//默認是false,表示是用戶線程thread.start();//上帝線程new Thread(you).start();//用戶線程啟動} }class God implements Runnable{@Overridepublic void run() {while (true){System.out.println("上帝保佑著你");}} }class You implements Runnable{@Overridepublic void run() {for (int i = 1; i < 100; i++) {System.out.println("已經(jīng)開心活過了"+ i +"年");}System.out.println("goodbye world");} }/* 部分運行結果 已經(jīng)開心活過了92年 已經(jīng)開心活過了93年 已經(jīng)開心活過了94年 已經(jīng)開心活過了95年 已經(jīng)開心活過了96年 已經(jīng)開心活過了97年 已經(jīng)開心活過了98年 已經(jīng)開心活過了99年 goodbye world 上帝保佑著你 上帝保佑著你 上帝保佑著你 上帝保佑著你 上帝保佑著你 Process finished with exit code 0*/多線程09:線程同步
線程同步是控制線程執(zhí)行的先后順序。
線程同步:即當有一個線程在對內(nèi)存進行操作時,其他線程都不可以對這個內(nèi)存地址進行操作,直到該線程完成操作, 其他線程才能對該內(nèi)存地址進行操作,而其他線程又處于等待狀態(tài),實現(xiàn)線程同步的方法有很多,臨界區(qū)對象就是其中一種。
多線程同時讀寫共享變量時,會造成邏輯錯誤,因此需要通過synchronized同步;
線程不安全舉例:
package lishizheng.demo06;//不安全地買票 public class UnsafeBuyTicket {public static void main(String[] args) {BuyTicket buyTicket = new BuyTicket();new Thread(buyTicket,"小米").start();new Thread(buyTicket,"小明").start();new Thread(buyTicket,"黃牛").start();} }class BuyTicket implements Runnable{private int ticketNums = 10;boolean flag = true;@Overridepublic void run() {//模擬延時try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//買票while (flag){buy();}}private void buy(){if(ticketNums <= 0) {flag = false;return;}System.out.println(Thread.currentThread().getName() + " 拿到" + ticketNums--);} }同步方法
同步塊:synchronized (Obj) {},Obj稱為同步監(jiān)視器。
- Obj可以是任何對象,但是推薦使用共享資源作為同步監(jiān)視器
- 同步方法中無需指定同步監(jiān)視器,因為同步方法的同步監(jiān)視器就是this,就是這個對象本身或者是class
同步監(jiān)視器的執(zhí)行過程
擴充指示JUC:并發(fā)編程的安全性。
package lishizheng.demo06;import java.util.concurrent.CopyOnWriteArrayList;//測試JUC安全類型的集合 public class TestJUC {public static void main(String[] args) throws InterruptedException {CopyOnWriteArrayList<String > list = new CopyOnWriteArrayList<String >();for (int i = 0; i < 10000; i++) {new Thread(() -> {list.add(Thread.currentThread().getName());}).start();}Thread.sleep(3000);System.out.println(list.size());} }多線程10:死鎖
多個線程各自占有一些共享資源,并且互相等待其他線程占有的資源才能運行,而導致兩個或多個線程都在等待對方釋放資源,都停止運行的情形。某個同步塊擁有"兩個以上對象的鎖"時,就可能發(fā)生死鎖。
死鎖舉例 :
灰姑娘和白雪公主都喜歡化妝,這里的化妝需要鏡子和口紅兩者都具備才能完成。 當灰姑娘拿到口紅,而白雪拿到鏡子的時候,兩者相互等待,這樣就構成死鎖。
程序進入死鎖(卡死),如下圖:
測試代碼:
死鎖避免的方法
產(chǎn)生死鎖的四個必要條件:
只要想辦法破壞上面任意一個或者多個就可以避免死鎖。
Lock(鎖)
從JDK 5.0開始,Java提供了更強大的線程同步機制:通過顯式定義同步鎖對象來實現(xiàn)同步。同步鎖使用Lock對象充當。java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。
鎖提供了對共享資源的獨占訪問,每次只能有1個對象對Lock對象加鎖,線程開始訪問共享資源之前要先獲得Lock對象。
ReentrantLock類實現(xiàn)了Lock,它擁有了與synchronized相同的并發(fā)性和內(nèi)存語義,在實現(xiàn)線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。
測試代碼:使用reentrantLock顯式定義鎖和解鎖。
package lishizheng.advance;import java.util.concurrent.locks.ReentrantLock;//測試Lock鎖 public class TestLock {public static void main(String[] args) {TestLock2 testLock2 = new TestLock2();new Thread(testLock2).start();new Thread(testLock2).start();new Thread(testLock2).start();} }class TestLock2 implements Runnable{private int ticketNums = 10;//定義lock鎖//可重入鎖 re + entrant + lock private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true){try {lock.lock();//加鎖if(ticketNums > 0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(ticketNums--);}else break;}finally {//解鎖lock.unlock();}}} }synchronized 和Lock對比
多線程11:線程協(xié)作
生產(chǎn)者消費者問題
生產(chǎn)者消費者問題(英語:Producer-consumer problem),也稱有限緩沖問題(Bounded-buffer problem),是一個多進程同步問題的經(jīng)典案例。該問題描述了共享固定大小緩沖區(qū)的兩個進程——即所謂的“生產(chǎn)者”和“消費者”——在實際運行時會發(fā)生的問題。生產(chǎn)者的主要作用是生成一定量的數(shù)據(jù)放到緩沖區(qū)中,然后重復此過程。與此同時,消費者也在緩沖區(qū)消耗這些數(shù)據(jù)。該問題的關鍵就是要保證生產(chǎn)者不會在緩沖區(qū)滿時加入數(shù)據(jù),消費者也不會在緩沖區(qū)中空時消耗數(shù)據(jù)。
來源:維基百科
線程通信
Java提供了幾個方法解決線程之間的通信問題
| wait() | 表示線程一直等待,直到其他線程的通知,會釋放鎖 |
| wait(long tiimeout) | 指定等待的毫秒數(shù) |
| notify() | 喚醒一個處于等待狀態(tài)的線程 |
| notifyAll() | 喚醒同一個對象上所有調用wait()方法的線程,優(yōu)先級別高的線程優(yōu)先被調度 |
注意:均為Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出異常IllegalMonitorStateException.
解決方法1:管程法
生產(chǎn)者將生產(chǎn)好的數(shù)據(jù)放入緩沖區(qū),消費者從緩沖區(qū)拿數(shù)據(jù)。
package lishizheng.demo06;//測試生產(chǎn)者消費者,利用緩沖區(qū)解決,即管程法//what do we need? //生產(chǎn)者,消費者,產(chǎn)品,緩沖區(qū) public class TestPC {public static void main(String[] args) {SynContainer container = new SynContainer();new Producer(container).start();new Consumer(container).start();} }//生產(chǎn)者 class Producer extends Thread{SynContainer container;public Producer( SynContainer container){this.container = container;}//生產(chǎn)@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("生產(chǎn)了 " + i + "只雞");container.push(new Chicken(i));}} }//消費者class Consumer extends Thread{SynContainer container;public Consumer( SynContainer container){this.container = container;}//消費@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("消費了第 " + container.pop().id + " 只雞");}} }//產(chǎn)品class Chicken{int id;public Chicken(int id) {this.id = id;} }//緩沖區(qū) class SynContainer{//容器大小10Chicken[] chickens = new Chicken[10];//容器計數(shù)器int count = 0;//生產(chǎn)者放入產(chǎn)品public synchronized void push(Chicken myChicken){//如果容器滿了,等待消費者消費if(count == 10){//通知消費者消費,生產(chǎn)者等待try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果未滿,則放入產(chǎn)品chickens[count] = myChicken;count++;//進程通信,通知 消費者消費this.notifyAll();}//消費者消費產(chǎn)品public synchronized Chicken pop(){//判斷能否消費if(count == 0){//等待生產(chǎn)者生產(chǎn),消費者等待try {wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果可以消費count --;Chicken chicken = chickens[count];//吃完了,通知生產(chǎn)者生產(chǎn)this.notifyAll();return chicken;} }解決方法2:信號燈法
暫無。
參考資料
[1]https://www.bilibili.com/video/BV1V4411p7EF?p=1
[2]https://www.runoob.com/java/java-multithreading.html
感謝您閱讀到最后,祝您一切順利。
總結
以上是生活随笔為你收集整理的Java多线程详解[狂神说Java]的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java入门学习笔记[狂神说Java]
- 下一篇: java美元兑换,(Java实现) 美元