前言
提到併發編程,很多人會想到多線程;希望讓多個線程共同完成一項任務,以提高生產效率。所以要聊併發編程之前,就要明白線程和進程的關係。
進程:在現代操作系統中,每一個獨立運行的程序都是一個進程,比如運行中的word,微信等等都是一個獨立進程。
線程:在現代操作系統中,線程也叫輕量級進程,每個進程裏面可以包含多個線程。CPU資源可以在多個線程之間不斷切換,彷彿所有線程在並行執行。每個線程都有自己的計數器,堆棧,和局部變量等屬性。這些線程也能夠訪問共享的內存變量。這將成爲日後阻礙我們寫出健壯且安全的併發程序的最大障礙。
使用多線程的原因
正確使用多線程,總是能夠給開發人員帶來顯著的好處,而使用多線程的原因主要有以下幾點:
1、更多的處理器核心
隨着處理器上的核心數量越來越多,以及超線程技術的廣泛運用,現在大多數計算機都比以往更加擅長並行計算,而處理器性能的提升方式,也從更高的主頻向更多的核心發展。
2、更快的響應時間
有時我們會編寫一些業務邏輯比較複雜的代碼,例如,一筆訂單的創建,它包括插入訂單數據、生成訂單快照、發送郵件通知賣家和記錄貨品銷售數量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結果。但是這麼多業務操作,如何能夠讓其更快地完成呢?
在上面的場景中,可以使用多線程技術,即將數據一致性不強的操作派發給其他線程處理(也可以使用消息隊列),如生成訂單快照、發送郵件等。這樣做的好處是響應用戶請求的線程能夠儘可能快地處理完成,縮短了響應時間,提升了用戶體驗。
3、 更好的編程模型
Java爲多線程編程提供了一致的編程模型,使開發人員能夠更加專注於問題的解決,即爲所遇到的問題建立合適的模型,而不是絞盡腦汁地考慮如何將其多線程化。
併發編程需要注意的問題
上下文切換
cpu通過時間分片來執行任務,多個線程在cpu上爭搶時間片執行,線程切換需要保存一些狀態,再次切換回去需要恢復狀態,此爲上下文切換成本。
因此並不是線程越多越快
,頻繁的切換會損失性能
減少上下文切換的方法:
- 無鎖併發編程:例如把一堆數據分爲幾塊,交給不同線程執行,避免用鎖
- 使用CAS:用自旋不用鎖可以減少線程競爭切換,但是可能會更加耗cpu
- 使用最少的線程
- 使用協程:在一個線程裏執行多個任務
死鎖
死鎖就是線程之間因爭奪資源, 處理不當出現的相互等待現象
避免死鎖的方法:
- 避免一個線程同時獲取多個鎖
- 避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源
- 嘗試使用定時鎖,lock.tryLock(timeout)
- 對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況
資源限制
程序的執行需要資源,比如數據庫連接、帶寬,可能會由於資源的限制,多個線程並不是併發,而是串行,不僅無優勢,反而帶來不必要的上下文切換損耗
常見資源限制
- 硬件資源限制
- 帶寬
- 磁盤讀寫速度
- cpu處理速度
- 軟件資源限制
- 數據庫連接數
- socket連接數
應對資源限制
- 集羣化,增加資源
- 根據不同的資源限制調整程序的併發度,找到瓶頸,把瓶頸資源搞多一些,或者根據這個瓶頸調整線程數
創建線程的三種方式
繼承Thread類
// 繼承Thread
class MyThread extends Thread {
// 重寫run方法執行任務
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 可以通過this拿到當前線程
System.out.println(this.getName()+"執行了"+i);
}
}
}
public class Demo_02_02_1_ThreadCreateWays {
public static void main(String[] args) {
// 先new出來,然後啓動
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 10; i++) {
// 通過Thread的靜態方法拿到當前線程
System.out.println(Thread.currentThread().getName()+"執行了"+i);
}
}
}
實現Runnable
// 實現Runnable接口
class MyThreadByRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 不能用this了
System.out.println(Thread.currentThread().getName() + "執行了" + i);
}
}
}
public class Demo_02_02_1_ThreadCreateWays {
public static void main(String[] args) {
// 實現Runnable接口的方式啓動線程
Thread thread = new Thread(new MyThreadByRunnable());
thread.start();
for (int i = 0; i < 10; i++) {
// 通過Thread的靜態方法拿到當前線程
System.out.println(Thread.currentThread().getName() + "執行了" + i);
}
}
}
因爲Runnable是函數式接口,用lamba也可以
new Thread(() -> {
System.out.println("Runnable是函數式接口, java8也可以使用lamba");
}).start();
使用Callable和Future
// 使用Callable
class MyThreadByCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"執行了"+i);
sum+=i;
}
return sum;
}
}
public class Demo_02_02_1_ThreadCreateWays {
public static void main(String[] args) {
// 用FutureTask包一層
FutureTask<Integer> futureTask = new FutureTask<>(new MyThreadByCallable());
new Thread(futureTask).start();
try {
// 調用futureTask的get能拿到返回的值
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
這是最複雜的一種方式,他可以有返回值,歸納一下步驟:
- 搞一個類實現
Callable
接口,重寫call
方法,在call
執行任務 - 用
FutureTask
包裝實現Callable
接口類的實例 - 將
FutureTask
的實例作爲Thread
構造參數 - 調用
FutureTask
實例的get
拿到返回值,調這一句會阻塞父線程
Callable
也是函數式接口,所以也能用lamba
爲啥Thread構造裏邊能放Runnable,也能放FutureTask? 其實FutureTask繼承RunnableFuture,而RunnableFuture繼承Runnable和Future,所以FutureTask也是Runnable
三種方式比較
方式 | 使用簡易程度 | 是否可以共享任務代碼 | 是否可以有返回值 | 是否可以聲明拋出異常 | 是否可以再繼承別的類 |
---|---|---|---|---|---|
繼承Thread | 簡單 | 不能 | 不能 | 不能 | 不能 |
Runnable | 中等 | 可以 | 不能 | 不能 | 可以 |
Callable | 複雜 | 可以 | 可以 | 可以 | 可以 |
繼承Thread
是最容易的,但是也是最不靈活的
使用Callable
時最複雜的,但是也是最靈活的
這裏說的共享任務代碼
舉個例子:
還是上面那個MyThreadByRunnable
類
MyThreadByRunnable myThreadByRunnable = new MyThreadByRunnable();
Thread thread = new Thread(myThreadByRunnable);
thread.start();
// 再來一個,複用了任務代碼,繼承Thread就不行
Thread thread2 = new Thread(myThreadByRunnable);
thread2.start();
線程間通信
線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執行,直到終止。但是,每個運行中的線程,如果僅僅是孤立地運行,那麼沒有一點兒價值,或者說價值很少,如果多個線程能夠相互配合完成工作,這將會帶來巨大的價值。
volatile和synchronized關鍵字
Java支持多個線程同時訪問一個對象或者對象的成員變量,由於每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量分配的內存是在共享內存中的,但是每個執行的線程還是可以擁有一份拷貝,這樣做的目的是加速程序的執行,這是現代多核處理器的一個顯著特性),所以程序在執行過程中,一個線程看到的變量並不一定是最新的。
關鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性。
關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性。
通過使用javap工具查看生成的class文件信息來分析synchronized關鍵字的實現細節,代碼如下
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){
m();
}
}
public static synchronized void m(){
}
}
對於同步塊的實現使用了monitorenter和monitorexit指令,而同步方法則是依賴方法修飾符上的ACC_SYNCHRONIZED來完成。無論採用哪種方式,其本質是對一個對象的監視器進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監視器。
任意一個對象都擁有自己的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。
控制線程
join
主線程join一個線程,那麼主線程會阻塞直到join進來的線程執行完,主線程繼續執行, join如果帶超時時間的話,那麼如果超時的話主線程也會不再等join進去的線程而繼續執行.
join實際就是判斷join進來的線程存活狀態,如果活着就調用wait(0),如果帶超時時間了的話,wait裏邊的時間會算出來
while (isAlive()) {
wait(0);
}
API
- public final void join() throws InterruptedException
- public final synchronized void join(long millis, int nanos)
- public final synchronized void join(long millis)
例子
public class Demo_02_06_1_join extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args) throws InterruptedException {
Demo_02_06_1_join joinThread = new Demo_02_06_1_join();
for (int i = 0; i < 100; i++) {
if (i == 10) {
joinThread.start();
joinThread.join();
}
// 打到9就停了,然後執行joinThread這裏邊的代碼,完事繼續從10打
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
sleep
睡覺方法,使得線程暫停一段時間,進入阻塞狀態。
API
- public static native void sleep(long millis) throws InterruptedException
- public static void sleep(long millis, int nanos) throws InterruptedException
示例
public class Demo_02_06_2_sleep extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 輸出到4停止, 5秒後繼續
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args) throws InterruptedException {
Demo_02_06_2_sleep sleepThread = new Demo_02_06_2_sleep();
sleepThread.start();
}
}
yield
也是讓線程暫停一下,但是是進入就緒狀態,讓系統重新開始一次新的調度過程,下一次可能運氣好被yield的線程又被選中。
Thread.yield()
中斷
Java中斷機制是一種協作機制,也就是說通過中斷並不能直接終止另一個線程,而需要被中斷的線程自己處理中斷。
前面有一些方法聲明瞭InterruptedException, 這意味者他們可以被中斷,中斷後把異常拋給調用方,讓調用方自己處理.
被中斷的線程可以自已處理中斷,也可以不處理或者拋出去。
public class Demo_02_06_3_interrupt extends Thread {
static class MyCallable implements Callable {
@Override
public Integer call() throws InterruptedException {
for (int i = 0; i < 5000; i++) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("3333");
throw new InterruptedException("中斷我幹嘛,關注 微信號 大雄和你一起學編程 呀");
}
}
return 0;
}
}
public static void main(String[] args) throws InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
for (int i = 0; i < 100; i++) {
if (i == 3) {
thread.interrupt();
}
}
try {
futureTask.get();
} catch (ExecutionException e) {
// 這裏會捕獲到異常
e.printStackTrace();
}
}
}