一,線程基礎(chǔ)
1,基礎(chǔ)概念
????一個(gè)線程就是運(yùn)行在一個(gè)進(jìn)程上下文中的一個(gè)邏輯流,而進(jìn)程是程序執(zhí)行的實(shí)例。系統(tǒng)中每個(gè)運(yùn)行著的程序都運(yùn)行在一個(gè)進(jìn)程上下文環(huán)境中,進(jìn)程上下文由程序正確運(yùn)行所必須的狀態(tài)組成,包括程序代碼,數(shù)據(jù),程序運(yùn)行棧,寄存器,指令計(jì)數(shù)器,環(huán)境變量以及進(jìn)程打開的文件描述符集合,這些都保存在進(jìn)程控制塊中。
????現(xiàn)代操作系統(tǒng)調(diào)度的最小單位是線程,也叫輕量級(jí)進(jìn)程,在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,每個(gè)線程也有自己的運(yùn)行上下文環(huán)境,包括唯一的線程ID,??臻g,程序計(jì)數(shù)器等。多個(gè)線程運(yùn)行在同一個(gè)進(jìn)程環(huán)境中,因此共享進(jìn)程環(huán)境中的堆,代碼,共享庫和打開的文件描述符。
????Java是天生的多線程程序,main() 方法由一個(gè)被稱為主線程的線程調(diào)用,之后在 main() 方法中可以再生出更多的自定義線程。
2,線程啟動(dòng)和暫停
????Java線程啟動(dòng)有兩種方式,一種是通過繼承 Thread 類,一種是通過實(shí)現(xiàn) Runnable 接口,示例代碼如下:
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* @author koma <komazhang@foxmail.com>
* @date 2018-10-09 17:21
*/
public class StartThreadDemo {
public static void main(String[] args) {
StartThreadDemo startThreadDemo = new StartThreadDemo();
startThreadDemo.testThread();
}
public void testThread() {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
threadDemo1.start();
ThreadDemo2 threadDemo2 = new ThreadDemo2();
new Thread(threadDemo2).start();
}
class ThreadDemo1 extends Thread {
@Override
public void run() {
System.out.printf("%s run...\n", Thread.currentThread().getName());
}
}
class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.printf("%s run...\n", Thread.currentThread().getName());
}
}
}
????通過示例代碼可以得知,由于同一個(gè)線程不能啟動(dòng)多次(即調(diào)用多次 start() 方法),繼承 Thread 類實(shí)現(xiàn)的線程中的 run() 方法如果要在多個(gè)線程中執(zhí)行,則需要 new 多次 ThreadDemo1,而對(duì)于實(shí)現(xiàn) Runnable 接口類的 ThreadDemo2 則只需要 new 一次即可。這兩種方法各有適用場(chǎng)景,需靈活運(yùn)用。
????Java多線程編程后期較為常用的方式是使用 Executors 框架,利用該框架啟動(dòng)線程的實(shí)例代碼如下(這會(huì)比較常見):
ThreadFactory threadFactory = Executors.defaultThreadFactory();
threadFactory.newThread(threadDemo2).start();
????這里重用了 ThreadDemo2 的實(shí)例,同時(shí)框架內(nèi)部使用了線程池技術(shù),這個(gè)后續(xù)再討論。
????線程暫停最基本的方法是通過 Thread 類的 sleep 方法,這會(huì)讓線程進(jìn)入到休眠狀態(tài)等待一段時(shí)間再運(yùn)行,線程卻不會(huì)退出。讓線程退出的方法一種是通過中斷,另外一種則是設(shè)置標(biāo)識(shí)位,這種方法相比是比較優(yōu)雅的一種方式,因?yàn)檫@給了線程充分的時(shí)間去執(zhí)行現(xiàn)場(chǎng)清理工作,從容退出。三種方式的舉例代碼如下:
/**
* @author koma <komazhang@foxmail.com>
* @date 2018-10-09 17:21
*/
public class StartThreadDemo {
public static void main(String[] args) {
StartThreadDemo startThreadDemo = new StartThreadDemo();
startThreadDemo.testThread();
}
public void testThread() {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
ThreadDemo2 threadDemo2 = new ThreadDemo2();
ThreadDemo3 threadDemo3 = new ThreadDemo3();
threadDemo1.start();
threadDemo2.start();
threadDemo3.start();
//讓線程充分運(yùn)行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
threadDemo1.interrupt();
threadDemo2.interrupt();
threadDemo3.exit();
//等待線程終止
try {
threadDemo2.join();
System.out.println("threadDemo2 exit");
threadDemo3.join();
System.out.println("threadDemo3 exit");
threadDemo1.join();
System.out.println("threadDemo1 exit");
} catch (InterruptedException e) {
}
}
class ThreadDemo1 extends Thread {
@Override
public void run() {
while (true) {
System.out.printf("%s run...\n", Thread.currentThread().getName());
try {
Thread.sleep(5000); //線程暫停3秒之后繼續(xù)運(yùn)行
} catch (InterruptedException e) {
}
}
}
}
class ThreadDemo2 extends Thread {
@Override
public void run() {
while (true) {
System.out.printf("%s run...\n", Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
//線程在暫停中收到中斷信號(hào),做出反應(yīng)并退出
System.out.printf("%s will exit...\n", Thread.currentThread().getName());
break;
}
}
}
}
class ThreadDemo3 extends Thread {
private boolean flag = false;
@Override
public void run() {
while (true) {
System.out.printf("%s run...\n", Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
if (flag) {
//線程運(yùn)行中判斷標(biāo)記位,當(dāng)標(biāo)記位被設(shè)置之后執(zhí)行清理工作并退出
System.out.printf("%s will exit...\n", Thread.currentThread().getName());
break;
}
}
}
public void exit() {
this.flag = true;
}
}
}
????運(yùn)行實(shí)例代碼觀察輸出可以知道,threadDemo2 因?yàn)槭盏街袛嘈盘?hào)而退出,threadDemo3 因?yàn)闃?biāo)志位被設(shè)置而退出,只有 threadDemo1 在一直運(yùn)行。
????觀察代碼運(yùn)行還可以發(fā)現(xiàn),調(diào)用線程的 interrupt() 方法會(huì)打斷 sleep 過程,即該方法可以使線程的 sleep 方法立即拋出一個(gè) InterruptedException 異常,而不去關(guān)心 sleep 時(shí)間是否到期。
3,線程互斥
????多線程程序中的各個(gè)線程的運(yùn)行時(shí)機(jī)是由操作系統(tǒng)調(diào)度確定的,而不能進(jìn)行人工干預(yù),因此當(dāng)多個(gè)線程操作同一個(gè)堆實(shí)例時(shí)由于運(yùn)行時(shí)機(jī)的不確定性導(dǎo)致運(yùn)行結(jié)果不可預(yù)測(cè),這在某些情況下會(huì)引發(fā)程序錯(cuò)誤。
????這種由于多個(gè)線程同時(shí)操作而引起錯(cuò)誤的情況稱為數(shù)據(jù)競(jìng)爭(zhēng)或競(jìng)態(tài)條件,這種情況下就需要進(jìn)行線程互斥處理。在 Java 中最簡(jiǎn)單的互斥操作是通過 synchronized 關(guān)鍵字。
import java.util.Random;
/**
* @author koma <komazhang@foxmail.com>
* @date 2018-10-09 23:26
*/
public class SyncThreadDemo {
public static void main(String[] args) {
SyncThreadDemo syncThreadDemo = new SyncThreadDemo();
syncThreadDemo.test();
}
public void test() {
Thread1 thread1 = new Thread1(new Data());
//啟動(dòng)四個(gè)線程
new Thread(thread1).start();
new Thread(thread1).start();
new Thread(thread1).start();
new Thread(thread1).start();
}
class Thread1 implements Runnable {
private Data data;
private Random random = new Random();
public Thread1(Data data) {
this.data = data;
}
@Override
public void run() {
while (true) {
this.changeData();
}
}
private void changeData() {
int count = this.data.getCount();
//睡眠隨機(jī)時(shí)間,模擬線程被搶占
try {
Thread.sleep(this.random.nextInt(500));
} catch (InterruptedException e) {
}
this.data.setCount(count+1);
System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
}
}
class Data {
private int count = 0;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
}
????上面代碼示例中,changeData() 這個(gè)方法在沒有做線程互斥時(shí),打印的 count 值變化混亂,沒有按預(yù)期多線程自增。當(dāng)給其增加線程互斥之后才能實(shí)現(xiàn)預(yù)期效果,如下:
private synchronized void changeData() {
int count = this.data.getCount();
//睡眠隨機(jī)時(shí)間,模擬線程被搶占
try {
Thread.sleep(this.random.nextInt(500));
} catch (InterruptedException e) {
}
this.data.setCount(count+1);
System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
}
????synchronized 關(guān)鍵字實(shí)現(xiàn)的原理是在執(zhí)行其包含的方法前,線程會(huì)先去嘗試獲得一把鎖,只有成功獲得鎖的線程才能執(zhí)行方法,而沒有獲得鎖的線程則會(huì)等待,直到方法被執(zhí)行完成返回之后鎖被釋放,其它線程才能再去競(jìng)爭(zhēng)鎖,這樣就保證了方法每次只運(yùn)行一個(gè)線程執(zhí)行,實(shí)際上在這里把并行的邏輯串行化了。
????上述示例代碼中的 run() 方法還可以寫成下面這樣:
@Override
public void run() {
while (true) {
synchronized (this) { //同步代碼塊
int count = this.data.getCount();
//睡眠隨機(jī)時(shí)間,模擬線程被搶占
try {
Thread.sleep(this.random.nextInt(500));
} catch (InterruptedException e) {
}
this.data.setCount(count+1);
System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
}
}
}
????兩種寫法說明,synchronized 關(guān)鍵字加在方法聲明上實(shí)際上持有的鎖是 this 對(duì)象的鎖。但是當(dāng)方法同時(shí)聲明為 static 時(shí), synchronized 持有的鎖就變成了類的鎖,這和 this 對(duì)象的鎖存在明顯差異。因?yàn)?this 對(duì)象的鎖是類實(shí)例的鎖,那么類實(shí)例化一次就會(huì)有一把鎖,而類始終只有一個(gè),因此類的鎖總是只有一把。
????把上述實(shí)例代碼中的 test() 方法和 run() 方法改寫,如下:
public void test() {
Data data = new Data();
new Thread(new Thread1(data)).start();
new Thread(new Thread1(data)).start();
new Thread(new Thread1(data)).start();
new Thread(new Thread1(data)).start();
}
@Override
public void run() {
while (true) {
synchronized (Thread1.class) { //使用類的鎖
//synchronized (this) { //使用類實(shí)例鎖
int count = this.data.getCount();
//睡眠隨機(jī)時(shí)間,模擬線程被搶占
try {
Thread.sleep(this.random.nextInt(500));
} catch (InterruptedException e) {
}
this.data.setCount(count+1);
System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
}
}
}
????這里在 synchronized 代碼塊中使用類的鎖和在 static 方法上加上 synchronized 關(guān)鍵字聲明意義一樣,因此這里使用代碼塊方式說明。如上代碼描述中所示,在 synchronized 代碼塊上如果繼續(xù)使用 this 鎖,則依然無法達(dá)到預(yù)期效果,根本原因是現(xiàn)在每個(gè)線程都有一個(gè)類實(shí)例,導(dǎo)致每個(gè)線程中的 this 鎖是獨(dú)立的,而使用類的鎖時(shí),則代碼會(huì)如預(yù)期運(yùn)行,原因就是類的鎖只有一把,和類實(shí)例個(gè)數(shù)無關(guān)。
4,線程協(xié)作
????所謂線程間協(xié)作,一種是上面說的線程之間在某一刻要互斥的順序運(yùn)行,一種則是類似于生產(chǎn)者-消費(fèi)者模式,線程之間合作完成任務(wù),當(dāng)任務(wù)狀態(tài)滿足或者不滿足時(shí)需要線程之間相互通知。這種機(jī)制就是通知/等待機(jī)制,使用 java Object 對(duì)象的 wait(),notify(),notifyAll() 方法來實(shí)現(xiàn),下面的代碼示例使用該機(jī)制實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的生產(chǎn)者-消費(fèi)者模式。
/**
* @author koma <komazhang@foxmail.com>
* @date 2018-10-10 00:35
*/
public class CooperationThread {
public static void main(String[] args) {
CooperationThread cooperationThread = new CooperationThread();
cooperationThread.test();
}
public void test() {
Product product = new Product();
new Thread(new Consumer(product)).start();
new Thread(new Consumer(product)).start();
//讓 Consumer 充分運(yùn)行
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
new Thread(new Producer(product)).start();
}
class Product {
private int count = 0;
public void produce() {
count++;
System.out.println("produce: "+count);
}
public void consume() {
count--;
System.out.println("consume: "+count);
}
public boolean canConsume() {
return this.count > 0;
}
public boolean canproduce() {
return this.count == 0;
}
}
class Producer implements Runnable {
private Product product;
public Producer(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
synchronized (this.product) {
while (!this.product.canproduce()) {
try {
this.product.wait();
} catch (InterruptedException e) {
}
}
this.product.produce();
this.product.notifyAll();
}
}
}
}
class Consumer implements Runnable {
private Product product;
public Consumer(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
synchronized (this.product) {
while (!this.product.canConsume()) {
try {
this.product.wait();
} catch (InterruptedException e) {
}
}
this.product.consume();
this.product.notifyAll();
}
}
}
}
}
????wait() 方法是讓當(dāng)前線程進(jìn)入到調(diào)用 wait() 方法的對(duì)象的等待隊(duì)列中,而 notify(),notifyAll() 方法則是從調(diào)用對(duì)象的等待隊(duì)列中喚醒一個(gè)或全部線程。規(guī)定調(diào)用 wait(),notify(),notifyAll() 前需要先獲取調(diào)用對(duì)象的鎖,同時(shí)在調(diào)用 wait() 方法之后,剛剛獲取到的對(duì)象的鎖會(huì)被釋放,以便其它線程有機(jī)會(huì)去競(jìng)爭(zhēng)鎖,而在調(diào)用 notify(),notifyAll() 方法之后則不會(huì)主動(dòng)釋放鎖,因?yàn)榭赡茉谶@之后當(dāng)前線程還有別的工作需要做完才能釋放鎖。
????由于在調(diào)用 wait() 之后線程會(huì)阻塞在當(dāng)前位置,當(dāng)調(diào)用 notify 之后線程會(huì)從當(dāng)前位置繼續(xù)往下執(zhí)行,但是由于這時(shí)有可能 product 的狀態(tài)恰好又被其它線程改變,那么當(dāng)前線程繼續(xù)往下執(zhí)行就會(huì)產(chǎn)生意外的情況,因此我們通常的調(diào)用 wait() 的方法是放到一個(gè) while 循環(huán)中,像下面這樣:
while (!this.product.canConsume()) {
try {
this.product.wait();
} catch (InterruptedException e) {
}
}
????這種方法會(huì)使得我們的代碼更加健壯。另外一個(gè)會(huì)使代碼更加健壯的做法是盡量使用 notifyAll() 而不是 notify(),因?yàn)檎{(diào)用 notify() 方法只喚醒等待隊(duì)列中的一個(gè)線程,那么對(duì)于等待隊(duì)列中既有消費(fèi)者,又有生產(chǎn)者時(shí),那么當(dāng)消費(fèi)者線程調(diào)用 notify() 有可能會(huì)還是喚醒消費(fèi)者線程,如果這種情況的概率較大,則程序便會(huì)停止但是不報(bào)錯(cuò)。
5,線程狀態(tài)轉(zhuǎn)換
????線程包括 NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 這幾種狀態(tài),可以通過 Thread 的 getState() 方法獲取到。線程在整個(gè)生命周期中經(jīng)歷的狀態(tài)轉(zhuǎn)換都可以包括到這張圖中。
二,多線程程序的評(píng)價(jià)標(biāo)準(zhǔn)
1,安全性
????安全性是指不損壞對(duì)象,即對(duì)象的狀態(tài)或值一定要復(fù)合預(yù)期設(shè)計(jì)。當(dāng)一個(gè)類被多線程調(diào)用時(shí),如果也能保證對(duì)象的安全性,則該類稱為線程安全類,否則稱為線程不安全類。
2,生存性
????生存性是指在任何時(shí)刻,程序的必要處理一定能夠完成,這也是程序正常運(yùn)行的必要條件,也稱為程序的活性。常見的場(chǎng)景是程序運(yùn)行存在死鎖或活鎖,導(dǎo)致程序不能夠正常運(yùn)行,這就違反了線程的生存性。
3,可復(fù)用性
????可復(fù)用性是指類能夠重復(fù)利用,主要目標(biāo)是提高程序的質(zhì)量。
4,性能
????性能是指程序能夠快速的,大批量的執(zhí)行處理,主要目標(biāo)是提高程序的質(zhì)量。性能的主要指標(biāo)包括:吞吐量 - 單位時(shí)間內(nèi)完成的處理數(shù)量,越大表示性能越高;響應(yīng)性 - 指從發(fā)出請(qǐng)求到收到請(qǐng)求響應(yīng)的時(shí)間間隔,越短表示性能越高;容量 - 是指可同時(shí)進(jìn)行的處理數(shù)量,越多表示性能越高;