同步方法
同步方法:使用synchonized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法的外面等待着,排隊。
格式:
public synchonized void method(){ //可能會產生線程安全問題的代碼 }
備注:同步鎖是誰?
對於非static方法,同步鎖就是this
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)
同步方法代碼示例如下:
1 public synchronized void saleTicket() { 2 synchronized(this){ 3 try { 4 Thread.sleep(100); 5 } catch (InterruptedException e) { 6 e.printStackTrace(); 7 } 8 // 票存在,買出第ticket張票 9 if (ticket > 0) { 10 System.out.println(Thread.currentThread().getName() + "----->正在售賣第" + ticket-- + "張票"); 11 } 12 } 13 } 14 public static synchronized void saleTicket() { 15 16 synchronized(RunnableImpl.class) { 17 try { 18 Thread.sleep(100); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 // 票存在,買出第ticket張票 23 if (ticket > 0) { 24 System.out.println(Thread.currentThread().getName() + "----->正在售賣第" + ticket-- + "張票"); 25 } 26 } 27 28 }
Lock鎖
java.util.concurrent.locks.Lock機制提供了比synchronized代碼塊和synchronized同步方法更加廣泛的鎖操作。
同步代碼塊/同步方法具有的功能,Lock都有,除此之外更強大,更能體現出面向對象特征。
Lock鎖也稱為同步鎖,定義了加鎖與解鎖的動作,方法如下:
- public void lock():加同步鎖
- public void unlock():釋放同步鎖
備注:鎖是控制多個線程對共享資源進行訪問的工具。通常,所提供了對共享資源的獨占訪問。一次只能有一個線程獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。
實例代碼如下:
1 public class RunnableImpl implements Runnable { 2 // 定義一個多線程共享的資源 3 private int ticket = 100; 4 //1.在成員的位置創建一個ReentrantLock對象 5 Lock Lock = new ReentrantLock(); 6 // 設置線程的任務:賣票 此時窗口--->線程 7 @Override 8 public void run() { 9 // 先判斷票是否存在 10 while (ticket > 0) { 11 // 票存在,買出第ticket張票 12 //2.在可能會引發線程安全問題的代碼前調用Lock接口中的lock方法獲取鎖 13 Lock.lock(); 14 try { 15 if (ticket > 0) { 16 Thread.sleep(100); 17 System.out.println(Thread.currentThread().getName() + "----->正在售賣第" + ticket-- + "張票"); 18 } 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } finally { 22 //無論程序出現異常,此時都會釋放鎖 23 //在finally語句塊中一般用於資源的釋放,關閉IO流,釋放lock鎖,關閉數據庫連接等等 24 //3.在可能會引發線程安全問題的代碼后調用Lock接口中的unlock釋放鎖 25 Lock.unlock(); 26 } 27 } 28 } 29 }
線程狀態
線程狀態概述
當線程被創建並啟動之后,它既不是一啟動就進入到了執行狀態,也不是一直處於執行狀態。在線程的生命周期中有6種狀態。
在JavaAPI幫助文檔中java.lang.Thread.State這個枚舉給出了線程的6種狀態。
導致狀態發生條件 | |
---|---|
NEW(新建) | 線程剛被創建,但是還沒有啟動,還沒有調用start方法 |
RUNNABLE(可運行) | 線程可以在java虛擬機中運行的狀態,可以是正在運行自己的代碼,也可能沒有,這取決於操作系統處理器 |
BLOCKED(鎖阻塞) | 當一個線程試圖獲取一個對象鎖,而該對象鎖被其他線程所持有,則該線程進入到Blocked狀態;當該線程持有鎖時,該線程就進入到Runnable狀態 |
WAITING(無限等待) | 一個線程在等待另一個線程執行一個動作(新建)時,該線程就進入到Waiting狀態,進入這個Waiting狀態后是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒 |
TIMED_WAITING(計時等待) | 同Waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態,這一狀態將一直保持到超時期滿或者是收到了喚醒通知。帶有超時參數的常用方法有Thread.sleep(),Object.wait(). |
TERMINATED(被終止) |
六種狀態切換描述:
Timed Waiting(計時等待)
Time Waiting在JavaAPI中描述為:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處於這一狀態
其實當我們調用了sleep方法治好,當前正在執行的線程就進入到了計時等待狀態。
練習:實現一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字輸出一個字符串。
1 public class MyThread extends Thread { 2 @Override 3 public void run() { 4 for (int i = 1;i <= 100 ; i ++) { 5 if (i % 10 == 0) { 6 System.out.println("------------------>" + i); 7 } 8 System.out.println(i); 9 // 在每個數字之間暫停1秒 10 try{ 11 Thread.sleep(1000); 12 } catch (Exception e) { 13 e.printStackTrace(); 14 } 15 } 16 } 17 // 准備一個main函數 18 public static void main(String[] args) { 19 new MyThread().start(); 20 } 21 }
備注:
1.進入到Timed Waiting狀態的一種常見的操作是調用sleep方法,單獨的線程也可以調用,不一定非要有協作關系;
2.為了讓其他線程有機會執行到,一般建議將Thread.sleep()調用放到線程run方法內,這樣才能保證該線程執行規程中會睡眠;
3.sleep與所無關,線程睡眠到期會自動蘇醒,並返回到Runnable狀態。sleep()里面的參數指定的時間是線程不會運行的最短時間,因此,sleep()方法不能保證該線程睡眠到期后就會立刻開始執行。
Blocked鎖阻塞狀態
Blocked狀態在JavaAPI中描述為:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處於這一狀態。比如:線程A與線程B代碼中使用同一把鎖,如果線程A獲取到鎖對象,線程A就進入Runnable狀態,反之線程B就進入到Blocked鎖阻塞狀態。
Waiting無限等待狀態
Waiting狀態在JavaAPI中的描述為:一個正在無限等待另一個線程執行一個特別的(喚醒)動作的線程處於這一狀態。
一個調用了某個對象的Object.wait()方法的線程,會等待另一個線程調用此對象的Object.notify()或者Object.notifyAll()方法
其實waiting狀態它並不是一個線程的操作,它體現的是多個線程之間的通信,可以理解為多個線程之間的協作關系,多個線程會爭取鎖,同時相互之間又存在協作關系。
等待喚醒機制
線程間通信
概念:多個線程在處理同一個資源,但是處理的動作(線程的任務),卻又不相同。
比如說,線程A用來生產一個娃哈哈飲料,線程B用來消費娃哈哈飲料,娃哈哈飲料可以理解為同一資源,線程A與線程B處理的動作,一個是生產,一個是消費,那么線程A與線程B之間就存在線程通信問題。
為什么要處理線程之間的通信:
多個線程並發在執行時,在默認情況下CPU是隨機切換線程的,當我們需要多個線程共同來完成一件任務時,並且我們希望他們有規律的執行,那么多線程之間就需要一些協調通信,以此來幫助我們達到多線程共同操作一份數據。
如何保證線程間通信有效利用資源:
多個線程在處理同一個資源的時候,並且任務還不相同,需要線程通信來幫助我們解決線程之間對同一個變量的使用或者操作。就是多個線程在操作同一份數據時,避免對同一共享變量的爭奪,也就是我們需要通過一定的手段使各個線程有效的利用資源。
而這種手段就是----->等待喚醒機制。
等待喚醒機制
什么是等待喚醒機制呢?
這是多個線程間的一種協作機制。
就是一個線程進行了規定操作后,就進入到了等待狀態(wait()),等待其他線程執行完他們的指定代碼后,再將其喚醒(notify());
在有多個線程進行等待時,如果需要,可以使用notifyAll()來喚醒所有的等待線程。
wait/notify就是線程間的一種協作機制。
等待喚醒中的方法:
等待喚醒機制就是用來解決線程間通信問題的。可以使用到的方法有三個如下:
- wait():線程不在活動,不再參與調度,進入到wait set中,因此不會浪費CPU資源,也不再去競爭鎖,這時的線程狀態就是WAITING。它還要等着別的線程執行一個特別的動作,就是喚醒通知(notify)在這個對象上等待的線程從wait set鍾師傅出來,重新進入到調度隊列(ready queue)中。
- notify():選取所通知對象的wait set中的一個線程釋放。例如:餐廳有空位置后,等候就餐最久的顧客最先入座。
- notifyAll():釋放所通知對象的wait set中的全部線程。
備注:
哪怕只通知了一個等待線程,被通知的線程也不能立即回復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖了,所以它需要再次嘗試着去獲取鎖(很可能面臨着其他線程的競爭),成功后才能在當初調用wait方法之后的地方恢復執行。
總結如下:
如果能獲取到鎖,線程就從WAITING狀態轉變成RUNNABLE狀態
否則,從wait set中出來,又進入set中,線程就從WAITING狀態轉變成BLOCKED狀態。
調用wait和notify方法的注意細節:
-
wait方法與notify方法必須由同一個鎖對象調用。因為,對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法后的線程。
-
wait方法與notify方法是屬於Object類的方法的。因為,鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
-
生產者與消費者問題
舉一個例子:生產包子與消費包子來描述等待喚醒機制如何有效的利用資源:
代碼示例:
1 /* 2 * 資源類:包子類 3 * 設置包子的屬性 4 * 皮 5 * 餡 6 * 包子的狀態 有 true 沒有 false 7 */ 8 public class Baozi { 9 // 皮 10 String pi; 11 // 餡 12 String xian; 13 // 包子的狀態 有 true 沒有 false,設置初始值為false,沒有包子 14 boolean flag = false; 15 16 } 17 // 包子鋪 18 /* 19 * 生產者(包子鋪):是一個線程類,繼承Thread類 20 * 設置線程的任務:生產包子 21 * true:有包子 22 * 包子鋪調用wait方法進入等待狀態 23 * false:沒有包子 24 * 增加一些難度:交替生產兩種包子 25 * 有兩種狀態:(i % 2 == 0) 26 * 包子鋪生產包子 27 * 修改包子的狀態為true 28 * 喚醒吃貨線程,讓吃貨去吃包子 29 * 30 * 注意: 31 * 包子鋪線程和吃貨線程關系---->通信(互斥) 32 * 必須使用同步技術保證兩個線程只能有一個線程在執行 33 * 鎖對象必須保證唯一,可以使用包子對象作為鎖對象 34 * 包子鋪線程和吃貨線程的類需要把包子對象作為參數傳遞進來 35 * 1.需要在成員的位置上創建一個包子變量 36 * 2.使用帶參構造,為這個包子變量賦值 37 */ 38 public class Costs extends Thread{ 39 //1.需要在成員的位置上創建一個包子變量 40 private Baozi baozi; 41 42 //2.使用帶參構造,為這個包子變量賦值 43 public Costs(Baozi baozi) { 44 this.baozi = baozi; 45 } 46 47 // 重寫run方法 48 @Override 49 public void run() { 50 // 設置線程任務:生產包子 51 // 定義一個變量 52 int count = 0; 53 // 讓包子鋪一直生產包子 54 while(true) { 55 // 必須保證兩個線程只能有一個線程在執行 56 synchronized (baozi) { 57 // 進行包子狀態的判斷 58 if (baozi.flag) { 59 //包子鋪有包子,包子鋪需要調用wait方法進入等待狀態 60 try { 61 baozi.wait(); 62 } catch (InterruptedException e) { 63 e.printStackTrace(); 64 } 65 } 66 // 包子鋪沒有包子,被喚醒之后,包子鋪生產包子 67 // 增加一些難度:交替生產兩種類型的包子 68 if (count % 2 == 0) { 69 //生產 三鮮餡的包子,皮是薄皮 70 baozi.pi = "薄皮"; 71 baozi.xian= "三鮮餡"; 72 } else { 73 // 生產 豬肉大蔥餡 冰皮 74 baozi.pi = "冰皮"; 75 baozi.xian = "豬肉大蔥餡"; 76 } 77 count++; 78 System.out.println("包子鋪正在生產:" + baozi.pi + baozi.xian + "包子"); 79 // 生產包子需要有一個過程:等待3秒鍾 80 try { 81 Thread.sleep(3000); 82 } catch (InterruptedException e) { 83 e.printStackTrace(); 84 } 85 // 包子鋪生產好了包子 86 // 修改包子的狀態為true 有 87 baozi.flag = true; 88 // 喚醒吃貨線程,讓吃貨線程去吃包子 89 baozi.notify(); 90 System.out.println("包子鋪已經生產好了:" + baozi.pi + baozi.xian + "包子,吃貨可以開始吃了。。"); 91 } 92 } 93 } 94 } 95 /* 96 * 消費者(吃貨)類:是一個線程類 extends Thread 97 * 設置線程的任務:吃包子 98 * 對包子的狀態進行判斷 99 * true:有包子 100 * 吃貨吃包子 101 * 吃貨吃完包子 102 * 修改包子的狀態味false:沒有包子 103 * 吃貨喚醒包子鋪線程,生產包子 104 * false:沒有包子 105 * 吃貨調用wait方法,進入到等待狀態 106 */ 107 public class Foodie extends Thread{ 108 // 1. 需要在成員的位置上定義一個包子變量 109 private Baozi baozi; 110 111 //2.使用帶參構造,為這個包子變量賦值 112 public Foodie(Baozi baozi) { 113 this.baozi = baozi; 114 } 115 116 //3. 重寫run方法 117 @Override 118 public void run() { 119 // 設置線程任務:吃包子 120 // 使用死循環,讓吃貨一直吃包子 121 while(true) { 122 // 使用同步技術保證兩個線程只有一個線程在執行 123 synchronized (baozi) { 124 // 對包子的狀態進行判斷 125 if (baozi.flag == false) { 126 // 讓吃貨線程進入到等待狀態 127 try { 128 baozi.wait(); 129 } catch (InterruptedException e) { 130 e.printStackTrace(); 131 } 132 } 133 // 被喚醒后執行吃包子 134 System.out.println("吃貨正在吃:" + baozi.pi + baozi.xian + "包子"); 135 // 吃貨吃完包子 136 // 修改包子的狀態為false 沒有 137 baozi.flag = false; 138 // 吃貨線程喚醒包子鋪線程--->生產包子 139 baozi.notify(); 140 System.out.println("吃貨已經把" + baozi.pi + baozi.xian + "的包子"); 141 System.out.println("------------------------------------------"); 142 } 143 } 144 } 145 } 146 public class TestChihuoAndBaoziPuDemo { 147 148 public static void main(String[] args) { 149 // 創建包子對象 150 Baozi baozi = new Baozi(); 151 // 創建包子鋪線程對象 152 new Costs(baozi).start(); 153 // 創建吃貨線程對象 154 new Foodie(baozi).start(); 155 } 156 157 }
線程池
線程池的概念
-
線程池:其實就是一個可以容納多個線程的容器,其中的線程可以反復的使用,省去了頻繁的創建線程對象的操作,無需反復創建線程而消耗過多的系統資源。
由於線程池中有很多操作都是與優化系統資源有關的,線程池的工作原理如下:
合理利用線程池能夠帶來什么樣的好處:
-
-
提高了響應速度。當任務到達時,任務可以不需要等到線程的創建就能立即執行。
-
提高了線程的可管理性。可以根據系統的承受能力,調整線程池中工作線程的數目,防止因為消耗過多的內存,而導致服務器的宕機(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,死機的風險也就更高)。
線程池的使用
java里面線程池的頂級接口是java.util.concurrent.Executor
,但是嚴格意義講,Executor它並不是一個線程池,它只是執行線程的一個工具,真正的線程池接口是java.util.concurrent.ExecutorService
。
因此在java.util.concurrent.Executors
線程工程類提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors來創建線程池對象。
Executors有創建線程池的方法如下:
-
public static ExecutorService newFixedThreadPool(int nThreads):返回的就是線程池對象。(創建的是有界的線程池,也就是池中的線程個數可以指定最大數量)。
獲取到了一個線程池ExecutorService對象,在該類中定義了一個使用線程池對象的方法如下:
-
public Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,並執行。
Future接口:用來記錄線程任務執行完畢后產生的結果。線程的創建與使用。
-
創建線程池對象
-
創建Runnable接口子類對象。(task)
-
提交Runnable接口子類對象。 (take task)
-
Lambda表達式
y = x + 1,在數學中,函數就是有輸入量,輸出量的一套計算方案;也就是“拿什么東西,做什么事情”。相對而言,面向對象過程過分強調"必須通過對象的形式來做事情",而函數式編程思想則盡量忽略面向對象的復雜語法---強調做什么,而不是以什么方式來做。
面向對象的思想:
做一件事情,找一個能解決這個事情的對象,調用對象的方法來完成事情。
函數式編程的思想:
只要能獲得這個事情的結果,誰去做的,怎么做的都不重要,重視的是結果,不重視過程。
冗余的Runnable代碼
當需要啟動一個線程去完成一項任務時,通常會通過Runnable接口來定義任務內容,並且使用Thread類來啟動線程。
1 /* 2 lambda表達式的標准格式: 3 有三部分組成: 4 a:一些參數0,1,...n 5 b:一個箭頭 6 c:一段代碼 7 格式: 8 (參數列表) -> {一些重寫run方法的代碼} 9 格式說明: 10 ():接口中抽象方法,參數列表可以沒有參數,就空着;有參數就寫出參數,多個參數使用逗號隔開 11 ->:傳遞的意思,把方法中的參數傳遞給方法體{} 12 {}:重寫接口的抽象方法的方法體。 13 */ 14 public class Demo02Lambda { 15 16 public static void main(String[] args) { 17 new Thread(new Runnable() { 18 @Override 19 public void run() { 20 System.out.println(Thread.currentThread().getName() + "----->新線程被創建了"); 21 } 22 }).start(); 23 24 // (參數列表) -> {一些重寫run方法的代碼} 25 // 使用lambda表達式實現多線程編程 26 new Thread(() -> { 27 System.out.println(Thread.currentThread().getName() + "----->新線程被創建了"); 28 } 29 ).start(); 30 31 //優化省略Lambda 32 //省略模式需要三項一起省 33 new Thread(() -> System.out.println(Thread.currentThread().getName() + "----->新線程被創建了")).start(); 34 } 35 36 }