java多線程編程從入門到卓越(超詳細總結)


導讀:java多線程編程不太熟?或是聽說過?或是想復習一下?找不到好的文章?別擔心我給你們又安利一波,文章內容很全,並且考慮到很多開發中遇到的問題和解決方案。循環漸進,通俗易懂,文章較長,建議收藏再看!

1.多線程的概念

  • 什么是進程?什么是線程?(了解這個還是很重要的,利於后面的學習,面試也常考)
    • 首先我們應該了解進程,在操作系統中進程是程序的一次執行。(也可理解為對靜態層序的一次動態實例化的過程),(在引入線程的操作系統中)線程是系統分配處理器時間資源的基本單元,或者說進程之內獨立執行的一個單元。對於操 作系統而言,其調度單元是線程。一個進程至少包括一個線程,通常將該線程稱為主線程。一個進程從主線程的執行開始進而創建一個或多個附加線程,就是所謂基於多線程的多任務。 那進程與線程的區別到底是什么?進程是執行程序的實例。
      在這里插入圖片描述
    • 為什么要引進進程?
      進程的引入是為了使多個程序並發執行以改善系統資源的利用率和系統的吞吐量。
      在這里插入圖片描述
    • 有了進程為什么還要引入線程呢?
      雖然進程能夠改善系統資源的利用率和系統的吞吐量。
      但是現實是:在多程序執行的情況下會生成多個進程,為了實現並發,系統會根據一定的算法進行進程切換來實現並發效果,但是進程的切換都會消耗較大的時空開銷來進行系統資源的重新分配,保存和釋放,如果進程一多,就會為其花費不少的處理機時間。
      線程的引入就是為了減少程序並發執行時所付出的時空開銷。線程本身不擁有資源。
      在這里插入圖片描述
      在這里插入圖片描述
      在這里插入圖片描述
    • 那么進程與線程之間有什么聯系和區別呢?
      在這里插入圖片描述
    • 一個進程可以啟動多個線程。
    • 一個對應一個應用程序。(軟件)
  • 對於java程序員來說,當在DOD窗口中輸入:java HelloWorld 層序來說:
    • 會先啟動JVM,而JVM就是一個進程。JVM在啟動一個主線程調用main方法。同時再啟動一個垃圾回收線程負責看護,回收垃圾。
  • 在沒有引入多線程編程之前程序實例都以順序方式執行,而多線程編程是一種並發的執行方式。
  • 進程之間內存不共享。
  • 線程之間內存共享嗎?
    • 在java中:
    • 線程之間共享堆內存和方法區內存;但是棧內存是獨立,一個線程一個棧。
    • 啟動10個線程,會有10個棧空間每個棧之間,互不干擾,各自執行各自的,這就是多線程並發。多線程提高了程序的處理效率。
    • 火車站,可以看作是一個進程,而火車站的每個售票窗口可以看作是一個線程。
  • 注意:使用了多線程之后,main方法結束,有可能程序還沒有結束。main方法結束只是主線程結束了,主棧空了,其他的線程可能還在壓棧彈棧。
    在這里插入圖片描述

2.多線程並發

  • 什么是真正的多線程並發?
    線程之間互不影響。
  • 對於單核CPU來說,真的可以做到真正的多線程並發嗎?
    • 多核CPU可以做到多線程並發。
    • 但是單核CPU不能真正做到,但是可以做到一種“多線程並發”的感覺。(多個線程之間頻繁切換執行,給人一種多個事情同時在做)

3.多線程程序設計

  • 分析以下程序中存在幾個線程:(除垃圾回收之外)
package Day3; public class ThreadTest1 { public static void main(String[] args) { System.out.println("main begin"); m1(); System.out.println("main end"); } private static void m1(){ System.out.println("m1 begin"); m2(); System.out.println("m1 end"); } private static void m2(){ System.out.println("m2 begin"); m3(); System.out.println("m2 end"); } private static void m3(){ System.out.println("m3 begin"); System.out.println("m3 end"); } } 

在這里插入圖片描述

  • 以上代碼順序執行,只有一個線程。
    java語言中,實現多線程有兩種方式。
    第一種是:編寫一個類繼承java.lang包下Thread類。
    第二種是:編寫一個類實現Runnable接口。

繼承Thread類創建線程

  • 如何創建線程對象呢? new就完事了。
  • 怎么啟動線程呢? 調用線程對象的start()方法就OK了。
  • 詳情請看下列代碼示意:(注釋是重點)
package Day3; //第一種:直接繼承Thread public class ThreadTest2 { public static void main(String[] args) { //這里是主線程,在主棧中執行 //新建一個分支線程 MyThread myThread = new MyThread(); //啟動線程 /*start方法的作用:啟動一個分支線程,在JVM種開辟一個新的棧空 間,這段代碼完成后瞬間就結束了,線程就啟動成功了*/ /*啟動成功的線程會自動調用run方法,並且run方法在分支棧的底部 run和main方法平級*/ myThread.start(); //myThread.run();//不會啟動動線程(為單線程) for(int i=0 ;i<100;i++){ System.out.println("主線程--->"+i); } } } class MyThread extends Thread{ @Override public void run() { //編寫代碼,這段程序在分支線程中執行 for(int i=0 ;i<100;i++){ System.out.println("分支線程--->"+i); } } } 
  • 直接使用run方法的內存分析圖:
    在這里插入圖片描述
  • run方法執行,其結果跟之前編寫的程序沒有區別。
  • 使用start方法內存圖分析:
    在這里插入圖片描述
  • 使用start方法,輸出結果有先有后,有多又少,這是為什么?
    • 控制台只有一個。
    • 占有CPU時間片的多少。
    • 搶時間片等等
  • 上述代碼多線程的執行結果。
    在這里插入圖片描述

新建類實現Runnable接口創建線程

  • 詳情請看注釋:
package Day3; public class ThreadTest3 { public static void main(String[] args) { //創建一個線程的對象 MyRunnable aa = new MyRunnable(); //將可運行的對象封裝成一個線程對象 Thread t =new Thread(aa); //啟動線程 t.start(); for(int i =0 ;i<1000;i++){ System.out.println("主線程-->"+i); } } } //這並不是一個線程類,是一個可運行的類,他還不是一個線程 class MyRunnable implements Runnable{ @Override public void run() { for(int i =0 ;i<1000;i++){ System.out.println("分支線程-->"+i); } } } 
  • 第二種:一個類實現了接口還可以去繼承其它的類,更加靈活。

改進(匿名內部類方式)

package Day3; public class ThreadTest4 { public static void main(String[] args) { Thread t = new Thread(new Runnable() {//new一個對象,且實現了Runnable接口 @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("分支線程--->" + i); } } }); //啟動線程 t.start(); for (int i = 0; i < 1000; i++) { System.out.println("主線程--->" + i); } } } 

獲取線程的名字和當前線程對象

  • 怎么獲取當前線程對象?
    • 所用的方法static Thread currectThread()為靜態方法。
    • Thread currentThread = Thread.currectThread();
  • 獲取線程對象的名字?
    • String name = 線程對象.getName();
  • 修改線程對象的名字?
    • 當線程沒有設置名字之前,默認有個名字Thread-0,Thread-1,Thread-2
    • 線程對象.setName(“s1”);方法修改線程的名字。
  • 口說無憑,舉個栗子:
package Day3; public class ThreadTest5 { public static void main(String[] args) { //以下代碼出現在main方法中,所以當前線程就是主線程,獲取當前線程對象 Thread current1 = Thread.currentThread(); System.out.println(current1.getName()); //創建線程對象 MyThread2 t1 = new MyThread2(); //設置線程的名字 t1.setName("sss"); //獲取線程的名字 System.out.println(t1.getName()); //創建線程對象 MyThread2 t2 = new MyThread2(); System.out.println(t2.getName()); //啟動線程 t1.start(); for(int i=0;i<10;i++){ Thread current2 = Thread.currentThread(); System.out.println(current2.getName()+"-->"+i); } } } class MyThread2 extends Thread{ @Override public void run() { //獲取當前對象 for (int i =0;i<1000;i++){ //獲取當前線程對象 Thread current2 = Thread.currentThread(); System.out.println(current2.getName()+"--->"+i); } } } 
  • 運行部分結果
    在這里插入圖片描述

4.線程的生命周期

  • 什么是線程的生命周期?
    指:線程創建到運行完畢的整個過程。
    在這里插入圖片描述
  • 新建(new Thread)
  • 就緒 (Runnable)
  • 運行(Running)
  • 阻塞(Blocked)
  • 消亡(Dead)
  • JVM的線程調度程序會自動處理相關過程。那為啥還要了解呢?
    當程序員想要干預這個線程的運行時,了解線程生命周期和相關知識就很重要了。

5.多線程的調度管理

  • 當多個線程對象要求cpu控制權的時候,就需要系統進行一定的管理,也就是進行調度。
  • 大多數cpu環境下,cpu一個時間點只能做一件事情,也就是說只能執行一個線程體,因此調度管理是很重要的。
    • 超線程技術:超線程技術是在一顆CPU同時執行多個程序而共同分享一顆CPU內的資源,理論上要像兩顆CPU一樣在同一時間執行兩個線程。雖然采用超線程技術能同時執行兩個線程,但它並不象兩個真正的CPU那樣,每個CPU都具有獨立的資源。當兩個線程都同時需要某一個資源時,其中一個要暫時停止,並讓出資源,直到這些資源閑置后才能繼續。因此超線程的性能並不等於兩顆CPU的性能。並且超線程:需要CPU支持,需要主板芯片組支持,需要BIOS支持,需要操作系統支持,需要應用軟件支持。目前很多應用軟件不支持多線程技術。(這里對於超線程做簡單了解)
    • Java中提供了優先級的概念,同一個優先級先來先服務,不同優先級線程對象按照優先級高低。
  • 常見的調度模型有哪些?
    • 搶占式調度模型:
      • 哪個線程的優先級比較高,搶到的cpu時間片的概率就高一些/多一些。Java采用的就是這個。
    • 均分式調度模型:
      • 均分cpu時間片,每個線程占有的cpu時間片長度一樣。有些編程語言采用這種模型。
  • 狀態切換的常用方法
  • void setPriority(int newPriotity)方法:設置線程的優先級。
  • int getPriority()方法:獲取線程的優先級。
  • 最低優先級1(MIN_PRIORITY)
  • 最高優先級10(MAX_PRIORITY)
  • 默認優先級5(NORM_PRIORITY)
  • join()/join(long millis)實例方法(線程合並):使用該方法,當前線程進入阻塞狀態,直到其它線程進入消亡后,才再次進入就緒狀態。
    • 口說無憑,舉個栗子:
package Day3; public class ThreadTest10 { public static void main(String[] args) { System.out.println("main begin!"); Thread f = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "-->" + i); } } }); f.setName("ttt"); f.start(); //合並到當前線程,當前線程受到阻塞,直到f線程執行結束 try { f.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main over!"); } } 

運行部分結果:
在這里插入圖片描述

  • yield()方法(線程讓位):靜態方法,暫停當前正在執行的線程對象(回到就緒狀態,回到就緒可能會重新搶到),並執行其它對象。(讓位方法,讓給同等優先權的線程,否則不起作用)
    • 口說無憑,舉個栗子:
package Day3; public class ThreadTest8 { public static void main(String[] args) { Thread t = new Thread(new MyRunnable3()); t.setName("t"); t.start(); for(int i =0;i<1000;i++){ System.out.println(Thread.currentThread().getName()+"-->"+i); } } } class MyRunnable3 implements Runnable{ @Override public void run() { for(int i =0;i<=1000;i++) { if (i %100==0){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"-->"+i); } } } 
  • Thread.sleep()/sleep(long millis)//靜態方法
    • 參數為毫秒。
    • 作用:讓當前線程進入休眠(重點需注意,面試常考:跟對象無關(靜態方法),用一個線程對象調用sleep方法,並不意味着改線程對象進入休眠),進入“阻塞狀態”,放棄占有cpu時間片,讓給其它線程使用。
    • 出現在哪里就讓當前線程睡覺。
    • 作用:不讓當前線程獨霸該進程獲取的cpu資源,留給其它線程。或者項目開發時讓某程序隔一段時間執行。
    • 口說無憑,舉個栗子:
package Day3; public class TreadTest6 { public static void main(String[] args) { //讓當前線程睡眠10秒,主線程 try { for(int i=0;i<10;i++) { System.out.println("Hello Wrold!"); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } } 
  • 怎么終止線程的休眠呢?
    • 線程對象.interrupted()方法:
      此方法發出中斷信號,並不會直接中斷線程,當調用該方法時,如果線程正在被某些方法阻塞,它將收到一個中斷異常的請求,提早終結這個線程被阻塞的狀態。(wait(),join(),sleep(long),等方法阻塞。其靠異常處理機制。
    • 口說無憑,舉個栗子:
package Day3; public class ThreadTest7 { public static void main(String[] args) { Thread t =new Thread(new Runnable() {//匿名內部類實現接口 @Override public void run() { try { Thread.sleep(1000*60*60); } catch (InterruptedException e) {//interrupted()方法引發這個異常 e.printStackTrace(); } System.out.println("Hello Wrold!"); } }); t.setName("se"); t.start(); t.interrupt();//中斷阻塞 } } 
  • 如何合理終止線程的執行?
    • 直接舉個栗子:(注意看注釋)
package Day3; import java.util.concurrent.ThreadLocalRandom; public class ThreadTest9 { public static void main(String[] args) { MyRunnable2 r =new MyRunnable2();//需要對象,合理終止需要用到對象的屬性 Thread t = new Thread(r); t.setName("Jia"); t.start(); try { Thread.sleep(5*1000);//5秒后終止線程 } catch (InterruptedException e) { e.printStackTrace(); } //暴力終止t線程` //t.stop();//缺點:線程沒有保存的數據會丟失,即容易丟失數據(已過時 //合理終止線程的執行,標記法 r.run=false; } } class MyRunnable2 implements Runnable{ //run標記 boolean run =true; @Override public void run() { for(int i =0;i<10;i++){ if(run) { System.out.println(Thread.currentThread().getName() + "-->" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } else { //終止線程 return; } } } } 
  • 運行結果:
    在這里插入圖片描述
    結果為啥是這樣:請記住現在的編程是多線程並發執行,在沒有終止的五秒前,運行了五秒。(可能一些小伙伴這里會轉不過彎來,所以啰嗦一下)
  • 注意:強行終止的stop()方法容易丟失數據。
  • run方法不能拋出任何異常,因為子類不能比父類拋出更多異常。

6.線程安全問題(重點)

  • 多線程並發環境下,數據安全問題。
  • 為什么說線程安全是重點?
    • 開發中,我們項目運行在服務器中,而服務起已經將線程的定義,線程對象的創建,線程的啟動等,都已經實現了,我們無需關心。
    • 我們要關心的是,我們編寫的程序放到一個多線程環境下運行,這些數據是否安全。
  • 什么情況下在多線程環境下,我們的數據會存在安全問題。
    • 舉個栗子:
      多線程並發對同一個賬戶進行取款:
      在這里插入圖片描述
package Day1; public class AccountTest1 { public static void main(String[] args) { Account account = new Account("Jia",10000); //創建兩個線程對象 Thread t1 = new MyAccountThread(account); Thread t2 = new MyAccountThread(account); t1.setName("t1"); t2.setName("t2"); t1.start(); t2.start(); } } class Account { private String name; private double blance; public Account() { } public Account(String name, double blance) { this.name = name; this.blance = blance; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getBlance() { return blance; } public void setBlance(double blance) { this.blance = blance; } //取款 public void GetMoney(double money){ //兩個線程並發,同時操作堆中的對象 //取款之前 double before =this.getBlance(); //取款之后 double after = before -money; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //如果t1線程執行到這之前余額還沒更新,t2也進來執行獲取余額了,這時就會出問題 this.setBlance(after); } } class MyAccountThread extends Thread{ //兩個線程共享一個賬戶對象 private Account account1 ; //通過構造方法傳遞過來賬戶對象 public MyAccountThread(Account account1) { this.account1 = account1; } @Override public void run() { //取款5000 double money =5000; account1.GetMoney(money); System.out.println("取款成功!"+"賬戶"+account1.getName()+"余款:"+account1.getBlance()); } } 

運行結果:取了兩次錢結果:
在這里插入圖片描述
說明:不同的類盡量寫在不同的源文件中。這里是為了便於觀察。

  • 總結以下情況會存在安全問題。
    • 條件一:多線程並發。
    • 條件二:有共享數據。
    • 條件三:共享數據有修改的行為。
    • 滿足以上條件會存在線程安全問題。
  • 怎么解決線程安全問題?
    • 用排隊執行解決線程安全問題。這種機制被稱為:“線程同步機制”。(線程排隊,即線程不能並發了)。

7.多線程的同步問題

  • 同步編程模型:
    • 線程之間的執行,需要等待另一個線程執行完畢,才能執行,效率較低。線程排隊執行。
  • 異步編程模型:
    • 線程之間,各自執行各自的,互不影響,其實就是多線程並發。
  • 如何實現多線程之間的“線程同步機制”(線程排隊)?
    • 方法為:給要操作的共享資源(重要)加鎖。保證一個線程對象在對它操作的時候不被其它線程干擾。
    • Java提供了一個synchronized關鍵字:如果線程t1遇到了synchronzied關鍵字,這時候自動找后面“共享資源”的對象鎖,找到后,並占有這把鎖,然后執行同步代碼塊的程序。直到執行結束,才會釋放該鎖。線程t2在t1沒有釋放鎖之前,只能排隊。synchronized后面小括號”數據很關鍵“,必須是多線程共享的數據,才能排隊。
    • Synchronized關鍵字可以隊線程對象要操作的資源(如方法,對象等)進行加鎖。(同步鎖)
      在這里插入圖片描述
    • synchromized后面的小括號寫什么看你想要哪幾個線程同步,例如t1,t2…t5,如果你只希望t1,t2,t3,排隊,你一定要在括號中寫一個t1,t2,t3共享的對象,t4,t5不共享這個對象,就可以並發執行。(這就好比t1,t2,t3,為男生,共享男衛生間。t4,t5為女生就不需要在男衛生間門口排隊,他們可以並發的去女衛生間執行(對於一些會去男衛生間上廁所的這里就不討論,哈哈)。)
  • java語言中,任何對象都有“一把鎖”,其實就是一個標記(只是稱為鎖)。
  • 口說無憑,舉個栗子:
package Day1; //使用同步機制解決線程安全問題 public class AccountTest1 { public static void main(String[] args) { Account account = new Account("Jia",10000); //創建兩個線程對象 Runnable tt1= new MyAccountThread(account);//多態 Runnable tt2 = new MyAccountThread(account); Thread t1 = new Thread(tt1); Thread t2 = new Thread(tt2); t1.setName("t1"); t2.setName("t2"); t1.start(); t2.start(); } } class Account { private String name; private double blance; public Account() { } public Account(String name, double blance) { this.name = name; this.blance = blance; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getBlance() { return blance; } public void setBlance(double blance) { this.blance = blance; } //取款 public void GetMoney(double money){ //以下代碼必須是線程排隊的,不能並發 //一個線程吧這里的代碼全部執行后,另一個才能進來執行 /*synchronized后面小括號”數據很關鍵“,必須是多線程共享的數據,才能排隊 小括號寫什么看你想要哪幾個線程同步,例如t1,t2..t5,如果你只希望t1,t2,t3,排隊 你一定要在括號中寫一個t1,t2,t3共享的對象。t4,t5不共享這個對象,就可以並發執行。*/ synchronized (this) {//線程同步機制,線程同步代碼塊,需要傳入共享資源;共享資源的不同,決定了什么不同的線程同步 double before = this.getBlance();//這里寫什么,取決於你想對於什么資源,對應的線程排隊。 double after = before - money; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.setBlance(after); } } } class MyAccountThread implements Runnable{ //兩個線程共享一個賬戶對象 private Account account1 ; //通過構造方法傳遞過來賬戶對象 public MyAccountThread(Account account1) { this.account1 = account1; } @Override public void run() { //取款5000 double money =5000; account1.GetMoney(money); System.out.println("取款成功!"+"賬戶"+account1.getName()+"余款:"+account1.getBlance()); } } 

運行結果:
在這里插入圖片描述

  • 共享資源的理解:不同的賬戶,不共享,所以對不同的賬戶取款,不需要排隊。只有同時對同一賬戶進行多個操作時,其共享一個賬戶信息,需要加鎖進行排隊。
  • Java的三大變量哪些存在線程安全問題?
    • 實例變量(堆內存):堆只有一個。
    • 靜態變量(方法區):方法區只有一個。
    • 堆和方法區都是多線程共享的,所以可能存在線程安全問題。
  • 局部變量(在棧中):局部變量永遠不會存在線程安全問題。因為局部變量不共享在棧中,一個線程一個棧。
  • 同步代碼塊越小,效果越好。
  • 常量不會有線程安全問題。
  • 實例方法中可以使用sychronized。但是出現在實例方法上,一定鎖的是this.沒得挑,只能是this.這種方式不靈活。
    • synchronized使用的優點:減少了代碼冗余。
    • 如果共享的就是this,並且需要同步的代碼塊就是方法體,建議使用這種方式(聲明同步方法,這種方式不會被繼承)。(即public Synchronized 返回值 方法名(){
      //方法的實現
      })
  • 回顧:線程安全:Vector,Hashtale
    • 非線程安全:ArrayList,HashMap,BashMap。
  • 總結:Synchronized有三種方法:
    • 第一種:同步代碼塊。靈活
      Synchronized(線程共享對象){
      同步代碼塊
      }
    • 第二種:在實例方法上使用Synchronzed。
      表示共享對象一定是this.並且同步代碼塊是整個方法體。
    • 第三種:在靜態方法上使用Synchronized
      表示找類鎖。類鎖永遠只有一把(保證了靜態變量的安全)不管對象有多少,所以對於類鎖而言,如果加類鎖的話,只要屬於該類的對象都是共享資源。
  • 死鎖的概念:
  • 死鎖會造成程序不出異常,也不出錯誤,一直僵持在那里。
  • 口說無憑,舉個栗子:
package Day1; import java.util.concurrent.SynchronousQueue; public class ExamTest1 { //死鎖要會寫 public static void main(String[] args) { //兩個線程共享o1,o2 Object o1 = new Object(); Object o2 = new Object(); Thread t1 = new MyThread1(o1,o2); Thread t2 = new MyThread2(o1,o2); t1.start(); t2.start(); } } class MyThread1 extends Thread { Object o1; Object o2; public MyThread1(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; } @Override public void run() { synchronized (o1) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2) { } } } } class MyThread2 extends Thread { Object o1; Object o2; public MyThread2(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; } @Override public void run() { synchronized (o2) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1) { } } } } 

在這里插入圖片描述

  • 總結:
    • 盡量不要使用兩個嵌套synchronized,容易造成死鎖。
    • 對於線程調度狀態的方法:destroy(),stop(),suspend()等調用后不會釋放線程本身的對象鎖,容易造成死鎖,不建議使用。而resume方法是用來喚醒suspend方法暫定的線程,因而也不建議使用。
  • 開發中如何解決線程安全問題?
    • synchronized會使執行效率降低,讓用戶體驗差,在不得已的時候在選取。
    • 第一種方案:盡量使用局部變量代替”實例變量和靜態變量。
    • 第二種方案:如果必須使實例變量,那么可以考慮創建對各對象,即不共享。一個線程一個對象。這樣就沒有安全問題了。(多搞幾個衛生間,如果每個人都有一個衛生間或那啥是吧,你說還需要排隊嗎?)
    • 第三種方案:前兩者都不能用,采用synchronized,線程同步機制。
  • 什么是守護線程?(了解)
    • java語言中線程分為用戶線程(主線程等)和守護線程。
    • 守護線程其實就是后台線程,其中比較有代表性的是:垃圾回收線程(守護線程)
  • 守護線程有什么作用呢?
    • 比如我們的定時數據自動備份,這個需要用到計時器,當到某個特定點后就自動備份一次。所以用戶線程退出后,守護線程自動退出,沒有必要進行數據備份了。
    • 話不多說,舉個栗子說明一下吧:
package Day1; public class TheadTest1 { public static void main(String[] args) { Thread JiSi=new MyDaemon(); JiSi.setName("備份數據"); //設為守護線程 JiSi.setDaemon(true); JiSi.start(); //主線程 for(int i =0 ;i<10;i++){ System.out.println(Thread.currentThread().getName()+"-->"+i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyDaemon extends Thread{ @Override public void run() { int i = 0; //即使是死循環,但由於改線程是守護者,當用戶結束,守護線程自動終止。 while (true) { System.out.println(Thread.currentThread().getName() + "-->" + (++i)); try { Thread.sleep(1000);//模擬一秒記錄一次 } catch (InterruptedException e) { e.printStackTrace(); } } } } 

運行結果:
在這里插入圖片描述
從結果可以看出當用戶線程結束的時候,守護線程也終止。

  • 如何設置一個守護線程呢?
    • 使用setDaemon(True);方法即可。
  • 什么是定時器
    • 定時器的作用?
      • 間隔特定的時間,執行特定的程序。比如隔一段時間備份數據,或進行總賬操作。在實際開發中很常見。
    • 那如何實現呢?
      • 可以采用sleep方法,最原始的定時器。
      • java.util.Timer計時器,可以來拿用。
      • 實際開發中,使用Spring框架中提供的SpirngTask框架,簡單配置就可以完成計時器的任務,其原理跟Timer相同。所以我們看一下Timer。
      • 多說無益,舉個栗子:
package Day2; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.AbstractMap; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import java.util.zip.DataFormatException; public class TimerTest1 { public static void main(String[] args) { //創建定時器對象 Timer timer =new Timer(); //Timer timer = new Timer(true);//守護線程的方式 //定時任務 //timer.schedule(定時任務,第一次執行時間,間隔多久一次)//定時任務屬於,TimerTask類 //創建對象 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { //將時間字符串轉換成可處理的時間 Date firstTime = sdf.parse("2020-04-11 10:40:00"); timer.schedule(new LogTimerTask(),firstTime,1000*5);//每隔5秒備份一次 } catch (ParseException e) { e.printStackTrace(); } } } //設為定時任務類,假設為記錄日志的定時任務 class LogTimerTask extends TimerTask{//TimerTask為抽象類,因為任務的類型為這個類 @Override public void run() { //創建一個日期格式對象 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String strTime = sdf.format(new Date());//將當前系統時間轉換為字符串 System.out.println(strTime+"成功完成了一次數據備份!"); } } 

部分運行結果:
在這里插入圖片描述

  • 實現線程的第三種方式:(JDK8新特性)
    • 實現Callable接口。
    • 在java.util.concurrent.FutureTask包下。
    • 這種方式實現的線程可以獲取現場的返回值。
    • 執行一個任務,需要返回一個值時,可以用這種方式。
    • 缺點:獲取另一個線程返回結果時,當前線程受到阻塞,效率較低。
    • 多說無益,舉個栗子:
package Day2; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; //實現Callable接口,實現線程的第三種方式 public class CallableTest1 { public static void main(String[] args) { //第一步:創建一個“未來任務類”對象 FutureTask task =new FutureTask(new Callable() {//匿名內部類實現接口 @Override public Object call() throws Exception {//相當於run方法,只不過有返回值 //線程執行一個任務,可能有一個執行結果 System.out.println("Call method begin!"); Thread.sleep(1000*5); System.out.println("Call method over!"); int a,b; a=100; b=200; return a+b;//自動裝箱(Interger) } }); //創建線程對象 Thread t = new Thread(task); //啟動線程 t.start(); //這里時main方法,在主線程 //用get()方法,獲取t線程的返回結果,但是當前線程受到阻塞 Object obj = null; try { obj = task.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } //主線程要執行以下程序必須等待get()方法執行結束。 System.out.println("線程執行結果:"+obj); System.out.println("Hello World!"); } } 

8.生產者與消費者模型

  • 了解生產者與消費者模型之前我們先來看一下wait和notify方法。
  • wait和notify方法不是線程對象的方法,是java中任何一個對象都有的方法,因為這兩個方法是Object類自帶的。wait方法和notify方法不是通過線程對象調用。(重點)
  • Object.wati()方法的作用?
    • 當一個線程執行到wait()方法時,進入一個和該對象相關的等待池,同時失去了對象鎖的擁有權,該線程進入阻塞狀態,直到調用notify或notifyAll()方法將其喚醒。當前線程必須擁有該對象的鎖,如果不擁有,會拋出異常,所以wait()方法必須在synchronized block中調用。(重點)
    • 以下舉個栗子說明一下:(如下圖)
      在這里插入圖片描述
  • Object.notify()/notifyAll()方法的作用?
    • Object o =new Object();
    • o.notify()/notifyAll()表示喚醒正在o對象等待的第一個線程/所有線程。這個方法不會釋放對象鎖,這個方法同樣必須有其對象鎖,否則會拋出異常(IllegalMonitorStateException)。
  • 生產者和消費者模型。
    • 學過操作系統的朋友這里應該比較熟悉,沒學過的朋友不要擔心,我們來嘮嘮,學過的就當好好復習一下。
      在這里插入圖片描述
    • 什么是“生產者和消費者模型”?
      • 生產線程負責生產,消費線程負責消費。
      • 生產線程和消費線程要達到均衡。
      • 這是一個特殊的業務需求,需要使用wait方法和notify方法。
  • wait和notify方法不是線程對象的方法,是普通java對象的方法。(又啰嗦一遍,因為很多人對這有誤解)
  • wait和notify方法建立在線程同步的基礎上,因為多線程共享一個資源(倉庫),存在線程安全問題。
  • 舉個生產者和消費者比較經典的栗子(重點):
package Day2; import java.util.ArrayList; import java.util.List; //List模擬倉庫,容量為1,即生產一個消費一個 public class ProductTest1 { public static void main(String[] args) { //創建一個倉庫對象,共享的 List list = new ArrayList(); //創建兩個線程對象 //生產者線程 Thread t1 = new Thread(new Producer(list)); //消費者線程 Thread t2 = new Thread(new Consumer(list)); t1.setName("生產者線程:"); t2.setName("消費者線程:"); t1.start(); t2.start(); } } //生產線程 class Producer implements Runnable{ //倉庫 private List list; public Producer(List list) { this.list = list; } @Override public void run() { //一直生產 while(true) { synchronized (list) {//給倉庫資源加鎖 if (list.size() > 0) { //當前線程進入等待狀態,釋放鎖,不放鎖的話,消費者線程無法訪問資源(生產者線程) try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //程序能執行到這里說明倉庫為空,可以生產 Object obj = new Object(); list.add(obj); System.out.println(Thread.currentThread().getName()+"-->"+obj); //喚醒消費者消費 list.notify(); } } } } //消費線程 class Consumer implements Runnable{ //同一個倉庫 private List list; public Consumer(List list) { this.list = list; } @Override public void run() { //一直消費 while (true){ synchronized (list){//沒有得到鎖,以下代碼都不能執行 if(list.size()==0){ //倉庫空了,停止消費,消費線程進入阻塞,釋放list集合的鎖 try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //程序能夠執行到這,說明倉庫有數據,進行消費 Object obj= list.remove(0); System.out.println(Thread.currentThread().getName()+"-->"+obj); //喚醒生產者進行生產 list.notify(); } } } } 

部分執行結果:
在這里插入圖片描述

碼字不易,點個贊再走吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM