
為更良好的閱讀體驗,請訪問原文:傳送門
一、前言
當我們使用計算機時,可以同時做許多事情,例如一邊打游戲一邊聽音樂。這是因為操作系統(tǒng)支持并發(fā)任務(wù),從而使得這些工作得以同時進行。
- 那么提出一個問題:如果我們要實現(xiàn)一個程序能一邊聽音樂一邊玩游戲怎么實現(xiàn)呢?
public class Tester {
public static void main(String[] args) {
System.out.println("開始....");
playGame();
playMusic();
System.out.println("結(jié)束....");
}
private static void playGame() {
for (int i = 0; i < 50; i++) {
System.out.println("玩游戲" + i);
}
}
private static void playMusic() {
for (int i = 0; i < 50; i++) {
System.out.println("播放音樂" + i);
}
}
}
我們使用了循環(huán)來模擬過程,因為播放音樂和打游戲都是連續(xù)的,但是結(jié)果卻不盡人意,因為函數(shù)體總是要執(zhí)行完之后才能返回。那么到底怎么解決這個問題?
并行與并發(fā)

并行性和并發(fā)性是既相似又有區(qū)別的兩個概念。
并行性是指兩個或多個事件在同一時刻發(fā)生。而并發(fā)性是指兩個或多個事件在同一時間間隔內(nèi)發(fā)生。
在多道程序環(huán)境下,并發(fā)性是指在一段時間內(nèi)宏觀上有多個程序在同時運行,但在單處理機環(huán)境下(一個處理器),每一時刻卻僅能有一道程序執(zhí)行,故微觀上這些程序只能是分時地交替執(zhí)行。例如,在 1 秒鐘時間內(nèi),0 - 15 ms 程序 A 運行;15 - 30 ms 程序 B 運行;30 - 45 ms 程序 C 運行;45 - 60 ms 程序 D 運行,因此可以說,在 1 秒鐘時間間隔內(nèi),宏觀上有四道程序在同時運行,但微觀上,程序 A、B、C、D 是分時地交替執(zhí)行的。
如果在計算機系統(tǒng)中有多個處理機,這些可以并發(fā)執(zhí)行的程序就可以被分配到多個處理機上,實現(xiàn)并發(fā)執(zhí)行,即利用每個處理機愛處理一個可并發(fā)執(zhí)行的程序。這樣,多個程序便可以同時執(zhí)行。以此就能提高系統(tǒng)中的資源利用率,增加系統(tǒng)的吞吐量。
進程和線程
進程是指一個內(nèi)存中運行的應(yīng)用程序。一個應(yīng)用程序可以同時啟動多個進程,那么上面的問題就有了解決的思路:我們啟動兩個進程,一個用來打游戲,一個用來播放音樂。這當然是一種解決方案,但是想象一下,如果一個應(yīng)用程序需要執(zhí)行的任務(wù)非常多,例如 LOL 游戲吧,光是需要播放的音樂就有非常多,人物本身的語音,技能的音效,游戲的背景音樂,塔攻擊的聲音等等等,還不用說游戲本身,就光播放音樂就需要創(chuàng)建許多許多的進程,而進程本身是一種非常消耗資源的東西,這樣的設(shè)計顯然是不合理的。更何況大多數(shù)的操作系統(tǒng)都不需要一個進程訪問其他進程的內(nèi)存空間,也就是說,進程之間的通信很不方便,此時我們就得引入“線程”這門技術(shù),來解決這個問題。
線程是指進程中的一個執(zhí)行任務(wù)(控制單元),一個進程可以同時并發(fā)運行多個線程。我們可以打開任務(wù)管理器,觀察到幾乎所有的進程都擁有著許多的「線程」(在 WINDOWS 中線程是默認隱藏的,需要在「查看」里面點擊「選擇列」,有一個線程數(shù)的勾選項,找到并勾選就可以了)。

進程和線程的區(qū)別
進程:有獨立的內(nèi)存空間,進程中的數(shù)據(jù)存放空間(堆空間和棧空間)是獨立的,至少有一個線程。
線程:堆空間是共享的,??臻g是獨立的,線程消耗的資源也比進程小,相互之間可以影響的,又稱為輕型進程或進程元。
因為一個進程中的多個線程是并發(fā)運行的,那么從微觀角度上考慮也是有先后順序的,那么哪個線程執(zhí)行完全取決于 CPU 調(diào)度器(JVM 來調(diào)度),程序員是控制不了的。我們可以把多線程并發(fā)性看作是多個線程在瞬間搶 CPU 資源,誰搶到資源誰就運行,這也造就了多線程的隨機性。下面我們將看到更生動的例子。
Java 程序的進程(Java 的一個程序運行在系統(tǒng)中)里至少包含主線程和垃圾回收線程(后臺線程),你可以簡單的這樣認為,但實際上有四個線程(了解就好):
- [1] main——main 線程,用戶程序入口
- [2] Reference Handler——清除 Reference 的線程
- [3] Finalizer——調(diào)用對象 finalize 方法的線程
- [4] Signal Dispatcher——分發(fā)處理發(fā)送給 JVM 信號的線程
多線程和單線程的區(qū)別和聯(lián)系?
單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執(zhí)行,是一種微觀上輪流占用 CPU 的機制。
多線程會存在線程上下文切換,會導(dǎo)致程序執(zhí)行速度變慢,即采用一個擁有兩個線程的進程執(zhí)行所需要的時間比一個線程的進程執(zhí)行兩次所需要的時間要多一些。
結(jié)論:即采用多線程不會提高程序的執(zhí)行速度,反而會降低速度,但是對于用戶來說,可以減少用戶的響應(yīng)時間。
多線程的優(yōu)勢
盡管面臨很多挑戰(zhàn),多線程有一些優(yōu)點仍然使得它一直被使用,而這些優(yōu)點我們應(yīng)該了解。
優(yōu)勢一:資源利用率更好
想象一下,一個應(yīng)用程序需要從本地文件系統(tǒng)中讀取和處理文件的情景。比方說,從磁盤讀取一個文件需要 5 秒,處理一個文件需要 2 秒。處理兩個文件則需要:
1| 5秒讀取文件A
2| 2秒處理文件A
3| 5秒讀取文件B
4| 2秒處理文件B
5| ---------------------
6| 總共需要14秒
從磁盤中讀取文件的時候,大部分的 CPU 時間用于等待磁盤去讀取數(shù)據(jù)。在這段時間里,CPU 非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用 CPU 資源??聪旅娴捻樞颍?/p>
1| 5秒讀取文件A
2| 5秒讀取文件B + 2秒處理文件A
3| 2秒處理文件B
4| ---------------------
5| 總共需要12秒
CPU 等待第一個文件被讀取完。然后開始讀取第二個文件。當?shù)诙募诒蛔x取的時候,CPU 會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU 大部分時間是空閑的。
總的說來,CPU 能夠在等待 IO 的時候做一些其他的事情。這個不一定就是磁盤 IO。它也可以是網(wǎng)絡(luò)的 IO,或者用戶輸入。通常情況下,網(wǎng)絡(luò)和磁盤的 IO 比 CPU 和內(nèi)存的 IO 慢的多。
優(yōu)勢二:程序設(shè)計在某些情況下更簡單
在單線程應(yīng)用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態(tài)。相反,你可以啟動兩個線程,每個線程處理一個文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用 CPU 去處理已經(jīng)讀取完的文件。其結(jié)果就是,磁盤總是在繁忙地讀取不同的文件到內(nèi)存中。這會帶來磁盤和 CPU 利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現(xiàn)。
優(yōu)勢三:程序響應(yīng)更快
有時我們會編寫一些較為復(fù)雜的代碼(這里的復(fù)雜不是說復(fù)雜的算法,而是復(fù)雜的業(yè)務(wù)邏輯),例如,一筆訂單的創(chuàng)建,它包括插入訂單數(shù)據(jù)、生成訂單趕快找、發(fā)送郵件通知賣家和記錄貨品銷售數(shù)量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結(jié)果。但是這么多業(yè)務(wù)操作,如何能夠讓其更快地完成呢?
在上面的場景中,可以使用多線程技術(shù),即將數(shù)據(jù)一致性不強的操作派發(fā)給其他線程處理(也可以使用消息隊列),如生成訂單快照、發(fā)送郵件等。這樣做的好處是響應(yīng)用戶請求的線程能夠盡可能快地處理完成,縮短了響應(yīng)時間,提升了用戶體驗。
其他優(yōu)勢
多線程還有一些優(yōu)勢也顯而易見:
- 進程之前不能共享內(nèi)存,而線程之間共享內(nèi)存(堆內(nèi)存)則很簡單。
- 系統(tǒng)創(chuàng)建進程時需要為該進程重新分配系統(tǒng)資源,創(chuàng)建線程則代價小很多,因此實現(xiàn)多任務(wù)并發(fā)時,多線程效率更高.
- Java 語言本身內(nèi)置多線程功能的支持,而不是單純地作為底層系統(tǒng)的調(diào)度方式,從而簡化了多線程編程.
上下文切換
即使是單核處理器也支持多線程執(zhí)行代碼,CPU 通過給每個線程分配 CPU 時間片來實現(xiàn)這個機制。時間片是 CPU 分配給各個線程的時間,因為時間片非常短,所以 CPU 通過不停地切換線程執(zhí)行,讓我們感覺多個線程是同時執(zhí)行的,時間片一般是幾十毫秒(ms)。
CPU 通過時間片分配算法來循環(huán)執(zhí)行任務(wù),當前任務(wù)執(zhí)行一個時間片后會切換到下一個任務(wù)。但是,在切換前會保存上一個任務(wù)的狀態(tài),以便下次切換回這個任務(wù)的時候,可以再加載這個任務(wù)的狀態(tài)。所以任務(wù)從保存到再加載的過程就是一次上下文切換。
這就像我們同時讀兩本書,當我們在讀一本英文的技術(shù)書時,發(fā)現(xiàn)某個單詞不認識,于是打開中英文字典,但是在放下英文技術(shù)書之前,大腦必須先記住這本書獨到了多少頁的多少行,等查完單詞之后,能夠繼續(xù)讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執(zhí)行速度。
二、創(chuàng)建線程的兩種方式
繼承 Thread 類
public class Tester {
// 播放音樂的線程類
static class PlayMusicThread extends Thread {
// 播放時間,用循環(huán)來模擬播放的過程
private int playTime = 50;
public void run() {
for (int i = 0; i < playTime; i++) {
System.out.println("播放音樂" + i);
}
}
}
// 方式1:繼承 Thread 類
public static void main(String[] args) {
// 主線程:運行游戲
for (int i = 0; i < 50; i++) {
System.out.println("打游戲" + i);
if (i == 10) {
// 創(chuàng)建播放音樂線程
PlayMusicThread musicThread = new PlayMusicThread();
musicThread.start();
}
}
}
}
運行結(jié)果發(fā)現(xiàn)打游戲和播放音樂交替出現(xiàn),說明已經(jīng)成功了。
實現(xiàn) Runnable 接口
public class Tester {
// 播放音樂的線程類
static class PlayMusicThread implements Runnable {
// 播放時間,用循環(huán)來模擬播放的過程
private int playTime = 50;
public void run() {
for (int i = 0; i < playTime; i++) {
System.out.println("播放音樂" + i);
}
}
}
// 方式2:實現(xiàn) Runnable 方法
public static void main(String[] args) {
// 主線程:運行游戲
for (int i = 0; i < 50; i++) {
System.out.println("打游戲" + i);
if (i == 10) {
// 創(chuàng)建播放音樂線程
Thread musicThread = new Thread(new PlayMusicThread());
musicThread.start();
}
}
}
}
也能完成效果。
以上就是傳統(tǒng)的兩種創(chuàng)建線程的方式,事實上還有第三種,我們后邊再講。
多線程一定快嗎?
先來一段代碼,通過并行和串行來分別執(zhí)行累加操作,分析:下面的代碼并發(fā)執(zhí)行一定比串行執(zhí)行快嗎?
import org.springframework.util.StopWatch;
// 比較并行和串行執(zhí)行累加操作的速度
public class Tester {
// 執(zhí)行次數(shù)
private static final long COUNT = 100000000;
private static final StopWatch TIMER = new StopWatch();
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
// 打印比較測試結(jié)果
System.out.println(TIMER.prettyPrint());
}
private static void serial() {
TIMER.start("串行執(zhí)行" + COUNT + "條數(shù)據(jù)");
int a = 0;
for (long i = 0; i < COUNT; i++) {
a += 5;
}
// 串行執(zhí)行
int b = 0;
for (long i = 0; i < COUNT; i++) {
b--;
}
TIMER.stop();
}
private static void concurrency() throws InterruptedException {
TIMER.start("并行執(zhí)行" + COUNT + "條數(shù)據(jù)");
// 通過匿名內(nèi)部類來創(chuàng)建線程
Thread thread = new Thread(() -> {
int a = 0;
for (long i = 0; i < COUNT; i++) {
a += 5;
}
});
thread.start();
// 并行執(zhí)行
int b = 0;
for (long i = 0; i < COUNT; i++) {
b--;
}
// 等待線程結(jié)束
thread.join();
TIMER.stop();
}
}
大家可以自己測試一下,每一臺機器 CPU 不同測試結(jié)果可能也會不同,之前在 WINDOWS 本兒上測試的時候,多線程的優(yōu)勢從 1 千萬數(shù)據(jù)的時候才開始體現(xiàn)出來,但是現(xiàn)在換了 MAC,1 億條數(shù)據(jù)時間也差不多,到 10 億的時候明顯串行就比并行快了... 總之,為什么并發(fā)執(zhí)行的速度會比串行慢呢?就是因為線程有創(chuàng)建和上下文切換的開銷。
繼承 Thread 類還是實現(xiàn) Runnable 接口?
想象一個這樣的例子:給出一共 50 個蘋果,讓三個同學(xué)一起來吃,并且給蘋果編上號碼,讓他們吃的時候順便要說出蘋果的編號:

運行結(jié)果可以看到,使用繼承方式實現(xiàn),每一個線程都吃了 50 個蘋果。這樣的結(jié)果顯而易見:是因為顯式地創(chuàng)建了三個不同的 Person 對象,而每個對象在堆空間中有獨立的區(qū)域來保存定義好的 50 個蘋果。
而使用實現(xiàn)方式則滿足要求,這是因為三個線程共享了同一個 Apple 對象,而對象中的 num 數(shù)量是一定的。
所以可以簡單總結(jié)出繼承方式和實現(xiàn)方式的區(qū)別:
繼承方式:
- Java 中類是單繼承的,如果繼承了 Thread 了,該類就不能再有其他的直接父類了;
- 從操作上分析,繼承方式更簡單,獲取線程名字也簡單..(操作上,更簡單)
- 從多線程共享同一個資源上分析,繼承方式不能做到...
實現(xiàn)方式:
- Java 中類可以實現(xiàn)多個接口,此時該類還可以繼承其他類,并且還可以實現(xiàn)其他接口(設(shè)計上,更優(yōu)雅)..
- 從操作上分析,實現(xiàn)方式稍微復(fù)雜點,獲取線程名字也比較復(fù)雜,需要使用
Thread.currentThread()來獲取當前線程的引用.. - 從多線程共享同一個資源上分析,實現(xiàn)方式可以做到..
在這里,三個同學(xué)完成搶蘋果的例子,使用實現(xiàn)方式才是更合理的方式。
對于這兩種方式哪種好并沒有一個確定的答案,它們都能滿足要求。就我個人意見,我更傾向于實現(xiàn) Runnable 接口這種方法。因為線程池可以有效的管理實現(xiàn)了 Runnable 接口的線程,如果線程池滿了,新的線程就會排隊等候執(zhí)行,直到線程池空閑出來為止。而如果線程是通過實現(xiàn) Thread 子類實現(xiàn)的,這將會復(fù)雜一些。
有時我們要同時融合實現(xiàn) Runnable 接口和 Thread 子類兩種方式。例如,實現(xiàn)了 Thread 子類的實例可以執(zhí)行多個實現(xiàn)了 Runnable 接口的線程。一個典型的應(yīng)用就是線程池。
常見錯誤:調(diào)用 run() 方法而非 start() 方法
創(chuàng)建并運行一個線程所犯的常見錯誤是調(diào)用線程的 run() 方法而非 start() 方法,如下所示:
1| Thread newThread = new Thread(MyRunnable());
2| newThread.run(); //should be start();
起初你并不會感覺到有什么不妥,因為 run() 方法的確如你所愿的被調(diào)用了。但是,事實上,run() 方法并非是由剛創(chuàng)建的新線程所執(zhí)行的,而是被創(chuàng)建新線程的當前線程所執(zhí)行了。也就是被執(zhí)行上面兩行代碼的線程所執(zhí)行的。想要讓創(chuàng)建的新線程執(zhí)行 run() 方法,必須調(diào)用新線程的 start() 方法。
三、線程的安全問題
吃蘋果游戲的不安全問題
我們來考慮一下上面吃蘋果的例子,會有什么問題?
盡管,Java 并不保證線程的順序執(zhí)行,具有隨機性,但吃蘋果比賽的案例運行多次也并沒有發(fā)現(xiàn)什么太大的問題。這并不是因為程序沒有問題,而只是問題出現(xiàn)的不夠明顯,為了讓問題更加明顯,我們使用 Thread.sleep() 方法(經(jīng)常用來模擬網(wǎng)絡(luò)延遲)來讓線程休息 10 ms,讓其他線程去搶資源。(注意:在程序中并不是使用 Thread.sleep(10)之后,程序才出現(xiàn)問題,而是使用之后,問題更明顯.)

為什么會出現(xiàn)這樣的錯誤呢?
先來分析第一種錯誤:為什么會吃重復(fù)的蘋果呢?就拿 B 和 C 都吃了編號為 47 的蘋果為例吧:
- A 線程拿到了編號為 48 的蘋果,打印輸出然后讓 num 減 1,睡眠 10 ms,此時 num 為 47。
- 這時 B 和 C 同時都拿到了編號為 47 的蘋果,打印輸出,在其中一個線程作出了減一操作的時候,A 線程從睡眠中醒過來,拿到了編號為 46 的蘋果,然后輸出。在這期間并沒有任何操作不允許 B 和 C 線程不能拿到同一個編號的蘋果,之前沒有明顯的錯誤僅僅可能只是因為運行速度太快了。
再來分析第二種錯誤:照理來說只應(yīng)該存在 1-50 編號的蘋果,可是 0 和-1 是怎么出現(xiàn)的呢?
- 當 num = 1 的時候,A,B,C 三個線程同時進入了 try 語句進行睡眠。
- C 線程先醒過來,輸出了編號為 1 的蘋果,然后讓 num 減一,當 C 線程醒過來的時候發(fā)現(xiàn) num 為 0 了。
- A 線程醒過來一看,0 都沒有了,只有 -1 了。
歸根結(jié)底是因為沒有任何操作來限制線程來獲取相同的資源并對他們進行操作,這就造成了線程安全性問題。
如果我們把打印和減一的操作分成兩個步驟,會更加明顯:
ABC 三個線程同時打印了 50 的蘋果,然后同時做出減一操作。
像這樣的原子操作,是不允許分步驟進行的,必須保證同步進行,不然可能會引發(fā)不可設(shè)想的后果。
要解決上述多線程并發(fā)訪問一個資源的安全性問題,就需要引入線程同步的概念。
線程同步
多個執(zhí)行線程共享一個資源的情景,是最常見的并發(fā)編程情景之一。為了解決訪問共享資源錯誤或數(shù)據(jù)不一致的問題,人們引入了臨界區(qū)的概念:用以訪問共享資源的代碼塊,這個代碼塊在同一時間內(nèi)只允許一個線程執(zhí)行。
為了幫助編程人員實現(xiàn)這個臨界區(qū),Java(以及大多數(shù)編程語言)提供了同步機制,當一個線程試圖訪問一個臨界區(qū)時,它將使用一種同步機制來查看是不是已經(jīng)有其他線程進入臨界區(qū)。如果沒有其他線程進入臨界區(qū),他就可以進入臨界區(qū)。如果已經(jīng)有線程進入了臨界區(qū),它就被同步機制掛起,直到進入的線程離開這個臨界區(qū)。如果在等待進入臨界區(qū)的線程不止一個,JVM 會選擇其中的一個,其余的將繼續(xù)等待。
synchronized 關(guān)鍵字
如果一個對象已用 synchronized 關(guān)鍵字聲明,那么只有一個執(zhí)行線程被允許訪問它。使用 synchronized 的好處顯而易見:保證了多線程并發(fā)訪問時的同步操作,避免線程的安全性問題。但是壞處是:使用 synchronized 的方法/代碼塊的性能比不用要低一些。所以好的做法是:盡量減小 synchronized 的作用域。
我們還是先來解決吃蘋果的問題,考慮一下 synchronized 關(guān)鍵字應(yīng)該加在哪里呢?

發(fā)現(xiàn)如果還再把 synchronized 關(guān)鍵字加在 if 里面的話,0 和 -1 又會出來了。這其實是因為當 ABC 同是進入到 if 語句中,等待臨界區(qū)釋放的時,拿到 1 編號的線程已經(jīng)又把 num 減一操作了,而此時最后一個等待臨界區(qū)的進程拿到的就會是 -1 了。
同步鎖 Lock
Lock 機制提供了比 synchronized 代碼塊和 synchronized 方法更廣泛的鎖定操作,同步代碼塊/ 同步方法具有的功能 Lock 都有,除此之外更強大,更體現(xiàn)面向?qū)ο蟆T诓l(fā)包的類族中,Lock 是 JUC 包的頂層接口,它的實現(xiàn)邏輯并未用到 synchronized,而是利用了 volatile 的可見性。
使用 Lock 最典型的代碼如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock();
try {
// ..... method body
} finally {
lock.unlock();
}
}
}
線程安全問題
線程安全問題只在多線程環(huán)境下才會出現(xiàn),單線程串行執(zhí)行不存在此類問題。保證高并發(fā)場景下的線程安全,可以從以下四個維度考量:
維度一:數(shù)據(jù)單線程可見
單線程總是安全的。通過限制數(shù)據(jù)僅在單線程內(nèi)可見,可以避免數(shù)據(jù)被其他線程篡改。最典型的就是線程局部變量,它存儲在獨立虛擬機棧幀的局部變量表中,與其他線程毫無瓜葛。TreadLocal 就是采用這種方式來實現(xiàn)線程安全的。
維度二:只讀對象
只讀對象總是安全的。它的特性是允許復(fù)制、拒絕寫入。最典型的只讀對象有 String、Integer 等。一個對象想要拒絕任何寫入,必須要滿足以下條件:
- 使用
final關(guān)鍵字修飾類,避免被繼承; - 使用
private final關(guān)鍵字避免屬性被中途修改; - 沒有任何更新方法;
- 返回值不能為可變對象。
維度三:線程安全類
某些線程安全類的內(nèi)部有非常明確的線程安全機制。比如 StringBuffer 就是一個線程安全類,它采用 synchronized 關(guān)鍵字來修飾相關(guān)方法。
維度四:同步與鎖機制
如果想要對某個對象進行并發(fā)更新操作,但又不屬于上述三類,需要開發(fā)工程師在代碼中實現(xiàn)安全的同步機制。雖然這個機制支持的并發(fā)場景很有價值,但非常復(fù)雜且容易出現(xiàn)問題。
處理線程安全的核心理念
要么只讀,要么加鎖。
合理利用好 JDK 提供的并發(fā)包,往往能化腐朽為神奇。Java 并發(fā)包(java.util.concurrent,JUC)中大多數(shù)類注釋都寫有:@author Doug Lea。如果說 Java 是一本史書,那么 Doug Lea 絕對是開疆拓土的偉大人物。Doug Lea 在當大學(xué)老師時,專攻并發(fā)編程和并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計,主導(dǎo)設(shè)計了 JUC 并發(fā)包,提高了 Java 并發(fā)編程的易用性,大大推進了 Java 的商用進程。
參考資料
- 《Java 零基礎(chǔ)入門教程》 - http://study.163.com/course/courseMain.htm?courseId=1003108028
- 《Java 并發(fā)編程的藝術(shù)》
- 《Java 7 并發(fā)編程實戰(zhàn)手冊》
- 《碼出高效 Java 開發(fā)手冊》 - 楊冠寶(孤盡) 高海慧(鳴莎)著
按照慣例黏一個尾巴:
歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處!
獨立域名博客:wmyskxz.com
簡書 ID:@我沒有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號:wmyskxz
分享自己的學(xué)習(xí) & 學(xué)習(xí)資料 & 生活
想要交流的朋友也可以加 qq 群:3382693
