1 多線程
1.1 多線程介紹
??學(xué)習(xí)多線程之前,我們先要了解幾個(gè)關(guān)于多線程有關(guān)的概念。
??進(jìn)程:進(jìn)程指正在運(yùn)行的程序。確切的來(lái)說(shuō),當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行,即變成一個(gè)進(jìn)程,進(jìn)程是處于運(yùn)行過程中的程序,并且具有一定獨(dú)立功能。

??線程:線程是進(jìn)程中的一個(gè)執(zhí)行單元,負(fù)責(zé)當(dāng)前進(jìn)程中程序的執(zhí)行,一個(gè)進(jìn)程中至少有一個(gè)線程。一個(gè)進(jìn)程中是可以有多個(gè)線程的,這個(gè)應(yīng)用程序也可以稱之為多線程程序。
??簡(jiǎn)而言之:一個(gè)程序運(yùn)行后至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程中可以包含多個(gè)線程

??什么是多線程呢?即就是一個(gè)程序中有多個(gè)線程在同時(shí)執(zhí)行。
??單線程程序:即,若有多個(gè)任務(wù)只能依次執(zhí)行。當(dāng)上一個(gè)任務(wù)執(zhí)行結(jié)束后,下一個(gè)任務(wù)開始執(zhí)行。如,去網(wǎng)吧上網(wǎng),網(wǎng)吧只能讓一個(gè)人上網(wǎng),當(dāng)這個(gè)人下機(jī)后,下一個(gè)人才能上網(wǎng)。
??多線程程序:即,若有多個(gè)任務(wù)可以同時(shí)執(zhí)行。如,去網(wǎng)吧上網(wǎng),網(wǎng)吧能夠讓多個(gè)人同時(shí)上網(wǎng)。

1.2 程序運(yùn)行原理
分時(shí)調(diào)度
??所有線程輪流使用 CPU 的使用權(quán),平均分配每個(gè)線程占用 CPU 的時(shí)間。
搶占式調(diào)度
??優(yōu)先讓優(yōu)先級(jí)高的線程使用 CPU,如果線程的優(yōu)先級(jí)相同,那么會(huì)隨機(jī)選擇一個(gè)(線程隨機(jī)性),Java使用的為搶占式調(diào)度。

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

??實(shí)際上,CPU(中央處理器)使用搶占式調(diào)度模式在多個(gè)線程間進(jìn)行著高速的切換。對(duì)于CPU的一個(gè)核而言,某個(gè)時(shí)刻,只能執(zhí)行一個(gè)線程,而 CPU的在多個(gè)線程間切換速度相對(duì)我們的感覺要快,看上去就是在同一時(shí)刻運(yùn)行。
??其實(shí),多線程程序并不能提高程序的運(yùn)行速度,但能夠提高程序運(yùn)行效率,讓CPU的使用率更高。
1.3 主線程
??回想我們以前學(xué)習(xí)中寫過的代碼,當(dāng)我們?cè)赿os命令行中輸入java空格類名回車后,啟動(dòng)JVM,并且加載對(duì)應(yīng)的class文件。虛擬機(jī)并會(huì)從main方法開始執(zhí)行我們的程序代碼,一直把main方法的代碼執(zhí)行結(jié)束。如果在執(zhí)行過程遇到循環(huán)時(shí)間比較長(zhǎng)的代碼,那么在循環(huán)之后的其他代碼是不會(huì)被馬上執(zhí)行的。
??代碼演示如下:
package com.qtw.api;
public class TestDemo{
public static void main(String[] args) {
System.out.println("start");
show();
System.out.println("end");
}
public static void show(){
int sum = 0;
for(int i = 0; i <= 100; i++){
sum = sum + i;
System.out.println("sum = "+ sum);
}
}
}
??若在上述代碼中show方法中的循環(huán)執(zhí)行次數(shù)很多,這時(shí)在show();下面的代碼是不會(huì)馬上執(zhí)行的,并且在dos窗口會(huì)看到不停的輸出sum=值,這樣的語(yǔ)句。為什么會(huì)這樣呢?
??原因是:jvm啟動(dòng)后,必然有一個(gè)執(zhí)行路徑(線程)從main方法開始的,一直執(zhí)行到main方法結(jié)束,這個(gè)線程在java中稱之為主線程。當(dāng)程序的主線程執(zhí)行時(shí),如果遇到了循環(huán)而導(dǎo)致程序在指定位置停留時(shí)間過長(zhǎng),則無(wú)法馬上執(zhí)行下面的程序,需要等待循環(huán)結(jié)束后能夠執(zhí)行。
??那么,能否實(shí)現(xiàn)一個(gè)主線程負(fù)責(zé)執(zhí)行其中一個(gè)循環(huán),再由另一個(gè)線程負(fù)責(zé)其他代碼的執(zhí)行,最終實(shí)現(xiàn)多部分代碼同時(shí)執(zhí)行的效果?
??能夠?qū)崿F(xiàn)同時(shí)執(zhí)行,通過Java中的多線程技術(shù)來(lái)解決該問題。
1.4 Thread類
??該如何創(chuàng)建線程呢?通過API中搜索,查到Thread類。通過閱讀Thread類中的描述。Thread是程序中的執(zhí)行線程。Java 虛擬機(jī)允許應(yīng)用程序并發(fā)地運(yùn)行多個(gè)執(zhí)行線程。

??構(gòu)造方法

??常用方法

??繼續(xù)閱讀,發(fā)現(xiàn)創(chuàng)建新執(zhí)行線程有兩種方法。
??一種方法是將類聲明為 Thread 的子類。該子類應(yīng)重寫 Thread 類的 run 方法。創(chuàng)建對(duì)象,開啟線程。run方法相當(dāng)于其他線程的main方法。
??另一種方法是聲明一個(gè)實(shí)現(xiàn) Runnable 接口的類。該類然后實(shí)現(xiàn) run 方法。然后創(chuàng)建Runnable的子類對(duì)象,傳入到某個(gè)線程的構(gòu)造方法中,開啟線程。
1.5 創(chuàng)建線程方式一繼承Thread類
創(chuàng)建線程的步驟:
??1. 定義一個(gè)類繼承Thread。
??2. 重寫run方法。
??3. 創(chuàng)建子類對(duì)象,就是創(chuàng)建線程對(duì)象。
??4. 調(diào)用start方法,開啟線程并讓線程執(zhí)行,同時(shí)還會(huì)告訴jvm去調(diào)用run方法。
自定義線程類MyThread .java
package com.qtw.api;
public class MyThread extends Thread {
//定義指定線程名稱的構(gòu)造方法
public MyThread(String name) {
//調(diào)用父類的String參數(shù)的構(gòu)造方法,指定線程的名稱
super(name);
}
/**
* 重寫run方法,完成該線程執(zhí)行的邏輯
*/
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":正在執(zhí)行!"+i);
}
}
}
測(cè)試類TestDemo.java
package com.qtw.api;
public class TestDemo{
public static void main(String[] args) {
//創(chuàng)建自定義線程對(duì)象
MyThread mt1 = new MyThread("新的線程1");
MyThread mt2 = new MyThread("新的線程2");
//開啟新線程
mt1.start();
mt2.start();
//在主方法中執(zhí)行for循環(huán)
for (int i = 0; i < 100; i++) {
System.out.println("main線程!"+i);
}
}
}
??思考:線程對(duì)象調(diào)用 run方法和調(diào)用start方法區(qū)別?
??線程對(duì)象調(diào)用run方法不開啟線程。僅是對(duì)象調(diào)用方法。線程對(duì)象調(diào)用start開啟線程,并讓jvm調(diào)用run方法在開啟的線程中執(zhí)行。
1.5.1 繼承Thread類原理
??我們?yōu)槭裁匆^承Thread類,并調(diào)用其的start方法才能開啟線程呢?
??繼承Thread類:因?yàn)門hread類用來(lái)描述線程,具備線程應(yīng)該有功能。那為什么不直接創(chuàng)建Thread類的對(duì)象呢?如下代碼:
Thread t1 = new Thread();
t1.start();
//這樣做沒有錯(cuò),但是該start調(diào)用的是Thread類中的run方法,
//而這個(gè)run方法沒有做什么事情,更重要的是這個(gè)run方法中并沒有定義我們需要讓線程執(zhí)行的代碼。
??創(chuàng)建線程的目的是什么?
??是為了建立程序單獨(dú)的執(zhí)行路徑,讓多部分代碼實(shí)現(xiàn)同時(shí)執(zhí)行。也就是說(shuō)線程創(chuàng)建并執(zhí)行需要給定線程要執(zhí)行的任務(wù)。
??對(duì)于之前所講的主線程,它的任務(wù)定義在main函數(shù)中。自定義線程需要執(zhí)行的任務(wù)都定義在run方法中。
??Thread類run方法中的任務(wù)并不是我們所需要的,只有重寫這個(gè)run方法。既然Thread類已經(jīng)定義了線程任務(wù)的編寫位置(run方法),那么只要在編寫位置(run方法)中定義任務(wù)代碼即可。所以進(jìn)行了重寫run方法動(dòng)作。
1.5.2 多線程的內(nèi)存圖解
??多線程執(zhí)行時(shí),到底在內(nèi)存中是如何運(yùn)行的呢?以上個(gè)程序?yàn)槔?,進(jìn)行圖解說(shuō)明,多線程執(zhí)行時(shí),在棧內(nèi)存中,其實(shí)每一個(gè)執(zhí)行線程都有一片自己所屬的棧內(nèi)存空間。進(jìn)行方法的壓棧和彈棧。

??當(dāng)執(zhí)行線程的任務(wù)結(jié)束了,線程自動(dòng)在棧內(nèi)存中釋放了。但是當(dāng)所有的執(zhí)行線程都結(jié)束了,那么進(jìn)程就結(jié)束了。
1.5.3 獲取線程名稱
??開啟的線程都會(huì)有自己的獨(dú)立運(yùn)行棧內(nèi)存,那么這些運(yùn)行的線程的名字是什么呢?該如何獲取呢?既然是線程的名字,按照面向?qū)ο蟮奶攸c(diǎn),是哪個(gè)對(duì)象的屬性和誰(shuí)的功能,那么我們就去找那個(gè)對(duì)象就可以了。查閱Thread類的API文檔發(fā)現(xiàn)有個(gè)方法是獲取當(dāng)前正在運(yùn)行的線程對(duì)象。還有個(gè)方法是獲取當(dāng)前線程對(duì)象的名稱。既然找到了,我們就可以試試。

??Thread.currentThread()獲取當(dāng)前線程對(duì)象
??Thread.currentThread().getName();獲取當(dāng)前線程對(duì)象的名稱
自定義線程類MyThread.java
package com.qtw.api;
public class MyThread extends Thread {
/**
* 重寫run方法,完成該線程執(zhí)行的邏輯
*/
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+",i="+i);
}
}
}
測(cè)試類TestDemo.java
package com.qtw.api;
public class TestDemo{
public static void main(String[] args) {
//創(chuàng)建兩個(gè)線程任務(wù)
MyThread d1 = new MyThread();
MyThread d2 = new MyThread();
d1.start();//開啟一個(gè)新線程,新線程調(diào)用run方法
d2.start();//開啟一個(gè)新線程,新線程調(diào)用run方法
}
}
??通過結(jié)果觀察,原來(lái)主線程的名稱:main;自定義的線程:Thread-0,線程多個(gè)時(shí),數(shù)字順延。如Thread-1......
??進(jìn)行多線程編程時(shí),不要忘記了Java程序運(yùn)行是從主線程開始,main方法就是主線程的線程執(zhí)行內(nèi)容。
1.6 創(chuàng)建線程方式—實(shí)現(xiàn)Runnable接口
??創(chuàng)建線程的另一種方法是聲明實(shí)現(xiàn) Runnable 接口的類。該類然后實(shí)現(xiàn) run 方法。然后創(chuàng)建Runnable的子類對(duì)象,傳入到某個(gè)線程的構(gòu)造方法中,開啟線程。
??為何要實(shí)現(xiàn)Runnable接口,Runable是啥玩意呢?繼續(xù)API搜索。
??查看Runnable接口說(shuō)明文檔:Runnable接口用來(lái)指定每個(gè)線程要執(zhí)行的任務(wù)。包含了一個(gè) run 的無(wú)參數(shù)抽象方法,需要由接口實(shí)現(xiàn)類重寫該方法。

接口中的方法

Thread類構(gòu)造方法

創(chuàng)建線程的步驟。
??1. 定義類實(shí)現(xiàn)Runnable接口。
??2. 覆蓋接口中的run方法。。
??3. 創(chuàng)建Thread類的對(duì)象
??4. 將Runnable接口的子類對(duì)象作為參數(shù)傳遞給Thread類的構(gòu)造函數(shù)。
??5. 調(diào)用Thread類的start方法開啟線程。
自定義線程執(zhí)行任務(wù)類MyRunnable.java
package com.qtw.api;
public class MyRunnable implements Runnable{
//實(shí)現(xiàn)run方法,同時(shí)定義線程要執(zhí)行的run方法邏輯
public void run(){
for(int i = 1; i <= 100; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
測(cè)試類TestDemo.java
package com.qtw.api;
public class TestDemo{
public static void main(String[] args) {
//創(chuàng)建線程執(zhí)行目標(biāo)類對(duì)象
Runnable runn1 = new MyRunnable();
Runnable runn2 = new MyRunnable();
//將Runnable接口的子類對(duì)象作為參數(shù)傳遞給Thread類的構(gòu)造函數(shù)
Thread thread1 = new Thread(runn1);
Thread thread2 = new Thread(runn2);
//開啟線程
thread1.start();
thread2.start();
}
}
1.6.1 實(shí)現(xiàn)Runnable的原理
??為什么需要定一個(gè)類去實(shí)現(xiàn)Runnable接口呢?繼承Thread類和實(shí)現(xiàn)Runnable接口有啥區(qū)別呢?
??實(shí)現(xiàn)Runnable接口,避免了繼承Thread類的單繼承局限性。覆蓋Runnable接口中的run方法,將線程任務(wù)代碼定義到run方法中。
??創(chuàng)建Thread類的對(duì)象,只有創(chuàng)建Thread類的對(duì)象才可以創(chuàng)建線程。線程任務(wù)已被封裝到Runnable接口的run方法中,而這個(gè)run方法所屬于Runnable接口的子類對(duì)象,所以將這個(gè)子類對(duì)象作為參數(shù)傳遞給Thread的構(gòu)造函數(shù),這樣,線程對(duì)象創(chuàng)建時(shí)就可以明確要運(yùn)行的線程的任務(wù)。
1.6.2 實(shí)現(xiàn)Runnable的好處
??第二種方式實(shí)現(xiàn)Runnable接口避免了單繼承的局限性,所以較為常用。實(shí)現(xiàn)Runnable接口的方式,更加的符合面向?qū)ο?,線程分為兩部分,一部分線程對(duì)象,一部分線程任務(wù)。繼承Thread類,線程對(duì)象和線程任務(wù)耦合在一起。一旦創(chuàng)建Thread類的子類對(duì)象,既是線程對(duì)象,有又有線程任務(wù)。實(shí)現(xiàn)runnable接口,將線程任務(wù)單獨(dú)分離出來(lái)封裝成對(duì)象,類型就是Runnable接口類型。Runnable接口對(duì)線程對(duì)象和線程任務(wù)進(jìn)行解耦。
1.7 線程的匿名內(nèi)部類使用
??使用線程的內(nèi)匿名內(nèi)部類方式,可以方便的實(shí)現(xiàn)每個(gè)線程執(zhí)行不同的線程任務(wù)操作。
??1. 創(chuàng)建線程對(duì)象時(shí),直接重寫Thread類中的run方法
package com.qtw.api;
public class TestDemo{
public static void main(String[] args) {
new Thread() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...X...." + x);
}
}
}.start();
}
}
??2. 使用匿名內(nèi)部類的方式實(shí)現(xiàn)Runnable接口,重新Runnable接口中的run方法
package com.qtw.api;
public class TestDemo{
public static void main(String[] args) {
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 線程池
??線程池,其實(shí)就是一個(gè)容納多個(gè)線程的容器,其中的線程可以反復(fù)使用,省去了頻繁創(chuàng)建線程對(duì)象的操作,無(wú)需反復(fù)創(chuàng)建線程而消耗過多資源。

??我們?cè)敿?xì)的解釋一下為什么要使用線程池?
??在java中,如果每個(gè)請(qǐng)求到達(dá)就創(chuàng)建一個(gè)新線程,開銷是相當(dāng)大的。在實(shí)際使用中,創(chuàng)建和銷毀線程花費(fèi)的時(shí)間和消耗的系統(tǒng)資源都相當(dāng)大,甚至可能要比在處理實(shí)際的用戶請(qǐng)求的時(shí)間和資源要多的多。除了創(chuàng)建和銷毀線程的開銷之外,活動(dòng)的線程也需要消耗系統(tǒng)資源。如果在一個(gè)jvm里創(chuàng)建太多的線程,可能會(huì)使系統(tǒng)由于過度消耗內(nèi)存或“切換過度”而導(dǎo)致系統(tǒng)資源不足。為了防止資源不足,需要采取一些辦法來(lái)限制任何給定時(shí)刻處理的請(qǐng)求數(shù)目,盡可能減少創(chuàng)建和銷毀線程的次數(shù),特別是一些資源耗費(fèi)比較大的線程的創(chuàng)建和銷毀,盡量利用已有對(duì)象來(lái)進(jìn)行服務(wù)。
??線程池主要用來(lái)解決線程生命周期開銷問題和資源不足問題。通過對(duì)多個(gè)任務(wù)重復(fù)使用線程,線程創(chuàng)建的開銷就被分?jǐn)偟搅硕鄠€(gè)任務(wù)上了,而且由于在請(qǐng)求到達(dá)時(shí)線程已經(jīng)存在,所以消除了線程創(chuàng)建所帶來(lái)的延遲。這樣,就可以立即為請(qǐng)求服務(wù),使用應(yīng)用程序響應(yīng)更快。另外,通過適當(dā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):返回線程池對(duì)象
??ExecutorService:線程池類
Future<?> submit(Runnable task):獲取線程池中的某一個(gè)線程對(duì)象,并執(zhí)行
??Future接口:用來(lái)記錄線程任務(wù)執(zhí)行完畢后產(chǎn)生的結(jié)果。線程池創(chuàng)建與使用
使用線程池中線程對(duì)象的步驟:
??1. 創(chuàng)建線程池對(duì)象
??2. 創(chuàng)建Runnable接口子類對(duì)象
??3. 提交Runnable接口子類對(duì)象
??4. 關(guān)閉線程池
Runnable接口實(shí)現(xiàn)類
package com.qtw.api;
public class MyRunnable implements Runnable{
//實(shí)現(xiàn)run方法,同時(shí)定義線程要執(zhí)行的run方法邏輯
public void run(){
for(int i = 1; i <= 10; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
代碼演示:
package com.qtw.api;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class TestDemo{
public static void main(String[] args) {
//創(chuàng)建線程池對(duì)象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個(gè)線程對(duì)象
//創(chuàng)建Runnable實(shí)例對(duì)象
MyRunnable r = new MyRunnable();
//自己創(chuàng)建線程對(duì)象的方式
//Thread t = new Thread(r);
//t.start();// ---> 調(diào)用MyRunnable中的run()
//從線程池中獲取線程對(duì)象,然后調(diào)用MyRunnable中的run()
service.submit(r);
//再獲取個(gè)線程對(duì)象,調(diào)用MyRunnable中的run()
service.submit(r);
service.submit(r);
service.submit(r);
//注意:submit方法調(diào)用結(jié)束后,程序并不終止,
//是因?yàn)榫€程池控制了線程的關(guān)閉。將使用完的線程又歸還到了線程池中
//關(guān)閉線程池,一般不進(jìn)行次操作
//service.shutdown();
}
}
2.3 使用線程池方式—Callable接口
??Callable接口:與Runnable接口功能相似,用來(lái)指定線程的任務(wù)。其中的call()方法,用來(lái)返回線程任務(wù)執(zhí)行完畢后的結(jié)果,call方法可拋出異常。
ExecutorService:線程池類
<T> Future<T> submit(Callable<T> task)
//獲取線程池中的某一個(gè)線程對(duì)象,并執(zhí)行線程中的call()方法
Future接口:用來(lái)記錄線程任務(wù)執(zhí)行完畢后產(chǎn)生的結(jié)果。線程池創(chuàng)建與使用
使用線程池中線程對(duì)象的步驟:
??1. 創(chuàng)建線程池對(duì)象
??2. 創(chuàng)建Callable接口子類對(duì)象
??3. 提交Callable接口子類對(duì)象
??4. 關(guān)閉線程池
演示代碼
package com.qtw.api;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class TestDemo{
public static void main(String[] args) {
//創(chuàng)建線程池對(duì)象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個(gè)線程對(duì)象
//創(chuàng)建Callable對(duì)象
MyCallable c = new MyCallable();
//從線程池中獲取線程對(duì)象,然后調(diào)用MyRunnable中的run()
service.submit(c);
//再獲取個(gè)教練
service.submit(c);
service.submit(c);
//注意:submit方法調(diào)用結(jié)束后,程序并不終止,是因?yàn)榫€程池控制了線程的關(guān)閉。
// 將使用完的線程又歸還到了線程池中
//關(guān)閉線程池
//service.shutdown();
}
}
Callable接口實(shí)現(xiàn)類,call方法可拋出異常、返回線程任務(wù)執(zhí)行完畢后的結(jié)果
package com.qtw.api;
import java.util.concurrent.Callable;
public class MyCallable implements Callable {
//call的返回值類型是泛型,call()拋父類異常
public Object call() throws Exception{
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return null;
}
}
2.4 線程池練習(xí):返回兩個(gè)數(shù)相加的結(jié)果
要求:通過線程池中的線程對(duì)象,使用Callable接口完成兩個(gè)數(shù)求和操作
Future接口:用來(lái)記錄線程任務(wù)執(zhí)行完畢后產(chǎn)生的結(jié)果。線程池創(chuàng)建與使用
V get() 獲取Future對(duì)象中封裝的數(shù)據(jù)結(jié)果
代碼演示:
package com.qtw.api;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class TestDemo{
public static void main(String[] args) throws Exception{
//創(chuàng)建線程池對(duì)象
ExecutorService threadPool = Executors.newFixedThreadPool(2);
//創(chuàng)建一個(gè)Callable接口子類對(duì)象
//MyCallable c = new MyCallable();
MyCallable c = new MyCallable(100, 200);
MyCallable c2 = new MyCallable(10, 20);
//獲取線程池中的線程,調(diào)用Callable接口子類對(duì)象中的call()方法, 完成求和操作
//<Integer> Future<Integer> submit(Callable<Integer> task)
// Future 結(jié)果對(duì)象
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接口實(shí)現(xiàn)類
package com.qtw.api;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
//成員變量
private int x = 5;
private int y = 3;
//構(gòu)造方法
public MyCallable(){
}
public MyCallable(int x, int y){
this.x = x;
this.y = y;
}
public Integer call() throws Exception {
return x+y;
}
}
3 多線程安全
3.1 線程安全
??如果有多個(gè)線程在同時(shí)運(yùn)行,而這些線程可能會(huì)同時(shí)運(yùn)行這段代碼。程序每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。
??我們通過一個(gè)案例,演示線程的安全問題:
??電影院要賣票,我們模擬電影院的賣票過程。假設(shè)要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(gè)(本場(chǎng)電影只能賣100張票)。
??我們來(lái)模擬電影院的售票窗口,實(shí)現(xiàn)多個(gè)窗口同時(shí)賣 “功夫熊貓3”這場(chǎng)電影票(多個(gè)窗口一起賣這100張票)需要窗口,采用線程對(duì)象來(lái)模擬;需要票,Runnable接口子類來(lái)模擬
測(cè)試類
package com.qtw.api;
public class TestDemo{
public static void main(String[] args) {
//創(chuàng)建票對(duì)象
Ticket ticket = new Ticket();
//創(chuàng)建3個(gè)窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
模擬票
package com.qtw.api;
public class Ticket implements Runnable {
//共100票
private int ticket = 100;
public void run() {
//模擬賣票
while (true) {
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
運(yùn)行結(jié)果發(fā)現(xiàn):上面程序出現(xiàn)了問題
??票出現(xiàn)了重復(fù)的票
??錯(cuò)誤的票 0、-1
??其實(shí),線程安全問題都是由全局變量及靜態(tài)變量引起的。若每個(gè)線程中對(duì)全局變量、靜態(tài)變量只有讀操作,而無(wú)寫操作,一般來(lái)說(shuō),這個(gè)全局變量是線程安全的;若有多個(gè)線程同時(shí)執(zhí)行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
3.2 線程同步(線程安全處理Synchronized)
java中提供了線程同步機(jī)制,它能夠解決上述的線程安全問題。線程同步的方式有兩種:
??方式1:同步代碼塊
??方式2:同步方法
3.2.1 同步代碼塊
同步代碼塊: 在代碼塊聲明上 加上synchronized
synchronized (鎖對(duì)象) {
可能會(huì)產(chǎn)生線程安全問題的代碼
}
??同步代碼塊中的鎖對(duì)象可以是任意的對(duì)象;但多個(gè)線程時(shí),要使用同一個(gè)鎖對(duì)象才能夠保證線程安全。
??使用同步代碼塊,對(duì)電影院賣票案例中Ticket類進(jìn)行如下代碼修改:
package com.qtw.api;
public class Ticket implements Runnable {
//共100票
private int ticket = 100;
Object lock = new Object();
public void run() {
//模擬賣票
while (true) {
synchronized (lock){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
}
當(dāng)使用了同步代碼塊后,上述的線程的安全問題,解決了。
3.2.2 同步方法
同步方法:在方法聲明上加上synchronized
public synchronized void method(){
可能會(huì)產(chǎn)生線程安全問題的代碼
}
同步方法中的鎖對(duì)象是 this
使用同步方法,對(duì)電影院賣票案例中Ticket類進(jìn)行如下代碼修改:
package com.qtw.api;
public class Ticket implements Runnable {
//共100票
private int ticket = 100;
public void run() {
//模擬賣票
while (true) {
if (ticket > 0) {
//模擬選坐的操作
method();
}
}
}
//同步方法,鎖對(duì)象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(){
可能會(huì)產(chǎn)生線程安全問題的代碼
}
靜態(tài)同步方法中的鎖對(duì)象是 類名.class
3.3 死鎖
??同步鎖使用的弊端:當(dāng)線程任務(wù)中出現(xiàn)了多個(gè)同步(多個(gè)鎖)時(shí),如果同步中嵌套了其他的同步。這時(shí)容易引發(fā)一種現(xiàn)象:程序出現(xiàn)無(wú)限等待,這種現(xiàn)象我們稱為死鎖。這種情況能避免就避免掉。
3.4 Lock接口
??查閱API,查閱Lock接口描述,Lock 實(shí)現(xiàn)提供了比使用 synchronized 方法和語(yǔ)句可獲得的更廣泛的鎖定操作。推薦使用
??Lock接口中的常用方法

??Lock提供了一個(gè)更加面對(duì)對(duì)象的鎖,在該鎖中提供了更多的操作鎖的功能。
??我們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對(duì)電影院賣票案例中Ticket類進(jìn)行如下代碼修改:
package com.qtw.api;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//創(chuàng)建Lock鎖對(duì)象
Lock ck = new ReentrantLock();
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();
//}
}
}
}
3.5 等待喚醒機(jī)制
??在開始講解等待喚醒機(jī)制之前,有必要搞清一個(gè)概念——線程之間的通信:多個(gè)線程在處理同一個(gè)資源,但是處理的動(dòng)作(線程的任務(wù))卻不相同。通過一定的手段使各個(gè)線程能有效的利用資源。而這種手段即—— 等待喚醒機(jī)制。
等待喚醒機(jī)制所涉及到的方法:
??wait():等待,將正在執(zhí)行的線程釋放其執(zhí)行資格 和 執(zhí)行權(quán),并存儲(chǔ)到線程池中。
??notify():?jiǎn)拘?,喚醒線程池中被wait()的線程,一次喚醒一個(gè),而且是任意的。
??notifyAll(): 喚醒全部:可以將線程池中的所有wait() 線程都喚醒。
??其實(shí),所謂喚醒的意思就是讓 線程池中的線程具備執(zhí)行資格。必須注意的是,這些方法都是在 同步中才有效。同時(shí)這些方法在使用時(shí)必須標(biāo)明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個(gè)鎖上的線程。
??仔細(xì)查看JavaAPI之后,發(fā)現(xiàn)這些方法 并不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,為什么這些操作線程的方法定義在Object類中?
??因?yàn)檫@些方法在使用時(shí),必須要標(biāo)明所屬的鎖,而鎖又可以是任意對(duì)象。能被任意對(duì)象調(diào)用的方法一定定義在Object類中。

接下里,我們先從一個(gè)簡(jiǎn)單的示例入手:

如上圖說(shuō)示,輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務(wù)是:
1.當(dāng)input發(fā)現(xiàn)Resource中沒有數(shù)據(jù)時(shí),開始輸入,輸入完成后,叫output來(lái)輸出。如果發(fā)現(xiàn)有數(shù)據(jù),就wait();
2.當(dāng)output發(fā)現(xiàn)Resource中沒有數(shù)據(jù)時(shí),就wait() ;當(dāng)發(fā)現(xiàn)有數(shù)據(jù)時(shí),就輸出,然后,叫醒input來(lái)輸入數(shù)據(jù)。
下面代碼,模擬等待喚醒機(jī)制的實(shí)現(xiàn):
模擬資源類
package com.qtw.api;
public class Resource {
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name, String sex) {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 設(shè)置成員變量
this.name = name;
this.sex = sex;
// 設(shè)置之后,Resource中有值,將標(biāo)記該為 true ,
flag = true;
// 喚醒output
this.notify();
}
public synchronized void out() {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 輸出線程將數(shù)據(jù)輸出
System.out.println("姓名: " + name + ",性別: " + sex);
// 改變標(biāo)記,以便輸入線程輸入數(shù)據(jù)
flag = false;
// 喚醒input,進(jìn)行數(shù)據(jù)輸入
this.notify();
}
}
輸入線程任務(wù)類
package com.qtw.api;
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
public void run() {
int count = 0;
while (true) {
if (count == 0) {
r.set("小明", "M");
} else {
r.set("小花", "F");
}
// 在兩個(gè)數(shù)據(jù)之間進(jìn)行切換
count = (count + 1) % 2;
}
}
}
輸出線程任務(wù)類
package com.qtw.api;
public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
public void run() {
while (true) {
r.out();
}
}
}
測(cè)試類
package com.qtw.api;
public class TestDemo {
public static void main(String[] args) {
// 資源對(duì)象
Resource r = new Resource();
// 任務(wù)對(duì)象
Input in = new Input(r);
Output out = new Output(r);
// 線程對(duì)象
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
// 開啟線程
t1.start();
t2.start();
}
}