多線程詳解

1.1 多線程介紹

學(xué)習(xí)多線程之前,我們先要了解幾個關(guān)于多線程有關(guān)的概念。

進程:進程指正在運行的程序。確切的來說,當一個程序進入內(nèi)存運行,即變成一個進程,進程是處于運行過程中的程序,并且具有一定獨立功能。


image.png

線程:線程是進程中的一個執(zhí)行單元,負責當前進程中程序的執(zhí)行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應(yīng)用程序也可以稱之為多線程程序。

簡而言之:一個程序運行后至少有一個進程,一個進程中可以包含多個線程


image.png

什么是多線程呢?即就是一個程序中有多個線程在同時執(zhí)行。

通過下圖來區(qū)別單線程程序與多線程程序的不同:

l 單線程程序:即,若有多個任務(wù)只能依次執(zhí)行。當上一個任務(wù)執(zhí)行結(jié)束后,下一個任務(wù)開始執(zhí)行。如,去網(wǎng)吧上網(wǎng),網(wǎng)吧只能讓一個人上網(wǎng),當這個人下機后,下一個人才能上網(wǎng)。

l 多線程程序:即,若有多個任務(wù)可以同時執(zhí)行。如,去網(wǎng)吧上網(wǎng),網(wǎng)吧能夠讓多個人同時上網(wǎng)。


image.png

1.2 程序運行原理

  • 分時調(diào)度

所有線程輪流使用 CPU 的使用權(quán),平均分配每個線程占用 CPU 的時間。

  • 搶占式調(diào)度

優(yōu)先讓優(yōu)先級高的線程使用 CPU,如果線程的優(yōu)先級相同,那么會隨機選擇一個(線程隨機性),Java使用的為搶占式調(diào)度。


image.png
1.2.1 搶占式調(diào)度詳解

大部分操作系統(tǒng)都支持多進程并發(fā)運行,現(xiàn)在的操作系統(tǒng)幾乎都支持同時運行多個程序。比如:現(xiàn)在我們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開著畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,”感覺這些軟件好像在同一時刻運行著“。

image.png

實際上,CPU(中央處理器)使用搶占式調(diào)度模式在多個線程間進行著高速的切換。對于CPU的一個核而言,某個時刻,只能執(zhí)行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。

其實,多線程程序并不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

1.3 主線程

回想我們以前學(xué)習(xí)中寫過的代碼,當我們在dos命令行中輸入java空格類名回車后,啟動JVM,并且加載對應(yīng)的class文件。虛擬機并會從main方法開始執(zhí)行我們的程序代碼,一直把main方法的代碼執(zhí)行結(jié)束。如果在執(zhí)行過程遇到循環(huán)時間比較長的代碼,那么在循環(huán)之后的其他代碼是不會被馬上執(zhí)行的。如下代碼演示:

class Demo{

String name;

Demo(String name){

   this.name = name;

}

void show() {

   for (int i=1;i<=10000 ;i++ ) {

        System.out.println("name="+name+",i="+i);

    }

}
}

 

class ThreadDemo {

public static void main(String[] args) {

         Demo d = new Demo("小強");

         Demo d2 = new Demo("旺財");

         d.show();

         d2.show();

         System.out.println("Hello World!");

}

}

若在上述代碼中show方法中的循環(huán)執(zhí)行次數(shù)很多,這時在d.show();下面的代碼是不會馬上執(zhí)行的,并且在dos窗口會看到不停的輸出name=小強,i=值,這樣的語句。為什么會這樣呢?

原因是:jvm啟動后,必然有一個執(zhí)行路徑(線程)從main方法開始的,一直執(zhí)行到main方法結(jié)束,這個線程在java中稱之為主線程。當程序的主線程執(zhí)行時,如果遇到了循環(huán)而導(dǎo)致程序在指定位置停留時間過長,則無法馬上執(zhí)行下面的程序,需要等待循環(huán)結(jié)束后能夠執(zhí)行。

那么,能否實現(xiàn)一個主線程負責執(zhí)行其中一個循環(huán),再由另一個線程負責其他代碼的執(zhí)行,最終實現(xiàn)多部分代碼同時執(zhí)行的效果?

能夠?qū)崿F(xiàn)同時執(zhí)行,通過Java中的多線程技術(shù)來解決該問題。

1.4 Thread類

該如何創(chuàng)建線程呢?通過API中搜索,查到Thread類。通過閱讀Thread類中的描述。Thread是程序中的執(zhí)行線程。Java 虛擬機允許應(yīng)用程序并發(fā)地運行多個執(zhí)行線程。

  • 構(gòu)造方法


    image.png
  • 常用方法


    image.png

    繼續(xù)閱讀,發(fā)現(xiàn)創(chuàng)建新執(zhí)行線程有兩種方法。

  • 一種方法是將類聲明為 Thread 的子類。該子類應(yīng)重寫 Thread 類的 run 方法。創(chuàng)建對象,開啟線程。run方法相當于其他線程的main方法。

  • 另一種方法是聲明一個實現(xiàn) Runnable 接口的類。該類然后實現(xiàn) run 方法。然后創(chuàng)建Runnable的子類對象,傳入到某個線程的構(gòu)造方法中,開啟線程。

1.5 創(chuàng)建線程方式一繼承Thread類

創(chuàng)建線程的步驟:

1 定義一個類繼承Thread。

2 重寫run方法。

3 創(chuàng)建子類對象,就是創(chuàng)建線程對象。

4 調(diào)用start方法,開啟線程并讓線程執(zhí)行,同時還會告訴jvm去調(diào)用run方法。

  • 測試類
public class Demo01 {

public static void main(String[] args) {

//創(chuàng)建自定義線程對象

MyThread mt = new MyThread("新的線程!");

//開啟新線程

mt.start();

//在主方法中執(zhí)行for循環(huán)

for (int i = 0; i < 10; i++) {

System.out.println("main線程!"+i);

}

}

}
  • 自定義線程類
public class MyThread extends Thread {

//定義指定線程名稱的構(gòu)造方法

public MyThread(String name) {

//調(diào)用父類的String參數(shù)的構(gòu)造方法,指定線程的名稱

super(name);

}

/**

 * 重寫run方法,完成該線程執(zhí)行的邏輯

 */

@Override

public void run() {

for (int i = 0; i < 10; i++) {

System.out.println(getName()+":正在執(zhí)行!"+i);

}

}

}

思考:線程對象調(diào)用 run方法和調(diào)用start方法區(qū)別?

線程對象調(diào)用run方法不開啟線程。僅是對象調(diào)用方法。線程對象調(diào)用start開啟線程,并讓jvm調(diào)用run方法在開啟的線程中執(zhí)行。

1.5.1 繼承Thread類原理

我們?yōu)槭裁匆^承Thread類,并調(diào)用其的start方法才能開啟線程呢?

繼承Thread類:因為Thread類用來描述線程,具備線程應(yīng)該有功能。那為什么不直接創(chuàng)建Thread類的對象呢?如下代碼:

Thread t1 = new Thread();

t1.start();//這樣做沒有錯,但是該start調(diào)用的是Thread類中的run方法,而這個run方法沒有做什么事情,更重要的是這個run方法中并沒有定義我們需要讓線程執(zhí)行的代碼。

創(chuàng)建線程的目的是什么?

是為了建立程序單獨的執(zhí)行路徑,讓多部分代碼實現(xiàn)同時執(zhí)行。也就是說線程創(chuàng)建并執(zhí)行需要給定線程要執(zhí)行的任務(wù)。

對于之前所講的主線程,它的任務(wù)定義在main函數(shù)中。自定義線程需要執(zhí)行的任務(wù)都定義在run方法中。

Thread類run方法中的任務(wù)并不是我們所需要的,只有重寫這個run方法。既然Thread類已經(jīng)定義了線程任務(wù)的編寫位置(run方法),那么只要在編寫位置(run方法)中定義任務(wù)代碼即可。所以進行了重寫run方法動作。

1.5.2 多線程的內(nèi)存圖解

多線程執(zhí)行時,到底在內(nèi)存中是如何運行的呢?

以上個程序為例,進行圖解說明:

多線程執(zhí)行時,在棧內(nèi)存中,其實每一個執(zhí)行線程都有一片自己所屬的棧內(nèi)存空間。進行方法的壓棧和彈棧。


image.png

當執(zhí)行線程的任務(wù)結(jié)束了,線程自動在棧內(nèi)存中釋放了。但是當所有的執(zhí)行線程都結(jié)束了,那么進程就結(jié)束了。

1.5.3 獲取線程名稱

開啟的線程都會有自己的獨立運行棧內(nèi)存,那么這些運行的線程的名字是什么呢?該如何獲取呢?既然是線程的名字,按照面向?qū)ο蟮奶攸c,是哪個對象的屬性和誰的功能,那么我們就去找那個對象就可以了。查閱Thread類的API文檔發(fā)現(xiàn)有個方法是獲取當前正在運行的線程對象。還有個方法是獲取當前線程對象的名稱。既然找到了,我們就可以試試。


image.png
  • Thread.currentThread()獲取當前線程對象

  • Thread.currentThread().getName();獲取當前線程對象的名稱

class MyThread extends Thread {  //繼承Thread

MyThread(String name){

super(name);

}

//復(fù)寫其中的run方法

public void run(){

for (int i=1;i<=20 ;i++ ){

System.out.println(Thread.currentThread().getName()+",i="+i);

}

}

}

class ThreadDemo {

public static void main(String[] args) {

//創(chuàng)建兩個線程任務(wù)

MyThread d = new MyThread();

MyThread d2 = new MyThread();

d.run();//沒有開啟新線程, 在主線程調(diào)用run方法

d2.start();//開啟一個新線程,新線程調(diào)用run方法

}

}

通過結(jié)果觀察,原來主線程的名稱:main;自定義的線程:Thread-0,線程多個時,數(shù)字順延。如Thread-1......

進行多線程編程時,不要忘記了Java程序運行是從主線程開始,main方法就是主線程的線程執(zhí)行內(nèi)容。

1.6 創(chuàng)建線程方式—實現(xiàn)Runnable接口

創(chuàng)建線程的另一種方法是聲明實現(xiàn) Runnable 接口的類。該類然后實現(xiàn) run 方法。然后創(chuàng)建Runnable的子類對象,傳入到某個線程的構(gòu)造方法中,開啟線程。

為何要實現(xiàn)Runnable接口,Runable是啥玩意呢?繼續(xù)API搜索。

查看Runnable接口說明文檔:Runnable接口用來指定每個線程要執(zhí)行的任務(wù)。包含了一個 run 的無參數(shù)抽象方法,需要由接口實現(xiàn)類重寫該方法。

  • 接口中的方法


    image.png
  • Thread類構(gòu)造方法


    image.png

    創(chuàng)建線程的步驟。

1、定義類實現(xiàn)Runnable接口。

2、覆蓋接口中的run方法。。

3、創(chuàng)建Thread類的對象

4、將Runnable接口的子類對象作為參數(shù)傳遞給Thread類的構(gòu)造函數(shù)。

5、調(diào)用Thread類的start方法開啟線程。

  • 代碼演示:
public class Demo02 {

public static void main(String[] args) {

//創(chuàng)建線程執(zhí)行目標類對象

Runnable runn = new MyRunnable();

//將Runnable接口的子類對象作為參數(shù)傳遞給Thread類的構(gòu)造函數(shù)

Thread thread = new Thread(runn);

Thread thread2 = new Thread(runn);

//開啟線程

thread.start();

thread2.start();

for (int i = 0; i < 10; i++) {

System.out.println("main線程:正在執(zhí)行!"+i);

}

}

}
  • 自定義線程執(zhí)行任務(wù)類
public class MyRunnable implements Runnable{

 

//定義線程要執(zhí)行的run方法邏輯

@Override

public void run() {

 

for (int i = 0; i < 10; i++) {

System.out.println("我的線程:正在執(zhí)行!"+i);

}

}

}
1.6.1 實現(xiàn)Runnable的原理

為什么需要定一個類去實現(xiàn)Runnable接口呢?繼承Thread類和實現(xiàn)Runnable接口有啥區(qū)別呢?

實現(xiàn)Runnable接口,避免了繼承Thread類的單繼承局限性。覆蓋Runnable接口中的run方法,將線程任務(wù)代碼定義到run方法中。

創(chuàng)建Thread類的對象,只有創(chuàng)建Thread類的對象才可以創(chuàng)建線程。線程任務(wù)已被封裝到Runnable接口的run方法中,而這個run方法所屬于Runnable接口的子類對象,所以將這個子類對象作為參數(shù)傳遞給Thread的構(gòu)造函數(shù),這樣,線程對象創(chuàng)建時就可以明確要運行的線程的任務(wù)。

1.6.2 實現(xiàn)Runnable的好處

第二種方式實現(xiàn)Runnable接口避免了單繼承的局限性,所以較為常用。實現(xiàn)Runnable接口的方式,更加的符合面向?qū)ο?,線程分為兩部分,一部分線程對象,一部分線程任務(wù)。繼承Thread類,線程對象和線程任務(wù)耦合在一起。一旦創(chuàng)建Thread類的子類對象,既是線程對象,有又有線程任務(wù)。實現(xiàn)runnable接口,將線程任務(wù)單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務(wù)進行解耦。

1.7 線程的匿名內(nèi)部類使用

使用線程的內(nèi)匿名內(nèi)部類方式,可以方便的實現(xiàn)每個線程執(zhí)行不同的線程任務(wù)操作。

  • 方式1:創(chuàng)建線程對象時,直接重寫Thread類中的run方法
new Thread() {

public void run() {

for (int x = 0; x < 40; x++) {

System.out.println(Thread.currentThread().getName()

+ "...X...." + x);

}

}

}.start();
  • 方式2:使用匿名內(nèi)部類的方式實現(xiàn)Runnable接口,重新Runnable接口中的run方法
Runnable r = new Runnable() {

public void run() {

for (int x = 0; x < 40; x++) {

System.out.println(Thread.currentThread().getName()

+ "...Y...." + x);

}

}

};

new Thread(r).start();

2.1 線程池概念

線程池,其實就是一個容納多個線程的容器,其中的線程可以反復(fù)使用,省去了頻繁創(chuàng)建線程對象的操作,無需反復(fù)創(chuàng)建線程而消耗過多資源。


image.png

我們詳細的解釋一下為什么要使用線程池?

在java中,如果每個請求到達就創(chuàng)建一個新線程,開銷是相當大的。在實際使用中,創(chuàng)建和銷毀線程花費的時間和消耗的系統(tǒng)資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。除了創(chuàng)建和銷毀線程的開銷之外,活動的線程也需要消耗系統(tǒng)資源。如果在一個jvm里創(chuàng)建太多的線程,可能會使系統(tǒng)由于過度消耗內(nèi)存或“切換過度”而導(dǎo)致系統(tǒng)資源不足。為了防止資源不足,需要采取一些辦法來限制任何給定時刻處理的請求數(shù)目,盡可能減少創(chuàng)建和銷毀線程的次數(shù),特別是一些資源耗費比較大的線程的創(chuàng)建和銷毀,盡量利用已有對象來進行服務(wù)。

線程池主要用來解決線程生命周期開銷問題和資源不足問題。通過對多個任務(wù)重復(fù)使用線程,線程創(chuàng)建的開銷就被分攤到了多個任務(wù)上了,而且由于在請求到達時線程已經(jīng)存在,所以消除了線程創(chuàng)建所帶來的延遲。這樣,就可以立即為請求服務(wù),使用應(yīng)用程序響應(yīng)更快。另外,通過適當?shù)恼{(diào)整線程中的線程數(shù)目可以防止出現(xiàn)資源不足的情況。

2.2 使用線程池方式--Runnable接口

通常,線程池都是通過線程池工廠創(chuàng)建,再調(diào)用線程池中的方法獲取線程,再通過線程去執(zhí)行任務(wù)方法。

  • Executors:線程池創(chuàng)建工廠類

  • public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象

  • ExecutorService:線程池類

  • Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,并執(zhí)行

  • Future接口:用來記錄線程任務(wù)執(zhí)行完畢后產(chǎn)生的結(jié)果。線程池創(chuàng)建與使用

  • 使用線程池中線程對象的步驟:

1. 創(chuàng)建線程池對象

2. 創(chuàng)建Runnable接口子類對象

3. 提交Runnable接口子類對象

4. 關(guān)閉線程池

代碼演示:

public class ThreadPoolDemo {

public static void main(String[] args) {

//創(chuàng)建線程池對象

ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象

//創(chuàng)建Runnable實例對象

MyRunnable r = new MyRunnable();

 

//自己創(chuàng)建線程對象的方式

//Thread t = new Thread(r);

//t.start(); ---> 調(diào)用MyRunnable中的run()

 

//從線程池中獲取線程對象,然后調(diào)用MyRunnable中的run()

service.submit(r);

//再獲取個線程對象,調(diào)用MyRunnable中的run()

service.submit(r);

service.submit(r);

//注意:submit方法調(diào)用結(jié)束后,程序并不終止,是因為線程池控制了線程的關(guān)閉。將使用完的線程又歸還到了線程池中

 

//關(guān)閉線程池

//service.shutdown();

}

}
  • Runnable接口實現(xiàn)類
public class MyRunnable implements Runnable {

@Override

public void run() {

System.out.println("我要一個教練");

 

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("教練來了: " +Thread.currentThread().getName());

System.out.println("教我游泳,交完后,教練回到了游泳池");

}

}

2.3 使用線程池方式—Callable接口

  • Callable接口:與Runnable接口功能相似,用來指定線程的任務(wù)。其中的call()方法,用來返回線程任務(wù)執(zhí)行完畢后的結(jié)果,call方法可拋出異常。

  • ExecutorService:線程池類

  • <T> Future<T> submit(Callable<T> task):獲取線程池中的某一個線程對象,并執(zhí)行線程中的call()方法

  • Future接口:用來記錄線程任務(wù)執(zhí)行完畢后產(chǎn)生的結(jié)果。線程池創(chuàng)建與使用

  • 使用線程池中線程對象的步驟:

1. 創(chuàng)建線程池對象

2. 創(chuàng)建Callable接口子類對象

3. 提交Callable接口子類對象

4. 關(guān)閉線程池

代碼演示:

public class ThreadPoolDemo {

public static void main(String[] args) {

//創(chuàng)建線程池對象

ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象

//創(chuàng)建Callable對象

MyCallable c = new MyCallable();

 

//從線程池中獲取線程對象,然后調(diào)用MyRunnable中的run()

service.submit(c);

 

//再獲取個教練

service.submit(c);

service.submit(c);

//注意:submit方法調(diào)用結(jié)束后,程序并不終止,是因為線程池控制了線程的關(guān)閉。將使用完的線程又歸還到了線程池中

 

//關(guān)閉線程池

//service.shutdown();

}

}
  • Callable接口實現(xiàn)類,call方法可拋出異常、返回線程任務(wù)執(zhí)行完畢后的結(jié)果
public class MyCallable implements Callable {

@Override

public Object call() throws Exception {

System.out.println("我要一個教練:call");

Thread.sleep(2000);

System.out.println("教練來了: " +Thread.currentThread().getName());

System.out.println("教我游泳,交完后,教練回到了游泳池");

return null;

}

}

2.4 線程池練習(xí):返回兩個數(shù)相加的結(jié)果

要求:通過線程池中的線程對象,使用Callable接口完成兩個數(shù)求和操作

  • Future接口:用來記錄線程任務(wù)執(zhí)行完畢后產(chǎn)生的結(jié)果。線程池創(chuàng)建與使用

  • V get() 獲取Future對象中封裝的數(shù)據(jù)結(jié)果

代碼演示:

public class ThreadPoolDemo {

public static void main(String[] args) throws InterruptedException, ExecutionException {

//創(chuàng)建線程池對象

ExecutorService threadPool = Executors.newFixedThreadPool(2);

 

//創(chuàng)建一個Callable接口子類對象

//MyCallable c = new MyCallable();

MyCallable c = new MyCallable(100, 200);

MyCallable c2 = new MyCallable(10, 20);

 

//獲取線程池中的線程,調(diào)用Callable接口子類對象中的call()方法, 完成求和操作

//<Integer> Future<Integer> submit(Callable<Integer> task)

// Future 結(jié)果對象

Future<Integer> result = threadPool.submit(c);

//此 Future 的 get 方法所返回的結(jié)果類型

Integer sum = result.get();

System.out.println("sum=" + sum);

 

//再演示

result = threadPool.submit(c2);

sum = result.get();

System.out.println("sum=" + sum);

//關(guān)閉線程池(可以不關(guān)閉)

 

}

}
  • Callable接口實現(xiàn)類
public class MyCallable implements Callable<Integer> {

//成員變量

int x = 5;

int y = 3;

//構(gòu)造方法

public MyCallable(){

}

public MyCallable(int x, int y){

this.x = x;

this.y = y;

}

 

@Override

public Integer call() throws Exception {

return x+y;

}

}

3.1 知識點總結(jié)

  • 創(chuàng)建線程的方式

  • 方式1,繼承Thread線程類

  • 步驟

1, 自定義類繼承Thread類

2, 在自定義類中重寫Thread類的run方法

3, 創(chuàng)建自定義類對象(線程對象)

4, 調(diào)用start方法,啟動線程,通過JVM,調(diào)用線程中的run方法

  • 方式2,實現(xiàn)Runnable接口

  • 步驟

1, 創(chuàng)建線程任務(wù)類 實現(xiàn)Runnable接口

2, 在線程任務(wù)類中 重寫接口中的run方法

3, 創(chuàng)建線程任務(wù)類對象

4, 創(chuàng)建線程對象,把線程任務(wù)類對象作為Thread類構(gòu)造方法的參數(shù)使用

5, 調(diào)用start方法,啟動線程,通過JVM,調(diào)用線程任務(wù)類中的run方法

4.1 線程安全

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結(jié)果和單線程運行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。

  • 我們通過一個案例,演示線程的安全問題:

電影院要賣票,我們模擬電影院的賣票過程。假設(shè)要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(本場電影只能賣100張票)。

我們來模擬電影院的售票窗口,實現(xiàn)多個窗口同時賣 “功夫熊貓3”這場電影票(多個窗口一起賣這100張票)

需要窗口,采用線程對象來模擬;需要票,Runnable接口子類來模擬

  • 測試類
public class ThreadDemo {

public static void main(String[] args) {

//創(chuàng)建票對象

Ticket ticket = new Ticket();

 

//創(chuàng)建3個窗口

Thread t1  = new Thread(ticket, "窗口1");

Thread t2  = new Thread(ticket, "窗口2");

Thread t3  = new Thread(ticket, "窗口3");

 

t1.start();

t2.start();

t3.start();

}

}
  • 模擬票
public class Ticket implements Runnable {

//共100票

int ticket = 100;

 

@Override

public void run() {

//模擬賣票

while(true){

if (ticket > 0) {

//模擬選坐的操作

try {

Thread.sleep(1);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);

}

}

}

}
image.png

運行結(jié)果發(fā)現(xiàn):上面程序出現(xiàn)了問題

  • 票出現(xiàn)了重復(fù)的票

  • 錯誤的票 0、-1

其實,線程安全問題都是由全局變量及靜態(tài)變量引起的。若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

4.2 線程同步(線程安全處理Synchronized)

java中提供了線程同步機制,它能夠解決上述的線程安全問題。

線程同步的方式有兩種:

  • 方式1:同步代碼塊

  • 方式2:同步方法

4.2.1 同步代碼塊

同步代碼塊: 在代碼塊聲明上 加上synchronized

synchronized (鎖對象) {

可能會產(chǎn)生線程安全問題的代碼

}

同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。

使用同步代碼塊,對電影院賣票案例中Ticket類進行如下代碼修改:

public class Ticket implements Runnable {

//共100票

int ticket = 100;

//定義鎖對象

Object lock = new Object();

@Override

public void run() {

//模擬賣票

while(true){

//同步代碼塊

synchronized (lock){

if (ticket > 0) {

//模擬電影選坐的操作

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);

}

}

}

}

}

當使用了同步代碼塊后,上述的線程的安全問題,解決了。

4.2.2 同步方法

  • 同步方法:在方法聲明上加上synchronized
public synchronized void method(){

    可能會產(chǎn)生線程安全問題的代碼

}

//同步方法中的鎖對象是 this

使用同步方法,對電影院賣票案例中Ticket類進行如下代碼修改:

public class Ticket implements Runnable {

//共100票

int ticket = 100;

//定義鎖對象

Object lock = new Object();

@Override

public void run() {

//模擬賣票

while(true){

//同步方法

method();

}

}

 

//同步方法,鎖對象this

public synchronized void method(){

if (ticket > 0) {

//模擬選坐的操作

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);

}

}

}
  • 靜態(tài)同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){

可能會產(chǎn)生線程安全問題的代碼

}

//靜態(tài)同步方法中的鎖對象是 類名.class

4.3 死鎖

同步鎖使用的弊端:當線程任務(wù)中出現(xiàn)了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發(fā)一種現(xiàn)象:程序出現(xiàn)無限等待,這種現(xiàn)象我們稱為死鎖。這種情況能避免就避免掉。

synchronzied(A鎖){

synchronized(B鎖){

         

}

}

我們進行下死鎖情況的代碼演示:

  • 定義鎖對象類
public class MyLock {

public static final Object lockA = new Object();

public static final Object lockB = new Object();

}
  • 線程任務(wù)類
public class ThreadTask implements Runnable {

int x = new Random().nextInt(1);//0,1

//指定線程要執(zhí)行的任務(wù)代碼

@Override

public void run() {

while(true){

if (x%2 ==0) {

//情況一

synchronized (MyLock.lockA) {

System.out.println("if-LockA");

synchronized (MyLock.lockB) {

System.out.println("if-LockB");

System.out.println("if大口吃肉");

}

}

} else {

//情況二

synchronized (MyLock.lockB) {

System.out.println("else-LockB");

synchronized (MyLock.lockA) {

System.out.println("else-LockA");

System.out.println("else大口吃肉");

}

}

}

x++;

}

}

}
  • 測試類
public class ThreadDemo {

public static void main(String[] args) {

//創(chuàng)建線程任務(wù)類對象

ThreadTask task = new ThreadTask();

//創(chuàng)建兩個線程

Thread t1 = new Thread(task);

Thread t2 = new Thread(task);

//啟動線程

t1.start();

t2.start();

}

}

4.4 Lock接口

查閱API,查閱Lock接口描述,Lock 實現(xiàn)提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。

  • Lock接口中的常用方法


    image.png

    Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。

我們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下代碼修改:

public class Ticket implements Runnable {

//共100票

int ticket = 100;

 

//創(chuàng)建Lock鎖對象

Lock ck = new ReentrantLock();

 

@Override

public void run() {

//模擬賣票

while(true){

//synchronized (lock){

ck.lock();

if (ticket > 0) {

//模擬選坐的操作

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);

}

ck.unlock();

//}

}

}

}

1.5 等待喚醒機制

在開始講解等待喚醒機制之前,有必要搞清一個概念——線程之間的通信:多個線程在處理同一個資源,但是處理的動作(線程的任務(wù))卻不相同。通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。

等待喚醒機制所涉及到的方法:

  • wait() :等待,將正在執(zhí)行的線程釋放其執(zhí)行資格 和 執(zhí)行權(quán),并存儲到線程池中。

  • notify():喚醒,喚醒線程池中被wait()的線程,一次喚醒一個,而且是任意的。

  • notifyAll(): 喚醒全部:可以將線程池中的所有wait() 線程都喚醒。

其實,所謂喚醒的意思就是讓 線程池中的線程具備執(zhí)行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的線程。

仔細查看JavaAPI之后,發(fā)現(xiàn)這些方法 并不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,為什么這些操作線程的方法定義在Object類中?

因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調(diào)用的方法一定定義在Object類中。


image.png

接下里,我們先從一個簡單的示例入手:


image.png

如上圖說示,輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務(wù)是:
  • 1.當input發(fā)現(xiàn)Resource中沒有數(shù)據(jù)時,開始輸入,輸入完成后,叫output來輸出。如果發(fā)現(xiàn)有數(shù)據(jù),就wait();

  • 2.當output發(fā)現(xiàn)Resource中沒有數(shù)據(jù)時,就wait() ;當發(fā)現(xiàn)有數(shù)據(jù)時,就輸出,然后,叫醒input來輸入數(shù)據(jù)。

下面代碼,模擬等待喚醒機制的實現(xiàn):

  • 模擬資源類
public class Resource {

private String name;

private String sex;

private boolean flag = false;

 

public synchronized void set(String name, String sex) {

if (flag)

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

// 設(shè)置成員變量

this.name = name;

this.sex = sex;

// 設(shè)置之后,Resource中有值,將標記該為 true ,

flag = true;

// 喚醒output

this.notify();

}

 

public synchronized void out() {

if (!flag)

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

// 輸出線程將數(shù)據(jù)輸出

System.out.println("姓名: " + name + ",性別: " + sex);

// 改變標記,以便輸入線程輸入數(shù)據(jù)

flag = false;

// 喚醒input,進行數(shù)據(jù)輸入

this.notify();

}

}
  • 輸入線程任務(wù)類
public class Input implements Runnable {

private Resource r;

 

public Input(Resource r) {

this.r = r;

}

 

@Override

public void run() {

int count = 0;

while (true) {

if (count == 0) {

r.set("小明", "男生");

} else {

r.set("小花", "女生");

}

// 在兩個數(shù)據(jù)之間進行切換

count = (count + 1) % 2;

}

}

}
  • 輸出線程任務(wù)類
public class Output implements Runnable {

private Resource r;

 

public Output(Resource r) {

this.r = r;

}

 

@Override

public void run() {

while (true) {

r.out();

}

}

}
  • 測試類
public class ResourceDemo {

public static void main(String[] args) {

// 資源對象

Resource r = new Resource();

// 任務(wù)對象

Input in = new Input(r);

Output out = new Output(r);

// 線程對象

Thread t1 = new Thread(in);

Thread t2 = new Thread(out);

// 開啟線程

t1.start();

t2.start();

}

}

5.1 知識點總結(jié)

  • 同步鎖

多個線程想保證線程安全,必須要使用同一個鎖對象

  • 同步代碼塊
         synchronized (鎖對象){

    可能產(chǎn)生線程安全問題的代碼

}

//同步代碼塊的鎖對象可以是任意的對象
  • 同步方法
         public synchronized void method()

              可能產(chǎn)生線程安全問題的代碼

}

//同步方法中的鎖對象是 this
  • 靜態(tài)同步方法
public synchronized void method()

              可能產(chǎn)生線程安全問題的代碼

}

//靜態(tài)同步方法中的鎖對象是 類名.class
  • 多線程有幾種實現(xiàn)方案,分別是哪幾種?

a, 繼承Thread類

b, 實現(xiàn)Runnable接口

c, 通過線程池,實現(xiàn)Callable接口

  • 同步有幾種方式,分別是什么?

a,同步代碼塊

b,同步方法

靜態(tài)同步方法

  • 啟動一個線程是run()還是start()?它們的區(qū)別?

啟動一個線程是start()

區(qū)別:

start: 啟動線程,并調(diào)用線程中的run()方法

run : 執(zhí)行該線程對象要執(zhí)行的任務(wù)

  • sleep()和wait()方法的區(qū)別

sleep: 不釋放鎖對象, 釋放CPU使用權(quán)

在休眠的時間內(nèi),不能喚醒

wait(): 釋放鎖對象, 釋放CPU使用權(quán)

在等待的時間內(nèi),能喚醒

  • 為什么wait(),notify(),notifyAll()等方法都定義在Object類中

鎖對象可以是任意類型的對象

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ??一個任務(wù)通常就是一個程序,每個運行中的程序就是一個進程。當一個程序運行時,內(nèi)部可能包含了多個順序執(zhí)行流,每個順...
    OmaiMoon閱讀 1,809評論 0 12
  • 0. 簡介 這個系列開始來講解 Java 多線程的知識,這節(jié)就先講解多線程的基本知識。 1. 進程與線程 1.1 ...
    一團撈面閱讀 1,490評論 0 3
  • 多線程作為Java中很重要的一個知識點,在此還是有必要總結(jié)一下的。 先看一下相關(guān)圖例 1. Java線程具有五中基...
    Monster_鼎輝閱讀 431評論 0 0
  • Java多線程學(xué)習(xí) [-] 一擴展javalangThread類 二實現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 3,115評論 1 18
  • 本文主要講了java中多線程的使用方法、線程同步、線程數(shù)據(jù)傳遞、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等。 首先講...
    李欣陽閱讀 2,602評論 1 15

友情鏈接更多精彩內(nèi)容