## 03 多線程開發如此簡單—Java中如何編寫多線程程序
> 學習這件事不在乎有沒有人教你,最重要的是在于你自己有沒有覺悟和恒心。
> —— 法布爾
## 1\. Java 實現多線程的方式
前文介紹了多線程的各種應用場景,你是不是已經磨刀霍霍,迫不及待想進入 java 多線程的世界里了?別急,我們第一步要先得到進入多線程世界的鑰匙,也就是如何在 java 中實現多線程。
在 java 中實現多線程有四種方式,如下:
1. 繼承 Thread 類
2. 實現 Runnable 接口
3. 使用 FutureTask
4. 使用 Executor 框架
其中繼承 Thread 類和實現 Runnable 接口是最基本的方式,但有一個共同的缺點 ---- 沒有返回值。而 FutureTask 則解決了這個問題,后面會單獨講解。Executor 是 JDK 提供的多線程框架,功能十分強大,后面也會有章節專門講解。本篇文章主要介紹前兩種最基本的方式,目的是讓你對多線程編程有初步的認識,帶你打開多線程編程的大門。
前文我說過,無形的軟件,都來自于有形的現實世界。我們在學習多線程的過程中,時刻以現實世界作為參照,理解起來就會容易很多。我們設想這樣一個生活中的場景,看看程序如何實現。
小明是一位學生,今天不太開心。因為昨天英語課學習了一個新的單詞,今天考試時他寫錯了。老師懲罰他抄寫 100 遍。這個單詞有點長,是什么單詞呢?internationalization。看著眼熟嗎?做過國際化開發的同學一定認識,這個單詞因為太長,在 java 中被稱為 i18n,也就是首字母 i 和尾字母 n 之間有 18 個字母。小明很苦惱,怎么能快點寫完呢?

## 2\. 單線程實現單詞抄寫
OK,下面我們通過程序來模擬小明抄寫單詞的任務。我們編寫如下幾個類:
1、Punishment.java
存儲要抄寫的單詞,以及剩余的抄寫次數。主要代碼如下:
~~~java
public class Punishment {
private int leftCopyCount;
private String wordToCopy;
}
~~~
2、Student.java
持有 Punishment 的引用。實現了抄寫單詞的 copyWord 方法。主要代碼如下:
~~~java
public class Student {
private String name;
private Punishment punishment;
public Student(String name,Punishment punishment) {
this.name=name;
this.punishment = punishment;
}
public void copyWord() {
int count = 0;
String threadName = Thread.currentThread().getName();
while (true) {
if (punishment.getLeftCopyCount() > 0) {
int leftCopyCount = punishment.getLeftCopyCount();
System.out.println(threadName+"線程-"+name + "抄寫" + punishment.getWordToCopy() + "。還要抄寫" + --leftCopyCount + "次");
punishment.setLeftCopyCount(leftCopyCount);
count++;
} else {
break;
}
}
System.out.println(threadName+"線程-"+name + "一共抄寫了" + count + "次!");
}
}
~~~
Student 構造函數傳入 Punishment。copyWord 方法是根據懲罰內容。完成單詞抄寫的主要邏輯。
我們重點看一下 coppyWord 方法。count 變量是計數器,記錄抄寫的總次數。threadName 是本線程的名稱,這里通過 Thread 的靜態方法 currentThread 取得當前線程,然后通過 getName 方法獲取線程名稱。
在 while 循環體中,當 punishment 的剩余抄寫次數大于 0 時,執行抄寫邏輯,否則抄寫任務完成,跳出循環。邏輯很簡單,相信大家都能看懂。接下來我們通過 main 方法嘗試運行,看看效果。main 方法代碼如下:
~~~java
public class StudentClient {
public static void main(String[] args) {
Punishment punishment = new Punishment(100,"internationalization");
Student student = new Student("小明",punishment);
student.copyWord();
}
}
~~~
輸出如下:
~~~
main線程-小明抄寫internationalization。還要抄寫99次
.........(中間省略)
main線程-小明抄寫internationalization。還要抄寫0次
main線程-小明一共抄寫了100次!
~~~
在控制臺可以清楚地看到小明抄寫了 100 次單詞。不過此時的代碼并沒有引入多線程,是單線程小明在工作。唯一看到的和線程沾邊的就是日志中的 “main 線程”,這是通過 Thread.*currentThread*().getName () 獲取的當前線程名稱,也就是 main 函數所在的線程。
## 3\. 繼承 Thread 實現獨立線程單詞抄寫
接下來我們嘗試為小明單獨起一個線程做這個事情,而不是在 main 線程中完成。回到我們所講的主題,實現多線程的方式上,我們先采用繼承 thread 類,重寫 run 方法的方式。改版后,student 代碼如下:
~~~java
//1、繼承Thread類
public class Student extends Thread{
private String name;
private Punishment punishment;
public Student(String name, Punishment punishment) {
//2、調用Thread構造方法,設置threadName
super(name);
this.name=name;
this.punishment = punishment;
}
public void copyWord() {
int count = 0;
String threadName = Thread.currentThread().getName();
while (true) {
if (punishment.getLeftCopyCount() > 0) {
int leftCopyCount = punishment.getLeftCopyCount();
System.out.println(threadName+"線程-"+name + "抄寫" + punishment.getWordToCopy() + "。還要抄寫" + --leftCopyCount + "次");
punishment.setLeftCopyCount(leftCopyCount);
count++;
} else {
break;
}
}
System.out.println(threadName+"線程-"+name + "一共抄寫了" + count + "次!");
}
//3、重寫run方法,調用copyWord完成任務
@Override
public void run(){
copyWord();
}
}
~~~
三個變化點在代碼中已經標出。不再多說,只提醒下,在第 2 個點,我們設置了線程的名稱,一會在輸出中會看到帶來的變化。
main 方法代碼如下:
~~~java
public class StudentClient {
public static void main(String[] args) {
Punishment punishment = new Punishment(100,"internationalization");
Student student = new Student("小明",punishment);
student.start();
}
}
~~~
可以看到此時調用的不是 student 的 copyWord 方法,而是調用了 start 方法。start 方法是從 Thread 類繼承而來,調用后線程進入就緒狀態,等待 CPU 的調用。而 start 方法最終會觸發執行 run 方法,在 run 方法中 copyWord 被執行。輸出如下:
~~~
小明線程-小明抄寫internationalization。還要抄寫99次
......(中間省略)
小明線程-小明抄寫internationalization。還要抄寫0次
小明線程-小明一共抄寫了100次!
~~~
我們可以看到,現在不再是 main 線程在工作了,而是小明線程。這說明 student 已經工作在 “小明” 線程上。為了更加直觀,我們在 student.start () 后面加一行代碼:
~~~java
System.out.println("Another thread will finish the punishment。 main thread is finished" );
~~~
再次運行程序,輸出如下:
~~~
Another thread to finish the punishment。main thread is finished
小明線程-小明抄寫internationalization。還要抄寫99次
......(中間省略)
小明線程-小明抄寫internationalization。還要抄寫0次
小明線程-小明一共抄寫了100次!
~~~
可以看到主線程在 student.start () 后,會立即向下執行。而小明線程則在獨立執行 copyWord 方法。這里你可以做個對比,單線程情況下,一定是在小明抄寫的所有輸出后才會輸出 “main thread is finished”。
## 4\. 多線程并發實現單詞抄寫
你心里一定在想,這個例子沒有看到多線程的好處啊?是的,如果僅僅是小明一個人去完成任務,其實和單線程沒有區別。但是假如小明找來了幾個同學幫他一起寫呢?
我們在 main 方法中啟動多個線程一塊完成單詞抄寫任務:
~~~java
public static void main(String[] args) {
Punishment punishment = new Punishment(100,"internationalization");
Student xiaoming = new Student("小明",punishment);
xiaoming.start();
Student xiaozhang = new Student("小張",punishment);
xiaozhang.start();
Student xiao趙 = new Student("小趙",punishment);
xiaozhang.start();
}
~~~
大家對這段代碼的期望結果是什么呢?按照正常的邏輯,應該是小明先開始寫,他會抄寫的次數多一點,而小張和小趙抄寫的次數少一點,但是三人抄寫的總量應該是 100。不過事與愿違,我們在控制臺可以看到如下輸出:
~~~
小趙線程-小趙一共抄寫了100次!
小明線程-小明一共抄寫了100次!
小張線程-小張一共抄寫了100次!
~~~
小明的工作量不但沒有減少,還連累小張和小趙白白抄寫了 100 遍,為什么會這樣呢?!我在下篇專欄中會詳細解答。這里我可以先肯定的告訴你,我們是有辦法解決現在的問題,達到想要的執行效果。本篇文章我們還是聚焦在多線程如何實現上。
接下來,我們看另外一種多線程實現方式。
## 5\. 實現 Runnable 接口,啟用單獨線程抄寫單詞
上面講解了通過繼承 Thread 的方式來實現多線程,接下來我們看看如何以實現 Runnable 接口的形式實現多線程。student 代碼改造后如下:
~~~java
public class Student implements Runnable{
private String name;
private Punishment punishment;
public Student(String name, Punishment punishment) {
this.name=name;
this.punishment = punishment;
}
public void copyWord() {
int count = 0;
String threadName = Thread.currentThread().getName();
while (true) {
if (punishment.getLeftCopyCount() > 0) {
int leftCopyCount = punishment.getLeftCopyCount();
System.out.println(threadName+"線程-"+name + "抄寫" + punishment.getWordToCopy() + "。還要抄寫" + --leftCopyCount + "次");
punishment.setLeftCopyCount(leftCopyCount);
count++;
} else {
break;
}
}
System.out.println(threadName+"線程-"+name + "一共抄寫了" + count + "次!");
}
//重寫run方法,完成任務。
@Override
public void run(){
copyWord();
}
}
~~~
和繼承 thread 實現多線程的區別,在于現在是實現 runnable 接口。不過也是需要實現 run () 方法。另外由于 runnable 是接口,所以之前構造函數中調用父類構造函數的語句需要去掉。
我們再看看 StudentClient 的代碼:
~~~java
public class StudentClient {
public static void main(String[] args) {
Punishment punishment = new Punishment(100,"internationalization");
Thread xiaoming = new Thread(new Student("小明",punishment),"小明");
xiaoming.start();
}
}
~~~
可以看到我們需要創建一個 thread,把實現了 runnable 接口的對象通過構造函數傳遞進去,Thread 構造函數的第二個參數是自定義的 thread name。之前由于 Student 就是 Thread 的子類,所以我們直接通過 new Student 就可以得到線程對象。最后都是通過調用 Thread 對象的 start 方法來啟動線程。運行代碼后發現輸出結果和繼承 thread 方式是一模一樣的。
## 6\. 總結
本篇講解的內容非常基礎,目的在于讓大家對多線程開發有所感知,快速上手。建議大家自己把代碼敲一邊,體會兩種啟動線程方式的異同。此外,可以重點思考下,為什么多線程并發時,結果并不是我們所期望的。看一看你的答案是否和下篇專欄所寫的原因一樣。通過本篇學習,我們知道在 java 中啟動多線程非常簡單。但是,要想處理好多線程間的協調,并不是一個容易的事情。而多線程開發的難點也就在于此。下一節我們就來看看多線程開發中會遇到的問題。
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35 拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語