- 多線程概述
- 進程:正在進行中的程序。
- 線程:進程中的獨立控制單元。線程控制着進程的執行。
- 一個進程中至少有一個線程。
- jvm啓動時有一個java.exe進程,該進程至少有一個主線程負責程序的執行,這個線程運行的代碼存在於main函數中,該線程稱之爲主線程。
- jvm啓動時,除了主線程,還有負責垃圾回收的線程。
- 多線程程序:有不止一個線程(執行路徑)的程序就是多線程程序,如迅雷。
- 多線程存在的意義:
- 使程序中不同部分的代碼產生“同時運行”的效果。
- 線程的創建方式
- 繼承Thread類
- 創建線程的步驟:
- 定義類繼承Thread。
- 覆寫Thread類中的run方法。
- 目的:將自定義代碼存儲在run()方法中,讓線程運行。
- 調用線程的start方法
- 該方法有兩個作用:啓動線程,調用run方法。
- 如果直接調用run方法,相當於僅僅是對象調用普通方法。線程雖然創建了,但是並沒有運行。
- Thread類
- Thread類用於描述線程。
- Thread類中的run()方法用於存儲線程要運行的代碼。
- start()方法用於開啓線程,並執行該線程的run()方法。
- 練習
- 需求:創建兩個線程,和主線程交替運行。
- 代碼:
打印結果:package cn.itcast.heima; public class Test { public static void main(String[] args) { SubThread st1 = new SubThread(); SubThread st2 = new SubThread(); st1.start(); st2.start(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "run..." + i); } } } class SubThread extends Thread{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "run..." + i); } } }
- 實現Runnable接口
- 創建線程的步驟:
- 定義類實現Runnable接口。
- 覆蓋Runnable接口中的run()方法。
- 創建類Thread的線程對象。
- 將Runnable子類對象作爲實際參數傳遞給Thread類的構造函數。
- 通過Thread對象的start()方法開啓線程並調用Runnable接口子類的run()方法。
- 定義類實現Runnable接口。
- 優勢:
- 避免了單繼承的侷限性。
- 可以創建多個線程共享Runnable子類對象的數據,避免了共享數據定義爲靜態。
- 例子:
- 需求:簡單的賣票程序。實現多個窗口同時賣票。
- 如果按照繼承Thread類的方式實現多線程,代碼如下:
package cn.itcast.heima; public class Test { public static void main(String[] args) { //開放四個窗口售票 Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket(); Ticket t4 = new Ticket(); t1.start(); t2.start(); t3.start(); t4.start(); } } class Ticket extends Thread{ private static int ticket = 20;//總共20張票 @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--); } } }
打印結果:
這種方式需要將票數定義爲靜態成員。
缺陷:ticket成員變量生命週期過長,且Ticket類不能繼承其它類。 - 可以將其改爲實現Runnable接口的方式實現多線程,代碼如下:
打印結果:package cn.itcast.heima; public class Test { public static void main(String[] args) { Ticket t = new Ticket(); //開放四個窗口售票 Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); } } class Ticket implements Runnable{ private int ticket = 20;//總共20張票 @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--); } } }
這種方式避免了將ticket成員變量定義爲static,並且避免了單繼承的侷限性。 - 實際開發中,建議使用實現Runnable接口的方式實現多線程。
- 兩種方式區別:
- 繼承Thread類:線程代碼存放在Thread子類的run()方法中。
- 實現Runnable接口:線程代碼存放在Runnable接口子類的run()方法中。
- 多線程的特性:隨機性
- 對於多線程程序,每一次的運行結果都不相同。
- 因爲多個線程都需要獲取CPU的執行權,CPU執行到哪個線程,哪個線程就運行。
- 在某一時刻,只能有一個線程在運行,CPU在做着快速的切換,以達到看上去是同時運行的效果。
- 線程的五種狀態
- 線程的五種狀態:被創建,運行,凍結,臨時(阻塞),消亡。關係如下圖:
- 線程的五種狀態:被創建,運行,凍結,臨時(阻塞),消亡。關係如下圖:
- 獲取線程對象以及名稱
- 線程有默認的名稱:Thread-編號(從0開始)。
- currentThread():獲取當前線程對象(靜態)。
- getName():獲取線程名稱。
- 設置線程名稱:setName()或者構造函數。
- 線程有默認的名稱:Thread-編號(從0開始)。
- 多線程的安全問題:同步
- 問題原因:
- 多個線程在操作同一個共享數據時,一個線程對多條語句只執行了一部分,另一個線程就參與進來,導致共享數據出現問題。
- 多個線程在操作同一個共享數據時,一個線程對多條語句只執行了一部分,另一個線程就參與進來,導致共享數據出現問題。
- 解決辦法:
- 一個線程在執行過程中,其它線程不可以參與執行。
- 一個線程在執行過程中,其它線程不可以參與執行。
- Java對於多線程的安全問題提供了專業的解決方案:同步代碼塊。
- 同步代碼塊格式:
- 判斷需要被同步的代碼:
- 操作共享數據的代碼就是需要被同步的代碼。
- 操作共享數據的代碼就是需要被同步的代碼。
- 同步代碼塊的原理:
- 對象如同鎖。持有鎖的線程纔可以進入同步代碼塊執行語句。不持有鎖的線程即使獲得了CPU的執行權,也不能進入同步代碼塊執行語句。
- 火車上的衛生間的例子。
- 對象如同鎖。持有鎖的線程纔可以進入同步代碼塊執行語句。不持有鎖的線程即使獲得了CPU的執行權,也不能進入同步代碼塊執行語句。
- 同步的前提:
- 同步需要兩個或者兩個以上的線程。
- 多個線程使用的是同一個鎖。
- 同步需要兩個或者兩個以上的線程。
- 同步的好處:
- 解決了多線程的安全問題。
- 同步的弊端:
- 當線程相當多時,因爲每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形 中會降低程序的運行效率。
- 當線程相當多時,因爲每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形 中會降低程序的運行效率。
- 同步函數
- 格式:
在函數上加上synchronized修飾符即可。 - 同步函數使用的鎖是this。要保證同步代碼塊和同步函數用的是同一個鎖,同步代碼塊的參數必須是this。
- 格式:
- 靜態同步函數
- 靜態同步函數的鎖是該方法所在類的字節碼文件對象:類名.class。
- 靜態同步函數的鎖是該方法所在類的字節碼文件對象:類名.class。
- 死鎖
- 死鎖的出現:同步中嵌套同步。
- 實際開發中,要避免出現死鎖問題。
- 問題原因:
- 線程間通信
- 概念
- 線程間通信就是多個線程在操作同一個資源,但是操作的動作不同。
- 等待喚醒機制
- 多線程操作共享數據時要考慮使用等待喚醒機制。
- 等待線程存放在線程池中,notify()通常喚醒線程池中的最先等待的線程,notifyAll()喚醒線程池中的所有線程。
- wait(),notify(),notifyAll()都要用在同步中,因爲要對持有監視器(鎖)的線程操作。
- wait(),notify(),notifyAll()都是Object類中的方法,因爲這些方法在操作線程時,都必須要標識他們所操作線程持有的鎖。
一個被等待的線程,只能被持有同一個鎖的線程喚醒,也就是說,等待和喚醒必須是同一個鎖。
爲鎖可以是任意對象,所以可以被任意對象調用的方法定義在Object類中。 - wait和sleep比較:
- wait():釋放cpu執行權,釋放鎖。
- sleep():釋放cpu執行權,不釋放鎖。到達休眠時間後線程將繼續執行,直到完成。若在休眠期另一線程中斷該線程,則該線程退出。
- wait():釋放cpu執行權,釋放鎖。
- 多線程操作共享數據時要考慮使用等待喚醒機制。
- 生產者消費者例子
- 需求:生產者和消費者分別創建兩個線程,保證生產者進程和消費者進程能夠交替執行。
- 代碼:
打印結果:package cn.itcast.heima; public class Test { public static void main(String[] args) { Resource r = new Resource();//商品 Producer pro = new Producer(r);//生產者 Consumer con = new Consumer(r);//消費者 Thread t1 = new Thread(pro);//生產線程 Thread t2 = new Thread(pro);//生產線程 Thread t3 = new Thread(con);//消費線程 Thread t4 = new Thread(con);//消費線程 t1.start(); t2.start(); t3.start(); t4.start(); } } //商品類 class Resource{ private String name;//商品名稱 private int count = 1;//商品編號 private boolean flag = false;//判斷商品是否還在 //生產商品 public synchronized void set(String name){ //如果商品還在,生產線程等待 while (flag) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.name = name + "---" + count++; System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name); flag = true;//商品生產出來了 notifyAll();//喚醒所有等待線程 } //消費商品 public synchronized void out(){ //如果商品沒了,消費線程等待 while (!flag) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "...消費者......" + this.name); flag = false;//商品被拿走了 notifyAll();//喚醒所有等待線程 } } //生產者類 class Producer implements Runnable{ private Resource res; public Producer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.set("商品"); } } } //消費者類 class Consumer implements Runnable{ private Resource res; public Consumer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.out(); } } }
- 需要定義while判斷標記flag:
- 原因:讓被喚醒的線程再一次判斷標記,否則會導致重複生產或者重複消費。
- 並且爲了避免線程全部凍結,需要在解鎖之前喚醒所有線程:
- 原因:需要喚醒對方線程。因爲只用notify()容易出現只喚醒本方線程的情況,導致程序中所有線程都掛起。
- 原因:需要喚醒對方線程。因爲只用notify()容易出現只喚醒本方線程的情況,導致程序中所有線程都掛起。
- JDK5.0升級版
- 定義了Lock接口,代替了同步函數和同步代碼塊。
- 定義了Condition接口,代替了Object中的wait(),notify(),notifyAll()方法。Condition對象可以通過Lock接口子類的newCondition()方法獲取。
- 一個Lock可以對應多個Condition對象,該方式可以對指定的某個Condition對象對應的線程進行等待和喚醒處理,而不需要喚醒所有線程。
- 生產者消費者例子的JDK5.0升級版代碼:
打印結果:package cn.itcast.heima; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Test { public static void main(String[] args) { Resource r = new Resource();//商品 Producer pro = new Producer(r);//生產商品 Consumer con = new Consumer(r);//消費商品 Thread t1 = new Thread(pro);//生產線程 Thread t2 = new Thread(pro);//生產線程 Thread t3 = new Thread(con);//消費線程 Thread t4 = new Thread(con);//消費線程 t1.start(); t2.start(); t3.start(); t4.start(); } } //商品類 class Resource{ private String name;//商品名稱 private int count = 1;//商品編號 private boolean flag = false;//判斷商品是否還在 private Lock lock = new ReentrantLock();//定義鎖對象 private Condition condition_pro = lock.newCondition();//定義生產線程的Condition對象 private Condition condition_con = lock.newCondition();//定義消費線程的Condition對象 //生產商品 public void set(String name){ try{ lock.lock();//上鎖 //如果商品還在,生產線程等待 while (flag) { try { condition_pro.await();//生產線程等待 } catch (InterruptedException e) { e.printStackTrace(); } } this.name = name + "---" + count++; System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name); flag = true;//商品生產出來了 condition_con.signal();//喚醒消費線程 }finally{ lock.unlock();//解鎖 } } //消費商品 public void out(){ try { lock.lock();//上鎖 //如果商品沒了,消費線程等待 while (!flag) { try { condition_con.await();//消費線程等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "...消費者......" + this.name); flag = false;//商品被拿走了 condition_pro.signal();//喚醒生產線程 } finally { lock.unlock();//解鎖 } } } //生產者類 class Producer implements Runnable{ private Resource res; public Producer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.set("商品"); } } } //消費者類 class Consumer implements Runnable{ private Resource res; public Consumer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.out(); } } }
- 定義了Lock接口,代替了同步函數和同步代碼塊。
- 線程生命控制
- 停止線程
- stop()方法已經過時。
- 如何停止線程:
- 只有一種方法,run()方法結束。
- 開啓多線程運行,運行代碼通常都是循環結構。只要控制住循環,就可以結束run()方法,從而結束線程。
- 只有一種方法,run()方法結束。
- Thread類提供的interrupt()方法,可以中斷線程的凍結狀態。
當沒有指定的方式讓凍結的線程恢復到運行狀態時,就需要對凍結狀態進行清除。強制線程恢復到運行狀態,並在異常處理中通過改變標記讓線程結束。
- stop()方法已經過時。
- 守護線程
- 守護線程相當於後臺線程,當前臺線程全部結束後,後臺線程自動結束,JVM退出。
- 如果一個線程的運行依賴於另一個線程的運行,當第一個線程結束時,希望第二個線程也隨之結束,則第二個線程可以標記爲守護線程。
- 設置守護線程的方法:
- 在線程開啓前調用setDaemon(true)方法。
- 在線程開啓前調用setDaemon(true)方法。
- 守護線程相當於後臺線程,當前臺線程全部結束後,後臺線程自動結束,JVM退出。
- 聯合線程
- 當A線程執行到了B線程的join()方法時,A就會等待,直到B線程都執行完,A線程纔會繼續執行。
- join()方法可以用來臨時加入線程執行。
- 當A線程執行到了B線程的join()方法時,A就會等待,直到B線程都執行完,A線程纔會繼續執行。
- 優先級和yield方法
- 優先級
- 線程被哪個線程開啓,就屬於哪個線程組。
- 線程默認優先級爲5。優先級取值範圍1-10。
- Thread類的toString()方法返回線程名稱、優先級和線程組。
- 優先級代表搶佔cpu的頻率。
- setPriority方法:改變線程優先級。
- yield方法
- yield方法:暫停當前正在執行的線程對象,並執行其他線程。
- 可以實現當前線程執行完後讓出CPU執行權,達到不同線程交替執行的效果。
- yield方法:暫停當前正在執行的線程對象,並執行其他線程。
- 優先級