特別說明:文章內容是《Java併發編程的藝術》讀書筆記java
Java是一種多線程語言,從誕生開始就內置了對多線程的支持。正確地使用多線程能夠顯著提升程序性能,但過多地建立線程和對線程的不當管理也很容易形成問題。面試
現代操做系統在運行一個程序時,會爲其建立一個進程。例如,啓動一個Java程序,操做系統就會建立一個Java進程。線程是現代操做系統調度的最小單元,也叫輕量級進程,在一個進程裏能夠建立多個線程,這些線程都擁有各自的計算器、堆棧和局部變量等屬性,而且可以訪問共享的內存變量。處理器在這些線程上高速切換,讓使用者感受到這些線程在同時執行。編程
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());
}
}
}
複製代碼
運行結果以下:多線程
正確使用多線程,老是可以給開發人員帶來顯著的好處,而使用多線程的緣由主要有如下幾點:併發
一、更多的處理器核心dom
隨着處理器上的核心數量愈來愈多,以及超線程技術的普遍運用,如今大多數計算機都比以往更加擅長並行計算,而處理器性能的提高方式,也從更高的主頻向更多的核心發展。ide
二、更快的響應時間工具
有時咱們會編寫一些業務邏輯比較複雜的代碼,例如,一筆訂單的建立,它包括插入訂單數據、生成訂單快照、發送郵件通知賣家和記錄貨品銷售數量等。用戶從單擊「訂購」按鈕開始,就要等待這些操做所有完成才能看到訂購成功的結果。可是這麼多業務操做,如何可以讓其更快地完成呢?性能
在上面的場景中,可使用多線程技術,即將數據一致性不強的操做派發給其餘線程處理(也可使用消息隊列),如生成訂單快照、發送郵件等。這樣作的好處是響應用戶請求的線程可以儘量快地處理完成,縮短了響應時間,提高了用戶體驗。
三、 更好的編程模型
Java爲多線程編程提供了一致的編程模型,使開發人員可以更加專一於問題的解決,即爲所遇到的問題創建合適的模型,而不是絞盡腦汁地考慮如何將其多線程化。
現代操做系統基本採用時分的形式調度運行的線程,操做系統會分出一個個時間片,線程會分配到若干時間片,當線程的時間片用完了就會發生線程調度,並等待着下次分配。線程分配到的時間片多少也就決定了線程使用處理器資源的多少,而線程優先級就是決定線程須要多或者少分配一些處理器資源的線程屬性。
在Java線程中,經過一個整型成員變量priority來控制優先級,優先級的範圍從1~10,在線程構建的時候能夠經過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多於優先級低的線程。
設置線程優先級時,針對頻繁阻塞(休眠或者I/O操做)的線程須要設置較高優先級,而偏重計算(須要較多CPU時間或者偏運算)的線程則設置較低的優先級,確保處理器不會被獨佔。
注意:線程優先級不能做爲程序正確性的依賴,由於操做系統能夠徹底不用理會Java線程對於優先級的設定。
Java線程在運行的生命週期中可能處於下表所示的6種不一樣的狀態,在給定的一個時刻,線程只能處於其中的一個狀態。
狀態名稱 | 說明 |
---|---|
NEW | 初始狀態,線程被構建,可是尚未調用start()方法 |
RUNNABLE | 運行狀態,Java線程將操做系統中的就緒和運行兩種狀態籠統地稱做「運行中」 |
BLOCKED | 阻塞狀態,表示線程阻塞於鎖 |
WAITING | 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程須要等待其餘線程作出一些特定動做(通知或中斷) |
TIME_WAITING | 超時等待狀態,該狀態不一樣於WAITING,它是能夠在指定的時間自行返回的 |
TERMINATED | 終止狀態,表示當前線程已經執行完畢 |
線程在自身的生命週期中,並非固定地處於某個狀態,而是隨着代碼的執行在不一樣的狀態之間進行切換,Java線程狀態變遷以下圖:
Java將操做系統中的運行和就緒兩個狀態合併稱爲運行狀態。阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態,可是阻塞在java.concurrent包中Lock接口的線程狀態倒是等待狀態,由於java.concurrent包中Lock接口對於阻塞的實現均使用了LockSupport類中的相關方法。
Daemon線程是一種支持型線程,由於它主要被用做程序中後臺調度以及支持性工做。當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。能夠經過調用Thread.setDaemon(true)將線程設置爲Daemon線程。Daemon屬性須要在啓動線程以前設置,不能在啓動線程以後設置。
在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。以下代碼:
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程序,能夠看到在終端或者命令提示符沒有任何輸出。
在運行線程以前首先要構造一個線程對象,線程對象在構造的時候須要提供線程所須要的屬性,如線程所屬的線程組、線程優先級、是不是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、優先級和加載資源的ContextClassLoader以及可繼承的ThreadLocal,同時還會分配一個惟一的ID來標識這個child線程。
線程對象在初始化完成以後,調用start()方法就能夠啓動這個線程。線程start()方法的含義是:當前線程(即parent線程)同步告知Java虛擬機,只要線程規劃器空閒,應當即啓動調用start()方法的線程。
啓動一個線程前,最好爲這個線程設置線程名稱,由於這樣在使用jstack分析程序或者進行問題排查時,就會給開發人員提供一些提示,自定義的線程最好可以起個名字。
中斷能夠理解爲線程的一個標識位屬性,它表示一個運行中的線程是否被其餘線程進行了中斷操做。中斷比如其餘線程對該線程打了個招呼,其餘線程經過調用該線程的interrupt()方法對其進行中斷操做。
線程經過檢查自身是否被中斷來進行響應,線程經過方法isInterrupted()來進行判斷是否被中斷,也能夠調用靜態方法Thread.interrupted()對當前線程的中斷標識位進行復位。若是該線程已經處於終結狀態,即便該線程被中斷過,在調用該線程對象的isInterrupted()時依舊會返回false。
從Java的API中能夠看到,許多聲明拋出InterruptedException的方法(例如Thread.sleep(longmillis)方法)這些方法在拋出InterruptedException以前,Java虛擬機會先將該線程的中斷標識位清除,而後拋出InterruptedException,此時調用isInterrupted()方法將會返回false。
suspend()、resume()和stop()方法完成了線程的暫停、恢復和終止工做,並且很是「人性化」。可是這些API是過時的,也就是不建議使用的。
不建議使用的緣由主要有:以suspend()方法爲例,在調用後,線程不會釋放已經佔有的資源(好比鎖),而是佔有着資源進入睡眠狀態,這樣容易引起死鎖問題。一樣,stop()方法在終結一個線程時不保證線程的資源正常釋放,一般是沒有給予線程完成資源釋放工做的機會,所以會致使程序可能工做在不肯定狀態下。
由於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得以終止。這種經過標識位或者中斷操做的方式可以使線程在終止時有機會去清理資源,而不是武斷地將線程中止,所以這種終止線程的作法顯得更加安全和優雅。
線程開始運行,擁有本身的棧空間,就如同一個腳本同樣,按照既定的代碼一步一步地執行,直到終止。可是,每一個運行中的線程,若是僅僅是孤立地運行,那麼沒有一點兒價值,或者說價值不多,若是多個線程可以相互配合完成工做,這將會帶來巨大的價值。
Java支持多個線程同時訪問一個對象或者對象的成員變量,因爲每一個線程能夠擁有這個變量的拷貝(雖然對象以及成員變量分配的內存是在共享內存中的,可是每一個執行的線程仍是能夠擁有一份拷貝,這樣作的目的是加速程序的執行,這是現代多核處理器的一個顯著特性),因此程序在執行過程當中,一個線程看到的變量並不必定是最新的。
關鍵字volatile能夠用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均須要從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證全部線程對變量訪問的可見性。
關鍵字synchronized能夠修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性。
經過使用javap工具查看生成的class文件信息來分析synchronized關鍵字的實現細節,代碼以下
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){
m();
}
}
public static synchronized void m(){
}
}
複製代碼
執行javap -v Synchronized.class,部分相關輸出以下所示:
對於同步塊的實現使用了monitorenter和monitorexit指令,而同步方法則是依賴方法修飾符上的ACC_SYNCHRONIZED來完成。不管採用哪一種方式,其本質是對一個對象的監視器進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監視器。
任意一個對象都擁有本身的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。
等待/通知機制是指一個線程A調用了對象O的wait()方法進入等待狀態,而另外一個線程B調用了對象O的notify()或notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操做。上述兩個線程對象O來完成交互,而對象上的wait()和notify/notifyAll()的關係就如同開關信號同樣,用來完成等待方通知方之間的交互工做。
等待/通知的相關方法是任意Java對象都具有的,這些方法被定義在全部對象的超類java.lang.Object上。
一、實現生產者-消費者模型,代碼以下:
public class WaitNotify {
private final static int CONTAINER_MAX_LENGTH = 3;
private static Queue<Integer> resources = new LinkedList<Integer>();
//做爲synchronized的對象監視器
private static final Object lock = new Object();
/** * 消息者 */
static class Consumer implements Runnable {
@Override
public void run() {
synchronized (lock) {
// 不能使用if判斷,防止過早喚醒
while (resources.isEmpty()) {
try {
// 當前釋放鎖,線程進入等待狀態。
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " get number is " + resources.remove());
// 喚醒全部等待狀態的線程
lock.notifyAll();
}
}
}
/** * 生產者 */
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();
}
}
}
複製代碼
調用wait()、notify()以及notifyAll()時須要注意的細節,以下:
使用wait()、notify()和notifyAll()時須要先對調用對象加鎖。
調用wait()方法後,線程狀態由RUNNING變爲WAITING,並將當前線程放置到對象的等待隊列。
notify()或notifyAll()方法調用後,等待線程依舊不會從wait()返回,須要調用notify()或notifAll()的線程釋放鎖以後,等待線程纔有機會從wait()返回。
notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中全部的線程所有移到同步隊列,被移動的線程狀態由WAITING變爲BLOCKED。
從wait()方法返回的前提是得到了調用對象的鎖。
二、面試題:設計一個程序,啓動三個線程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();
}
}
}
}
}
}
}
複製代碼