Java多執行緒
- 一、創建執行緒四種方式
- 1)繼承Thread
- 2)呼叫Runnable
- 3)匿名內部類
- 4)使用lambda運算式來創建
- 二、了解Thread 類
- 2.1Thread的常見的構造方法
- 2.2Thread的幾個常見的屬性
- 三、啟動一個執行緒
- 四、中斷一個執行緒
- 4.1 讓執行緒的入口方法執行完畢
- 4.2 使用Thread類提供的interrupt方法
- 五、等待執行緒
- 六、執行緒休眠
- 七、執行緒的狀態
- 八、執行緒安全(重要)!!!
- 8.1導致執行緒不安全的原因:
- 8.2解決執行緒安全問題方法
- synchronized
- volatile
- 九、物件等待集
一、創建執行緒四種方式
1)繼承Thread
利用多型機制,繼承于Thread機制
1)創建一個子類,繼承于Thread
2)重寫run 方法
3)創建子類實體
4)呼叫start 方法
public class Demo {
//創建多執行緒
public static void main(String[] args) {
MyThread2 myThread2=new MyThread2();
myThread2.start();
}
}
class MyThread2 extends Thread{
@Override
public void run() {
//這里寫執行緒要執行的代碼
}
}
2)呼叫Runnable
通過實作Runnable 介面,把Runnable 介面的實體賦值給Thread
1)定義Runnable介面的實作類
2)創建Runnable實作類的實體,并用這個實體作為Thread的target來創建Thread物件,這個Thread物件才是真正的執行緒物件
3)呼叫start () 方法
public class Demo {
//創建多執行緒
public static void main(String[] args) {
//通過Runnable 介面來創建
Runnable myTask=new MyTask();
Thread t=new Thread();
t.start();
}
}
class MyTask implements Runnable{
@Override
public void run() {
//重寫run()方法
}
}
Runnable 本質上還是要搭配Thread來使用,只不過和直接繼承Thread相比,換了一種指定任務的方式而已
這兩種方式中Runnable 方式更好一點,能夠讓執行緒本身,和執行緒要執行的任務,更加“解耦合”
3)匿名內部類
通過匿名內部類相當于繼承了Thread,作為子類重寫run()實作
public class Demo {
//創建多執行緒
public static void main(String[] args) {
//通過匿名內部類來實作
Thread t=new Thread(){
@Override
public void run() {
//重寫run方法
}
};
t.start();
}
}
通過Runnable 匿名內部類來實作
public class Demo {
//創建多執行緒
public static void main(String[] args) {
//通過匿名內部類來實作
Thread t=new Thread(new Runnable() {
@Override
public void run() {
//重寫run方法
}
});
t.start();
}
}
4)使用lambda運算式來創建
public class Demo {
//創建多執行緒
public static void main(String[] args) {
Thread t=new Thread(()->{
//撰寫執行緒代碼
}
);
t.start();
}
}
()->{ }這個就是lambda運算式
二、了解Thread 類
2.1Thread的常見的構造方法
| Thread() | 創建執行緒物件 |
|---|---|
| Thread(Runnable target) | 使用Runnable物件創建執行緒物件 |
| Thread(String name) | 創建執行緒物件并命名 |
| Thread(Runnable target,String name) | 使用Runnable物件來創建執行緒,并命名 |
2.2Thread的幾個常見的屬性
| ID | .getId() |
|---|---|
| 名稱 | .getName() |
| 優先級 | .getPriority() |
| 狀態 | .getState() |
| 是否后臺執行緒 | .isDaemon() |
| 是否存活 | .isAlive() |
| 是否被中斷 | .isInterrupted |
| 獲取當前執行緒的實體 | currentThread() |
優先級和執行緒調度有關,由作業系統來完成,
后臺執行緒,不影響整個行程的結束
前臺執行緒,會影響到整個行程的結束
是否存活就是run()方法是否運行結束了
三、啟動一個執行緒
start
start 是Thread類的一個關鍵方法
功能:讓作業系統內核真正創建一個執行緒來執行
start 和run 的區別
start 是創建執行緒(有新的執行流)
呼叫run只是一個普通的方法呼叫,不涉及創建新執行緒(仍然在原來的執行緒中,沒有涉及到新的執行流)
呼叫strat 方法
public class Demo {
//start 和 run 的區別
public static void main(String[] args) {
MyThread2 myThread2=new MyThread2();
myThread2.start();
while(true){
System.out.println("hehe");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
while(true){
System.out.println("haha");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

可以看到兩個執行緒并發執行,
而如果是呼叫run()方法,就是普通的呼叫,沒有創建新執行緒,一直在回圈里出不來

四、中斷一個執行緒
4.1 讓執行緒的入口方法執行完畢
執行緒執行完畢,運行5S 執行緒執行完畢
public class Demo {
//中斷一個執行緒
static boolean isRunning=true;
public static void main(String[] args) {
Thread t1=new Thread(){
@Override
public void run() {
while(isRunning){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isRunning=false;
System.out.println("執行緒運行5S結束");
}
}
4.2 使用Thread類提供的interrupt方法
針對上面的方式進行修改
1,把上面的while()中判斷條件進行修改
2,把catch里面的代碼,加一個break
呼叫 interrupt()是通知執行緒結束,具體還是看內部代碼的實作
public class Demo {
//使用Thread 方法來中斷執行緒
public static void main(String[] args) {
Thread t1=new Thread(){
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
break;
}
}
}
};
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒結束");
t1.interrupt();
}
}
五、等待執行緒
我們在創建多個執行緒之后,每個執行緒都是一個獨立的執行流~
這些執行緒每個執行緒的執行順序都是不確定的,完全取決于作業系統的調度,這里的等待執行緒機制就是一種確定執行緒先后順序方式,確定執行緒的結束順序,無法確定誰先開始,可以確定誰先結束 使用join()
join 起到的效果就是等待某個執行緒結束,誰呼叫join就等待誰結束
通過代碼來解釋:
public class Demo {
//使用Thread 方法來中斷執行緒
public static void main(String[] args) {
Thread t1=new Thread(){
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
這里在main 方法中呼叫了join,相當于在主執行緒中,等待t1執行緒結束
main方法在執行的時候,遇到join就會堵塞等待,一直等t1執行緒執行完畢,這個時候join才會繼續往下執行
也就是說誰呼叫join 誰先結束
六、執行緒休眠
當執行sleep時就是讓執行緒休眠,所謂的休眠就是把執行緒的task struct放入等待佇列
CPU在執行的時候是挑等待佇列中的執行緒來執行的,而sleep的執行緒在等待佇列不在就緒佇列,所以不會被執行
也可以在sleep中加上時間,等時間過去后,等待佇列的執行緒才有機會到就緒佇列,至于什么時候到就緒佇列還是要看調度器執行
等待佇列可能有好多了,具體誰先出佇列,先回到就緒佇列和設定的時間相關,如果時間一樣就看系統的調度
七、執行緒的狀態
在我們除錯多執行緒程式有幫助
| NEW | Thread 物件剛創建,還沒有在系統中創建執行緒,相當于任務交給了執行緒,但是執行緒還沒有開始執行 |
|---|---|
| Runnable | 執行緒是一個準備就緒的狀態,隨時可能調度到CPU上執行,或者正在CPU上執行(執行緒的task struct在就緒佇列中) |
| Blocked | 執行緒堵塞(執行緒在等待佇列里)沒有競爭到鎖 |
| Waiting | 執行緒堵塞(執行緒在等待佇列里)呼叫waiting 方法 |
| Timed_Waiting | 執行緒堵塞(執行緒在等待佇列里)呼叫sleep方法 |
| Terminated | 執行緒結束了(Thread物件還沒銷毀) |
八、執行緒安全(重要)!!!
多執行緒雖然是更輕量的并發編程(相比于行程),但是執行緒是訪問同一份記憶體資源,由于執行緒是一個搶占式執行的程序中誰先執行,誰后執行,不確定,完全卻決于系統的調度,由于這里不確定性太多就可能導致多個執行緒訪問同一個資源的時候,出現BUG所以引出了執行緒安全問題, 訪問分為讀和寫操作,讀操作不會涉及到執行緒安全問題,只有寫操作涉及執行緒安全作業
多執行緒修改同一變數
public class Demo {
//執行緒安全問題
static class Counter{
//創建一個自增類,通過執行緒調度來展示執行緒安全問題
public int count=0;
public void increase(){
count++;
}
}
public static void main(String[] args) {
//通過兩個執行緒同時對count進行自增
Counter counter=new Counter();
Thread t1=new Thread(){
@Override
public void run() {
for(int i=0;i<50000;i++){
counter.increase();
}
}
};
Thread t2=new Thread(){
@Override
public void run() {
for(int i=0;i<50000;i++){
counter.increase();
}
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.count);
}
}
此處代碼就是t1,t2 兩個執行緒修改同一變數,存在執行緒安全問題,正常情況下count值應該是100000,但是運行下來,count值是在50000~100000之間,每次都在變化,這是為什么呢? 這里就是觸發了執行緒安全的問題
這里我們先看count ++ 具體做了什么事情,這里我們就要引入JMM了
JMM(JVM 實作方式的抽象,Java 程式和記憶體之間是如何互動的)
1)先把記憶體資料讀取到CPU的暫存器中
2)針對暫存器中的內容,通過類似于ADD這樣的指令進行+1,操作的結果仍然是放在暫存器中里
3)把暫存器中的資料,寫回到記憶體中

由于多執行緒之間是搶占式執行的,可能第一個執行緒執行自增一半時,就可能被調度出CPU,由第二個執行緒再次自增

LOAD就是從記憶體中讀取資料到暫存器 ADD就是進行自增效果 SAVE就是將暫存器中資料寫回記憶體中
如果出現第一個執行緒讀取到資料為0時,在進行自增操作時,執行緒二也進行讀取資料,兩個執行緒都讀到的是0,相當于兩次自增操作只增加了一次,只要是執行緒二的讀取不是在執行緒一SAVE操作后,就會發生自增例外的情況!所以兩個執行緒分別自增50000次,資料最后的數值是在50000~100000之間的,這就是搶占式執行同一個資源所帶來的例外!
如果這里是兩個CPU也是同樣的情況,這樣的不確定性,不符合預期的要求,就認為是BUG,因為我們執行代碼就是追求的是確定性
8.1導致執行緒不安全的原因:
由于多執行緒是搶占式執行,可能會出現第一個執行緒自增執行到一半就被調度出去,就會執行第二個執行緒,
1)執行緒的搶占式執行程序(無法修改)作業系統內核實作的
2)多個執行緒修改同一變數
3)修改操作不是原子性 (原子性:不可拆分)如果三個操作(讀取,自增,寫回)打包成一個整體,這就能解決了執行緒不安全問題,保證操作原子性,是保證執行緒安全的主要手段
4)記憶體可見性
兩個執行緒同時操作一個記憶體,比如一個讀一個寫,寫操作的執行緒進行修改時,讀執行緒讀取到的可能是修改之前的結果,也可能是讀取到修改之后的結果,也會帶來執行緒安全問題
記憶體可見性也可能是編譯器優化,假設執行一個回圈自增,這樣的操作就涉及到大量的讀寫操作,讀寫記憶體的操作比訪問CPU暫存器要慢幾千倍,所以JVM往往對指令進行優化,把它等價轉換成另外一種情況 保證邏輯不變的情況下,讀取一次記憶體,之后進行自增,自增結束后在寫回記憶體,節省了很多的 讀寫記憶體的開銷,但是這樣會觸發執行緒不安全,
解決可見性,方案就是直接禁止這樣的編譯器的優化,讓程式跑慢點,關鍵是要對,不能在多執行緒情況下出錯
5)指令重排序
和執行緒不安全直接相關,也是和編譯器優化直接相關,為了讓程式跑的更快,調整了執行順序~(調整的前提邏輯不改變,但是效率提高),如果是多執行緒的情況下可能重排會改變邏輯,會導致執行緒不安全問題,
8.2解決執行緒安全問題方法
1)多執行緒不修改同一變數
synchronized
synchronized(關鍵字) 監視器鎖 ,我們通過加鎖來保證操作原子性,同時禁止指令重排序和保證記憶體可見性
用法:
1.修飾一個方法 (方法前加上,就是針對代碼進行加鎖作業,呼叫方法就加鎖,出了代碼塊就解鎖)

2.修飾一個代碼塊(包裹起來) 針對哪個物件加鎖,括號內就填哪個物件

分析synchroized 作業程序
使用synchronized 就是 相當于增加于給操作增加了兩個指令 LOCK UNLOCK
LOCK 操作的特性,只有一個執行緒能執行成功,直到另一個執行緒釋放UNLOCK 另一個執行緒才能執行
就比如上面演示的自增操作,加鎖就相當于把LOAD ADD SAVE 三個操作打包為一個操作,這樣就解決了執行緒的原子性,并且synchronized也能禁止編譯器的進行記憶體可見性和指令重排序,所以使用synchronized就解決了執行緒安全問題,但是synchronized也付出了代價,程式運行的效率大大降低了,
注意synchroized 括號內填什么
針對哪個物件加鎖,就填哪個物件,每一個物件能都加鎖,如果多個執行緒競爭同一鎖物件(嘗試對同一個物件加鎖,此時就會出現一個競爭成功,其他等待的情況),如果兩個執行緒競爭不同的鎖,兩個執行緒都能成功獲取到鎖,
如果synchronized修飾的就是方法,相當于加鎖的物件是this
如果synchronized修飾靜態方法,相當于加鎖的物件是類物件
錯誤示范:1)加鎖物件錯誤

此時由于我們加鎖的不是同一個物件,第一個為t1,第二個是t2,這兩個加鎖操作就不會構成競爭,沒有作用
2)嵌套加鎖 :

這里我們在給increase方法加了鎖,還對counter加了一次鎖,相當于連續加了兩次鎖,這會產生什么情況?
會產生死鎖,因為
1)執行程式時,運行到回圈時,因為對counter 物件就行了加鎖,此時用LOCK將counter 物件鎖起來
2)當程式運行到呼叫increase () 方法時,這個方法也有加鎖,但是此時無法加鎖,因為有鎖在上面,所以increase ()方法就進入堵塞等待,等待上一個加鎖操作進行釋放~
3)但是上一個加鎖釋放是要執行完increase ()方法的,但是此時方法無法執行,這里就產生了死鎖情況!
但是如果運行程式的話,可以運行成功,因為synchroized 內部對這種狀況進行了解決,利用特殊的手段來處理這個場景“可重入鎖”
synchroized 如何實作的可重入鎖效果?
synchroized內部記錄了當前這把鎖時哪個執行緒持有的, 如果當前加鎖執行緒和持有執行緒是同一執行緒,而不是真的進行加鎖,而是把一個計數器++ ,如果后續該執行緒繼續嘗試獲取鎖,繼續判定加鎖執行緒和持有執行緒是不是同一執行緒,只要是同一執行緒,就不真正加鎖,而是計數器++,如果該執行緒呼叫解鎖操作,也不是立即解鎖,而是計數器- - ,直到計數器減為0了,才認為真的要“釋放鎖了 ”,才允許其他執行緒來獲取鎖~
volatile
起到的效果也是輔助保證執行緒安全~~ 主要是用于讀寫同一個變數的時候
volatile能夠禁止指令重排序,和記憶體可見性,但是不能保證原子性
我們來設計一個場景 執行緒一進行回圈,執行緒二通過修改回圈條件,來使得執行緒1回圈結束
import java.util.Scanner;
public class Demo {
static class Counter{
public int flag=0;
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1=new Thread(){
@Override
public void run() {
while(counter.flag==0){
//do nothing
}
System.out.println("執行緒一回圈結束");
}
};
t1.start();
Thread t2=new Thread(){
@Override
public void run() {
Scanner scanner=new Scanner(System.in);
System.out.println("請輸入一個整數");
counter.flag=scanner.nextInt();
}
};
t2.start();
}
}
我們預想的效果是輸入一個非0整數之后,回圈停止,但是運行程式發現,回圈并沒有停止,這是因為記憶體可見性,如果沒有優化,此時CPU就需要頻繁的讀取記憶體資料,但是這時進行了優化,編譯器就把這個讀操作優化成只從記憶體中讀一次,后續都直接讀暫存器中數值,即使記憶體中數值發生變化,也不會讀取到,
這種場景情況下,使用synchroized也可以,但是沒必要因為volatile 比synchroized更輕量化,解決方法就是在flag 前加上volatile,加上volatile之后執行緒一每次讀取flag的值都必須從記憶體中讀取了(效率降低了,但是代碼邏輯準確了)
但是volatile只使用于一讀一寫的情況,如果多個執行緒都要執行寫操作,那么volatile就沒有作用了,就要使用synchroized了
九、物件等待集
功能: 協調多個執行緒之間執行的先后順序
物件等待集的應用場景
由于多執行緒之間是一個”搶占式執行“,可能會導致某個執行緒一直占用,其他執行緒就會出現執行緒餓死的情況,等待集就是解決執行緒太頻繁占用,
實作等待集: wait () ,notify() ,notifyAll ()
wait /notify 這一系列方法必須搭配,synchronized 來使用,如果不在synchronized 使用就會出現例外,因為當前預期是獲取到鎖的狀態才能呼叫wait(),沒有synchronized相當于還沒獲取到鎖,就嘗試呼叫,于是就會出現例外
wait 內部做了三件事
1.釋放鎖
2. 等待其他執行緒的通知
3.等待通知之后,重新嘗試獲取鎖
notify()
通知某個執行緒被喚醒,從wait中醒來,notify也是在synchroized中使用,呼叫notify()方法之后,代碼不會立即釋放鎖,而是在執行完當前的synchroized之后才釋放鎖,同時等待中的執行緒就嘗試重新競爭這個鎖
演示wait 和 notify 用法
//演示wait 和 notify 用法
public class Demo {
//創建一個鎖物件
static public Object Locker=new Object();
//用來等待的執行緒
static class WaitTask implements Runnable{
@Override
public void run() {
synchronized (Locker){
while (true){
try {
System.out.println("wait 開始");
Locker.wait();
System.out.println("wait 結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//用來通知的執行緒
static class NoitfyTask implements Runnable{
@Override
public void run() {
synchronized (Locker){
System.out.println("nofity 開始");
Locker.notify();
System.out.println("nofity 結束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new WaitTask());
Thread t2=new Thread(new NoitfyTask());
t1.start();
Thread.sleep(3000);
t2.start();
}
}

1.先運行WaitTask(),執行到wait ()方法后,釋放鎖并等待通知
2.3S后開始執行nofity Task 方法
3.執行nofity Task()方法之后,喚醒WaitTask 執行緒,WaitTask就從WAITING 狀態醒來,嘗試競爭鎖,由于當前鎖沒有被nofity Task釋放,于是競爭鎖失敗,進入堵塞佇列,進入BLOCKED 狀態
4.NoitfyTask執行完畢后,鎖就被釋放了,WaitTask()才能夠競爭到鎖,于是就從wait內部回傳了,于是繼續執行,列印“wait 結束”
5.繼續進入下次回圈在進行等待,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/292936.html
標籤:java
