1.單線程 單線程:只有一個線程,即CPU只執行一個任務(一個線程) 1 class Hero{ 2 String name; 3 Hero(String name){ 4 this.name = name; 5 } 6 public void show(){ 7 System.out.printl ...
1.單線程
單線程:只有一個線程,即CPU只執行一個任務(一個線程)
1 class Hero{ 2 String name; 3 Hero(String name){ 4 this.name = name; 5 } 6 public void show(){ 7 System.out.println(name + "。。。。。"); 8 } 9 } 10 11 public class ThreadDemo { 12 public static void main(String[] args) { 13 Hero d1 = new Hero("亞瑟"); 14 Hero d2 = new Hero("妲己"); 15 Hero d3 = new Hero("貂蟬"); 16 d1.show(); 17 d2.show(); 18 d3.show(); 19 } 20 }
結果:,多線程順序可能會不一樣
2.多線程
多線程:就是多個線程同時運行,一個CPU執行多個任務(線程)
- 優點:能讓代碼同時執行,可以大大提高效率
- 能讓代碼同時執行,可以大大提高效率
1.主線程
java中,main方法是程式的入口,所以 main 方法被稱為:主方法
- 執行 main 方法中代碼的線程,被稱為:主線程
需要註意的地方有 2 點:
- main 方法中的代碼都是有 main 線程執行的
- 在 main 方法中調用其他方法,那麼其他方法中的代碼也是由main線程執行
2.創建線程
除了主線程外,java允許我們自己創建線程,通常有 2 種方式:
- 繼承Thread類
- 實現Runnable介面
1.Thread類
java中有一個專門描述線程的類:Thread,通過繼承這個類可以創建自己的線程,比如:
1 //1. 繼承Thread 2 class Hero extends Thread{ 3 String name; 4 Hero(String name){ 5 this.name = name; 6 } 7 //2. 覆寫run方法 8 @Override 9 public void run(){ 10 System.out.println(name + "。。。。。"); 11 } 12 } 13 14 public class ThreadDemo { 15 public static void main(String[] args) { 16 //3. 創建線程對象 17 Hero yase = new Hero("亞瑟"); 18 Hero daji = new Hero("妲己"); 19 Hero diaochao = new Hero("貂蟬"); 20 //4. 調用start方法:啟動線程,之後會自動執行 run 方法 21 yase.start(); 22 daji.start(); 23 diaochao.start(); 24 } 25 }
結果:
註意:想要啟動一個線程,必須調用的是 start 方法,只是調用 run 方法並不會開啟新的線程
啟動線程方法start()和run()區別
在Java中啟動線程有兩種方式:調用start()方法和調用run()方法。它們的區別如下:
- 調用start()方法:會啟動一個新線程,併在新線程中執行run()方法裡面的代碼。
- 調用run()方法:不會啟動新線程,而是在當前線程中同步執行run()方法裡面的代碼。
- 只有調用start()方法,才會表現出多線程的特性,不同線程的run()方法裡面的代碼交替執行。如果只是調用run()方法,那麼代碼還是同步執行的,必須等待一個線程的run()方法裡面的代碼全部執行完畢之後,另外一個線程才可以執行其run()方法裡面的代碼。
2.實現Runnable介面
實現 java 中的 Runnable 介面並覆寫 run 方法,也能創建線程,比如:
1 //1. 實現Runnable介面 2 class Hero implements Runnable{ 3 String name; 4 Hero(String name){ 5 this.name = name; 6 } 7 //2. 實現run方法 8 @Override 9 public void run() { 10 System.out.println(name + "。。。。。"); 11 } 12 } 13 public class ThreadDemo { 14 public static void main(String[] args) { 15 //3. 創建Thread對象時把Runnable對象作為參數扔進去 16 Thread t1 = new Thread(new Hero("亞瑟")); 17 Thread t2 = new Thread(new Hero("妲己")); 18 Thread t3 = new Thread(new Hero("貂蟬")); 19 //4.調用start方法,啟動一個線程,會自動調用run方法 20 t1.start(); 21 t2.start(); 22 t3.start(); 23 } 24 }
結果:
通常在學習時,我們都用 ‘匿名內部類’創建線程,比如:
1 public static void main(String[] args) { 2 //使用 匿名內部類 創建線程 3 Thread yase = new Thread(new Runnable() { 4 @Override 5 public void run() { 6 System.out.println("使用匿名內部類創建一個線程。。。。。。"); 7 } 8 },"yase"); 9 yase.start(); 10 }
實現 Runnable 介面和繼承 Thread 比較:
- java不支持多繼承,如果繼承Thread,那麼就不能繼承其他類了
- java支持多實現,如果實現Runnable,不但可以實現其他介面,也可以繼承其他類
工作中推薦使用 Runnable 的方式創建線程
3.線程名
為了區分不同的線程,預設情況下每個線程都有名稱
1.獲取線程名
通過調用 getName() 方法獲取
結果:
2.主線程名字
主線程也是有名稱的,但是我們無法使用 this.getName() 獲取主線程的名稱
- 想要獲取主線程名稱,得先獲取主線程對象
Thread類中有一個靜態方法:currentThread(),用來獲取當前線程對象,比如:
結果:
推薦使用:Thread.currentThread().getName() 獲取線程名稱
3.設置線程名
如果對預設的名稱不滿意,也可以在創建對象的時候自定義線程名稱
- 繼承 Thread 類
代碼:
1 class Hero extends Thread{ 2 String name; 3 //1. 增加一個 threadName 參數,作為線程名 4 Hero(String name,String threadName){ 5 //2. 調用super,把自定義名稱扔給父類 6 super(threadName); 7 this.name = name; 8 } 9 @Override 10 public void run(){ 11 //3. 通過Thread.currentThread().getName()獲取線程名稱 12 System.out.println(name + "。。。。。"+Thread.currentThread().getName()); 13 } 14 } 15 16 public class ThreadDemo { 17 public static void main(String[] args) { 18 //4. 創建Thread實例對象,自定義線程名 19 Hero d1 = new Hero("亞瑟", "心靈戰士"); 20 Hero d2 = new Hero("妲己","女僕咖啡"); 21 Hero d3 = new Hero("貂蟬", "異域舞娘"); 22 d1.start(); 23 d2.start(); 24 d3.start(); 25 } 26 }
結果:
2.實現Runnable介面
1 class Hero implements Runnable{ 2 String name; 3 Hero(String name){ 4 this.name = name; 5 } 6 @Override 7 public void run() { 8 System.out.println(name + "。。。。。"+Thread.currentThread().getName()); 9 } 10 } 11 public class ThreadDemo { 12 public static void main(String[] args) { 13 //創建Thread對象時候,直接把線程名稱扔進去 14 Thread t1 = new Thread(new Hero("亞瑟"), "心靈戰士"); 15 Thread t2 = new Thread(new Hero("妲己"), "女僕咖啡"); 16 Thread t3 = new Thread(new Hero("貂蟬"), "異域舞娘"); 17 t1.start(); 18 t2.start(); 19 t3.start(); 20 } 21 }
結果:
4.線程名好處
線程名在實際工作中很重要,有以下好處
- 調試方便:當程式運行出現問題時,如果每個線程都有自己的名稱,可以快速定位問題
- 可讀性提高:如果代碼中存在多個線程,如果有名稱可以使得代碼更易於理解和維護。
- 日誌記錄方便:當出現問題時,如果線程有名稱,根據日誌可以更方便地識別每個線程的相關信息
例:
1 class Hero implements Runnable{ 2 String name; 3 Hero(String name){ 4 this.name = name; 5 } 6 @Override 7 public void run() { 8 //如果 name 為null,會報異常 9 if(this.name.length()>0){ 10 System.out.println("aaaaaaaaaaaa"); 11 } 12 System.out.println(name + "。。。。。"); 13 } 14 } 15 public class Demo { 16 public static void main(String[] args) { 17 Thread t1 = new Thread(new Hero(null)); 18 Thread t2 = new Thread(new Hero("妲己")); 19 Thread t3 = new Thread(new Hero("貂蟬")); 20 //4.調用start方法,啟動一個線程,會自動調用run方法 21 t1.start(); 22 t2.start(); 23 t3.start(); 24 25 } 26 }
上面代碼中沒有設置線程名,結果:
上圖中雖然有預設的線程名,但是根據 Thread-0 很難定位到 Thread t1 = new Thread(new Hero(null)); 這句代碼在什麼地方
修改代碼,設置線程名:
1 Thread t1 = new Thread(new Hero("亞瑟"), "心靈戰士"); 2 Thread t2 = new Thread(new Hero("妲己"), "女僕咖啡"); 3 Thread t3 = new Thread(new Hero("貂蟬"), "異域舞娘");
結果:
4.多線程提高工作效率
示例:醫生給病人看病,一個人需要10分鐘,兩個人就需要20分鐘,如果兩個醫生,那麼一共需要10分鐘
示例:
1 class SickPerson{ 2 String name; 3 4 public SickPerson(String name){ 5 this.name = name; 6 } 7 } 8 9 public class ThreadTest { 10 public static void main(String[] args) { 11 //1. 利用數組模擬兩個病人 12 SickPerson[] arr = new SickPerson[]{new SickPerson("yase"), new SickPerson("daji")}; 13 14 Thread bianque = new Thread(new Runnable() { 15 @Override 16 public void run() { 17 System.out.println("扁鵲開始看病。。。。"); 18 //2. 使用 for 迴圈,一個個的處理病人 19 for(int i = 0;i<arr.length;i++){ 20 System.out.println("處理" +arr[i].name+ ",需要5秒鐘。。。。。。"); 21 try { 22 Thread.sleep(5000);//讓當前線程睡一會兒,然後接著執行 23 } catch (InterruptedException e) { 24 } 25 } 26 System.out.println("結束。。。。。。"); 27 } 28 },"bianque"); 29 bianque.start(); 30 } 31 }
結果:
開啟兩個線程
1 //用數組模擬兩個病人 2 SickPerson yase = new SickPerson("yase"); 3 SickPerson daji = new SickPerson("daji"); 4 5 // SickPerson[] arr = new SickPerson[]{new SickPerson("yase"),new SickPerson("daji")}; 6 //創建內部類Runnable重寫run方法,使用for迴圈,處理病人, 線程睡眠,接著執行 7 Thread bianque = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 System.out.println("扁鵲開始看病"); 11 System.out.println("給"+yase.name+"看病需要5秒"); 12 try { 13 Thread.sleep(5000); 14 } catch (InterruptedException e) { 15 } 16 } 17 },"bianque"); 18 bianque.start(); 19 //開啟第二個線程 20 Thread huatuo = new Thread(new Runnable() { 21 @Override 22 public void run() { 23 System.out.println("華佗開始看病"); 24 System.out.println("給"+daji.name+"看病要5秒"); 25 try { 26 Thread.sleep(5000); 27 } catch (InterruptedException e) { 28 throw new RuntimeException(e); 29 } 30 } 31 },"huatuo"); 32 huatuo.start();
結果:
5.數據安全問題
1.前提
之前我們瞭解到,CPU是不斷的切換線程執行的,當CPU從A線程切換到B線程時,A線程就會暫停執行,比如:
1 public void run() { 2 //當線程執行到第一句代碼時,CPU很可能就切換其他線程執行,那麼當前線程就會暫停 3 System.out.println(name + "。。。。。"); 4 System.out.println(name + "。。。。。"); 5 System.out.println(name + "。。。。。"); 6 }
2.多線程操作數據
當多個線程同時操作同一個數據時候,可能產生數據安全問題
示例:
1 class Hero implements Runnable{ 2 //1. 定義一個靜態變數,多個線程同時操作它 3 public static int num = 10; 4 @Override 5 public void run() { 6 while (true){// while中用true,這是死迴圈,謹慎使用,這裡是為了演示效果 7 //2. run方法中,對num--,當num<=0時,跳出迴圈 8 if(num > 0){ 9 //sleep(5),讓當前線程休眠5毫秒,此時CPU會執行其他線程 10 try { Thread.sleep(5); } catch (InterruptedException e) {} 11 num--; 12 System.out.println(Thread.currentThread().getName() + "***********" + num); 13 }else{ 14 break; 15 } 16 } 17 } 18 } 19 public class ThreadDemo { 20 public static void main(String[] args) { 21 Hero hero = new Hero(); 22 Thread yase = new Thread(hero, "yase"); 23 Thread daji = new Thread(hero, "daji"); 24 //3. 開啟兩個線程操作num 25 yase.start(); 26 daji.start(); 27 } 28 }
結果:
代碼中,當 num>0 時,才會執行輸出語句,但是卻輸出了負數
分析一下執行過程:
- 2個線程剛開始正常執行,都會執行num--。。。。。
- 當num=1時,假如 'yase' 先進入 if ,休眠5毫秒,這時 CPU 切換線程‘daji’進入 if
- ‘yase’休眠結束,執行num--,接著‘daji’睡醒後也執行num--,最終num=-1
- 註意:即使沒有 sleep 語句,也可能輸出負數,只不過概率太低
總結:一個線程在操作數據時,其他線程也參與運算,最終可能造成數據錯誤
解決:保證操作數據的代碼在某一時間段內,只被一個線程執行,執行期間,其他線程不能參與,即使用synchronized關鍵字
6.線程同步
線程同步:就是讓線程一個接一個的排隊執行
- 同步:即一步一步,也是一個接一個的意思
java中提供 synchronized 關鍵字用來實現同步,可以解決多線程時的數據安全問題
不過,使用 synchronized 的方式有很多種,我們一個一個解釋
1.synchronized代碼塊
格式:synchronized(鎖對象){
同步代碼塊
}
鎖對象:可以理解為鑰匙、通行證,只有線程拿到通行證後,才能執行 { } 中的代碼
-
演示
使用 synchronized 修改 run 方法:
1 public static Object obj = new Object(); 2 @Override 3 public void run() { 4 while (true){ 5 //使用同步代碼塊,obj被稱為鎖對象 6 synchronized (obj){ 7 if(num > 0){ 8 //sleep(5),讓當前線程休眠5毫秒,此時CPU會執行其他線程 9 try { Thread.sleep(5); } catch (InterruptedException e) {} 10 num--; 11 System.out.println(Thread.currentThread().getName() + "***********" + num); 12 }else{ 13 break; 14 } 15 } 16 } 17 }
結果:
原因:
- 當線程執行到 synchronized (obj) 時,會獲取嘗試 obj 對象
- synchronized 保證多線程下,只有一個線程能獲取到obj對象,其他線程就會阻塞(暫停)
- 多個線程同時執行到 synchronized (obj) 這句代碼時,只有一個線程能夠拿到obj,其他線程暫停
- 當持有 obj 的線程從 synchronized 退出後,會釋放 obj 對象,然後其他線程再次爭奪 obj
這樣就保證了 synchronized 中的代碼在某一時刻只能被一個線程執行
- 好處和弊端
好處:解決多線程數據安全問題
弊端:同步代碼塊同時只能被一個線程執行,降低效率
synchronized註意事項
- 必須是多個線程
- 線程數>1,就是多線程
- 多線程爭搶的鎖對象只能有一個,下圖中的做法要堅決抵制(會被開哦)
2.同步方法
把 synchronized 放到方法上,那麼這個方法就是同步方法
- 演示
1 class Hero implements Runnable{ 2 public static int num = 10; 3 public static Object obj = new Object(); 4 @Override 5 public void run() { 6 //run方法中調用同步方法 7 this.show(); 8 } 9 //同步方法 10 public synchronized void show(){ 11 while (true){ 12 if(num > 0){ 13 try { Thread.sleep(5); } catch (InterruptedException e) {} 14 num--; 15 System.out.println(Thread.currentThread().getName() + "***********" + num); 16 }else{ 17 break; 18 } 19 } 20 } 21 22 } 23 public class ThreadDemo { 24 public static void main(String[] args) { 25 Hero hero = new Hero(); 26 Thread yase = new Thread(hero, "yase"); 27 Thread daji = new Thread(hero, "daji");