特別說明:文章內(nèi)容是《Java并發(fā)編程的藝術》讀書筆記
Java是一種多線程語言,從誕生開始就內(nèi)置了對多線程的支持。正確地使用多線程可以顯著提高程序性能,但過多地創(chuàng)建線程和對線程的不當管理也很容易造成問題。
線程簡介
線程定義
現(xiàn)代操作系統(tǒng)在運行一個程序時,會為其創(chuàng)建一個進程。例如,啟動一個Java程序,操作系統(tǒng)就會創(chuàng)建一個Java進程。線程是現(xiàn)代操作系統(tǒng)調(diào)度的最小單元,也叫輕量級進程,在一個進程里可以創(chuàng)建多個線程,這些線程都擁有各自的計算器、堆棧和局部變量等屬性,并且能夠訪問共享的內(nèi)存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執(zhí)行。
Java程序天生就是多線程程序,可以通過JMX查看一個普通的Java程序包含那些線程,代碼如下:
public class MutilThread {
public static void main(String[] args) {
// 獲取Java線程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 獲取線程和線程堆棧信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
// 遍歷線程線程,僅打印線程ID和線程名稱信息
for(ThreadInfo threadInfo:threadInfos){
System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());
}
}
}
運行結果如下:
使用多線程的原因
正確使用多線程,總是能夠給開發(fā)人員帶來顯著的好處,而使用多線程的原因主要有以下幾點:
1、更多的處理器核心
隨著處理器上的核心數(shù)量越來越多,以及超線程技術的廣泛運用,現(xiàn)在大多數(shù)計算機都比以往更加擅長并行計算,而處理器性能的提升方式,也從更高的主頻向更多的核心發(fā)展。
2、更快的響應時間
有時我們會編寫一些業(yè)務邏輯比較復雜的代碼,例如,一筆訂單的創(chuàng)建,它包括插入訂單數(shù)據(jù)、生成訂單快照、發(fā)送郵件通知賣家和記錄貨品銷售數(shù)量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結果。但是這么多業(yè)務操作,如何能夠讓其更快地完成呢?
在上面的場景中,可以使用多線程技術,即將數(shù)據(jù)一致性不強的操作派發(fā)給其他線程處理(也可以使用消息隊列),如生成訂單快照、發(fā)送郵件等。這樣做的好處是響應用戶請求的線程能夠盡可能快地處理完成,縮短了響應時間,提升了用戶體驗。
3、 更好的編程模型
Java為多線程編程提供了一致的編程模型,使開發(fā)人員能夠更加專注于問題的解決,即為所遇到的問題建立合適的模型,而不是絞盡腦汁地考慮如何將其多線程化。
線程優(yōu)先級
現(xiàn)代操作系統(tǒng)基本采用時分的形式調(diào)度運行的線程,操作系統(tǒng)會分出一個個時間片,線程會分配到若干時間片,當線程的時間片用完了就會發(fā)生線程調(diào)度,并等待著下次分配。線程分配到的時間片多少也就決定了線程使用處理器資源的多少,而線程優(yōu)先級就是決定線程需要多或者少分配一些處理器資源的線程屬性。
在Java線程中,通過一個整型成員變量priority來控制優(yōu)先級,優(yōu)先級的范圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優(yōu)先級,默認優(yōu)先級是5,優(yōu)先級高的線程分配時間片的數(shù)量要多于優(yōu)先級低的線程。
設置線程優(yōu)先級時,針對頻繁阻塞(休眠或者I/O操作)的線程需要設置較高優(yōu)先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較低的優(yōu)先級,確保處理器不會被獨占。
注意:線程優(yōu)先級不能作為程序正確性的依賴,因為操作系統(tǒng)可以完全不用理會Java線程對于優(yōu)先級的設定。
線程的狀態(tài)
Java線程在運行的生命周期中可能處于下表所示的6種不同的狀態(tài),在給定的一個時刻,線程只能處于其中的一個狀態(tài)。
狀態(tài)名稱 | 說明 |
---|---|
NEW | 初始狀態(tài),線程被構建,但是還沒有調(diào)用start()方法 |
RUNNABLE | 運行狀態(tài),Java線程將操作系統(tǒng)中的就緒和運行兩種狀態(tài)籠統(tǒng)地稱作“運行中” |
BLOCKED | 阻塞狀態(tài),表示線程阻塞于鎖 |
WAITING | 等待狀態(tài),表示線程進入等待狀態(tài),進入該狀態(tài)表示當前線程需要等待其他線程做出一些特定動作(通知或中斷) |
TIME_WAITING | 超時等待狀態(tài),該狀態(tài)不同于WAITING,它是可以在指定的時間自行返回的 |
TERMINATED | 終止狀態(tài),表示當前線程已經(jīng)執(zhí)行完畢 |
線程在自身的生命周期中,并不是固定地處于某個狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進行切換,Java線程狀態(tài)變遷如下圖:
Java將操作系統(tǒng)中的運行和就緒兩個狀態(tài)合并稱為運行狀態(tài)。阻塞狀態(tài)是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態(tài),但是阻塞在java.concurrent包中Lock接口的線程狀態(tài)卻是等待狀態(tài),因為java.concurrent包中Lock接口對于阻塞的實現(xiàn)均使用了LockSupport類中的相關方法。
Daemon線程
Daemon線程是一種支持型線程,因為它主要被用作程序中后臺調(diào)度以及支持性工作。當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。可以通過調(diào)用Thread.setDaemon(true)將線程設置為Daemon線程。Daemon屬性需要在啟動線程之前設置,不能在啟動線程之后設置。
在構建Daemon線程時,不能依靠finally塊中的內(nèi)容來確保執(zhí)行關閉或清理資源的邏輯。如下代碼:
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(new DeamonRunner(),"DeamonRunner");
thread.setDaemon(true);
thread.start();
}
static class DeamonRunner implements Runnable{
@Override
public void run() {
try {
Thread.sleep(2000l);
} catch (InterruptedException e) {
//
}finally {
System.out.println("DeamonThread finally run.");
}
}
}
}
運行Deamon程序,可以看到在終端或者命令提示符沒有任何輸出。
啟動線程
在運行線程之前首先要構造一個線程對象,線程對象在構造的時候需要提供線程所需要的屬性,如線程所屬的線程組、線程優(yōu)先級、是否是Daemon線程等信息。
private void init(ThreadGroup g, Runnable target, String name,long stackSize,AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
// 當前線程就是該線程的父線程
Thread parent = currentThread();
this.group = g;
// 將daemon、priority屬性設置為父線程的對應屬性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.name = name.toCharArray();
this.target = target;
setPriority(priority);
// 將父線程的InheritableThreadLocal復制過來
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 分配一個線程ID
tid = nextThreadID();
}
在上述過程中,一個新構造的線程對象是由其parent線程來進行空間分配的,而child線程繼承了parent是否為Deamon、優(yōu)先級和加載資源的ContextClassLoader以及可繼承的ThreadLocal,同時還會分配一個唯一的ID來標識這個child線程。
線程對象在初始化完成之后,調(diào)用start()方法就可以啟動這個線程。線程start()方法的含義是:當前線程(即parent線程)同步告知Java虛擬機,只要線程規(guī)劃器空閑,應立即啟動調(diào)用start()方法的線程。
啟動一個線程前,最好為這個線程設置線程名稱,因為這樣在使用jstack分析程序或者進行問題排查時,就會給開發(fā)人員提供一些提示,自定義的線程最好能夠起個名字。
理解中斷
中斷可以理解為線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程通過調(diào)用該線程的interrupt()方法對其進行中斷操作。
線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調(diào)用靜態(tài)方法Thread.interrupted()對當前線程的中斷標識位進行復位。如果該線程已經(jīng)處于終結狀態(tài),即使該線程被中斷過,在調(diào)用該線程對象的isInterrupted()時依舊會返回false。
從Java的API中可以看到,許多聲明拋出InterruptedException的方法(例如Thread.sleep(longmillis)方法)這些方法在拋出InterruptedException之前,Java虛擬機會先將該線程的中斷標識位清除,然后拋出InterruptedException,此時調(diào)用isInterrupted()方法將會返回false。
過期的suspend()、resume()和stop()
suspend()、resume()和stop()方法完成了線程的暫停、恢復和終止工作,而且非常“人性化”。但是這些API是過期的,也就是不建議使用的。
不建議使用的原因主要有:以suspend()方法為例,在調(diào)用后,線程不會釋放已經(jīng)占有的資源(比如鎖),而是占有著資源進入睡眠狀態(tài),這樣容易引發(fā)死鎖問題。同樣,stop()方法在終結一個線程時不保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態(tài)下。
因為suspend()、resume()和stop()方法帶來的副作用,這些方法才被標注為不建議使用的過期方法,而暫停和恢復操作可以用等待/通知機制來替代。
安全地終止線程
中斷操作是一種簡便的線程間交互方式,而這種交互方式最適合用來取消或停止任務。除了中斷以外,還可以利用一個boolean變量來控制是否需要停止任務并終止該線程。
public class Shutdown {
public static void main(String[] args) throws Exception {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒,main線程對CountThread進行中斷,使CountThread能夠感知中斷而結束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main線程對Runner two進行取消,使CountThread能夠感知on為false而結束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
main線程通過中斷操作和cancel()方法均可使CountThread得以終止。這種通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷地將線程停止,因此這種終止線程的做法顯得更加安全和優(yōu)雅。
線程間通信
線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執(zhí)行,直到終止。但是,每個運行中的線程,如果僅僅是孤立地運行,那么沒有一點兒價值,或者說價值很少,如果多個線程能夠相互配合完成工作,這將會帶來巨大的價值。
volatile和synchronized關鍵字
Java支持多個線程同時訪問一個對象或者對象的成員變量,由于每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量分配的內(nèi)存是在共享內(nèi)存中的,但是每個執(zhí)行的線程還是可以擁有一份拷貝,這樣做的目的是加速程序的執(zhí)行,這是現(xiàn)代多核處理器的一個顯著特性),所以程序在執(zhí)行過程中,一個線程看到的變量并不一定是最新的。
關鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內(nèi)存中獲取,而對它的改變必須同步刷新回共享內(nèi)存,它能保證所有線程對變量訪問的可見性。
關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處于方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性。
通過使用javap工具查看生成的class文件信息來分析synchronized關鍵字的實現(xiàn)細節(jié),代碼如下
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){
m();
}
}
public static synchronized void m(){
}
}
執(zhí)行javap -v Synchronized.class,部分相關輸出如下所示:
對于同步塊的實現(xiàn)使用了monitorenter和monitorexit指令,而同步方法則是依賴方法修飾符上的ACC_SYNCHRONIZED來完成。無論采用哪種方式,其本質(zhì)是對一個對象的監(jiān)視器進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監(jiān)視器。
任意一個對象都擁有自己的監(jiān)視器,當這個對象由同步塊或者這個對象的同步方法調(diào)用時,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進入同步塊或者同步方法,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態(tài)。
等待/通知機制
等待/通知機制是指一個線程A調(diào)用了對象O的wait()方法進入等待狀態(tài),而另一個線程B調(diào)用了對象O的notify()或notifyAll()方法,線程A收到通知后從對象O的wait()方法返回,進而執(zhí)行后續(xù)操作。上述兩個線程對象O來完成交互,而對象上的wait()和notify/notifyAll()的關系就如同開關信號一樣,用來完成等待方通知方之間的交互工作。
等待/通知的相關方法是任意Java對象都具備的,這些方法被定義在所有對象的超類java.lang.Object上。
1、實現(xiàn)生產(chǎn)者-消費者模型,代碼如下:
public class WaitNotify {
private final static int CONTAINER_MAX_LENGTH = 3;
private static Queue<Integer> resources = new LinkedList<Integer>();
//作為synchronized的對象監(jiān)視器
private static final Object lock = new Object();
/**
* 消息者
*/
static class Consumer implements Runnable {
@Override
public void run() {
synchronized (lock) {
// 不能使用if判斷,防止過早喚醒
while (resources.isEmpty()) {
try {
// 當前釋放鎖,線程進入等待狀態(tài)。
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " get number is " + resources.remove());
// 喚醒所有等待狀態(tài)的線程
lock.notifyAll();
}
}
}
/**
* 生產(chǎn)者
*/
static class Producer implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (resources.size() == CONTAINER_MAX_LENGTH) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int number = (int) (Math.random() * 100);
System.out.println(Thread.currentThread().getName() + " produce number is " + number);
resources.add(number);
lock.notifyAll();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(new Consumer(), "consumer-" + i).start();
}
for (int i = 0; i < 50; i++) {
new Thread(new Producer(), "producer-" + i).start();
}
}
}
調(diào)用wait()、notify()以及notifyAll()時需要注意的細節(jié),如下:
使用wait()、notify()和notifyAll()時需要先對調(diào)用對象加鎖。
調(diào)用wait()方法后,線程狀態(tài)由RUNNING變?yōu)閃AITING,并將當前線程放置到對象的等待隊列。
notify()或notifyAll()方法調(diào)用后,等待線程依舊不會從wait()返回,需要調(diào)用notify()或notifAll()的線程釋放鎖之后,等待線程才有機會從wait()返回。
notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態(tài)由WAITING變?yōu)锽LOCKED。
從wait()方法返回的前提是獲得了調(diào)用對象的鎖。
2、面試題:設計一個程序,啟動三個線程A,B,C,各個線程只打印特定的字母,各打印10次,例如A線程只打印‘A’。要求在控制臺依次顯示“ABCABC…”
public class WaitNotify02 {
public static void main(String[] args) {
Print print = new Print(15);
new Thread(print, "A").start();
new Thread(print, "B").start();
new Thread(print, "C").start();
}
private final static Object lock = new Object();
static class Print implements Runnable {
private int max_print;
private int count = 0;
private String str = "A";
public Print(int max_print) {
this.max_print = max_print;
}
@Override
public void run() {
synchronized (lock) {
String name = Thread.currentThread().getName();
while (count < max_print) {
if (str.equals(name)) {
System.out.print(name);
if (str.equals("A")) {
str = "B";
} else if (str.equals("B")) {
str = "C";
} else {
count++;
str = "A";
}
lock.notifyAll();
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}