適用於Android開發者的多線程總結
highlight: a11y-dark
前言
對於程序員來説,線程一直是我們開發中最常出現的技術,可以説,使用起來完全沒問題,通過百度以及熟悉度可以順手拈來,但是對於深入理解,卻不是所有人都能做到,寫這篇文章的目的,主要用於自己進行復習,總結,未來也會持續修改該文,目前,作者對多線程並不深入,因此文中摘抄了很多大佬的一些文章,感謝大佬們的開源。文中附有相關鏈接,可自行跳轉,感謝呦!!
👓 線程、進程
🚗 進程
指在系統中正在運行的一個應用程序;程序一旦運行就是進程;是系統進行資源分配的基本單位
🧷 線程
進程之內獨立執行的一個單元執行流。線程是CPU
調度和執行的最小單位,包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。
👓 為什麼使用多線程
- 主線程不能執行耗時較長的任務,否則會阻塞UI線程,引起
ANR
、卡頓等問題(只能在UI線程操作UI視圖,不能在子線程中操作) - Android 強制要求開發者在發起網絡請求時,必須在工作線程,不能在主線程,否則拋出
NetworkOnMainThreadException
👓 多線程場景
- 在Android中,App從一啟動。就算是一個空白demo,它也是多線程應用,原因是因為App是運行在
art
上,art
自帶GC
線程,再加上App必有的主線程(UI線程),就組成了一個多線程應用 - 日常使用的三方庫,比如
Okhttp、Glide、RxJava
- 處理耗時任務,刪除數據,清空緩存,操作數據庫等等
UI 線程為什麼不會結束?因為它在初始化完畢後會執⾏死循環,循環的內容是刷新界⾯
👓 併發和並行
- **併發和並行最開始都是操作系統中的概念,表示的是CPU執行多個任務的方式。這兩個概念極容易混淆。
- 如果使用的是單核CPU,那麼我們是無法執行並行操作的,只能做到併發,這樣來充分調用CPU的資源。
- 如果我們使用的是多核CPU,我們才可以真正的意義上做到並行操作。
-
併發(Concurrent),在操作系統中,是指一個時間段中有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行。操作系統的時間片分時調度。打遊戲和聽音樂兩件事情在同一個時間段內都是在同一台電腦上完成了從開始到結束的動作。那麼,就可以説聽音樂和打遊戲是併發的。
-
並行(Parallel),當系統CPu有一個以上的內核時,當一個內核執行一個進程時,另一個內核可以執行另一個進程,兩個進程互不搶佔內核資源,可以同時進行,這種方式我們稱之為並行(Parallel)。
演示在同一個程序中,併發跟並行發生的場景 ``` js public class TestDemo {
public static void main(String[] args) {
//併發(搶佔共有資源)
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread, "thread-01");
Thread thread1 = new Thread(testThread, "thread-02");
Thread thread2 = new Thread(testThread, "thread-03");
Thread thread3 = new Thread(testThread, "thread-04");
thread.start();
thread1.start();
thread2.start();
thread3.start();
//並行(互相不搶佔資源)
TestThread testThread1 = new TestThread();
TestThread testThread2 = new TestThread();
testThread1.start();
testThread2.start();
}
static class TestThread extends Thread {
public TestThread() { super("TestThread"); }
private int count = 10;
@Override
public void run() {
super.run();
System.out.println(count--);
}
}
} ```
👓 異步和同步
同步和異步關注的是消息通信機制.
-
同步是指: 發送方發出數據後, 等待接收方發回響應後才發下一個數據包的通訊方式. 就是在發出一個調用時, 在沒有得到結果之前, 該調用就不返回, 但是一旦調用返回, 就得到返回值了. 也就是由"調用者"主動等待這個"調用"的結果.
-
異步是指: 發送方發出數據後, 不等待接收方發回響應, 接着發送下個數據包的通訊方式. 當一個異步過程調用發出後, 調用者不會立刻得到結果. 而是在調用發出後, "被調用者"通過狀態、通知來通知調用者, 或通過回調函數處理這個調用.
👓 使用線程的幾種方式
👓 Java
🎯 Thread
```js Thread thread1 = new Thread() { @Override public void run() { super.run(); System.out.println("直接new出來,簡單粗暴"); } }; thread1.start();
```
🎯 Thread+Runnable
js
Runnable target = new Runnable() {
@Override
public void run() {
System.out.println("實例一個runnable對象,交由線程使用,方便複用");
}
};
Thread thread = new Thread(target);
Thread thread2 = new Thread(target);
thread.start();
thread2.start();
👓 ThreadFactory+Runnable
```js ThreadFactory threadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { return new Thread(runnable, "Thread-" + new Random().nextInt(1000)); } };
Runnable target1 = new Runnable() {
@Override
public void run() {
System.out.println("實例一個runnable對象,交由線程工廠使用,得到thread對象,方便複用");
}
};
threadFactory.newThread(target1);
```
🎯 ExecutorService+Runnable
```js Executor executor = Executors.newSingleThreadExecutor(); Executor executor1 = Executors.newCachedThreadPool(); Executor executor2 = Executors.newFixedThreadPool(10); Executor executor3 = Executors.newScheduledThreadPool(1);
Runnable target2 = new Runnable() {
@Override
public void run() {
System.out.println("實例一個runnable對象,交由線程池使用,線程池幫助集中管理線程,避免資源浪費,方便複用,");
}
};
executor.execute(target2);
executor1.execute(target2);
executor2.execute(target2);
executor3.execute(target2);
```
Future+Callable
```js
Callable
ExecutorService executor4 = Executors.newCachedThreadPool();
Future<String> future = executor4.submit(callable);
try {
String result = future.get();
System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e)
{
e.printStackTrace();
}
```
👓 android專屬
- Handler
- IntentService
- HandlerThread
- AsyncTask
- Rxjava
考慮到篇幅以及這些異步機制都是大家比較熟知的,暫不列出使用方式,貼上大佬們寫過的文章鏈接,供大家瞭解,如果後續有需要在進行列出,不然篇幅太長,看着看着很容易噁心
👓 kotlin
👓 線程安全
## 👓 為什麼會出現線程安全問題 - Java 內存模型規定了所有的變量都存儲在主內存中,每條線程有自己的工作內存。 - 線程的工作內存中保存了該線程中用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。 - 線程訪問一個變量,首先將變量從主內存拷貝到工作內存,對變量的寫操作,不會馬上同步到主內存。 - 不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。
🎯 解決方式
解決方式部分摘抄自面試官:説説多線程併發問題\ 本人對於多線程相知不多,目前也在學習中,該部分暫時引用,等後續會加入自己的見解進行改寫 - 保證共享資源在同一時間只能由一個線程進行操作(原子性,有序性)。 - 將線程操作的結果及時刷新,保證其他線程可以立即獲取到修改後的最新數據(可見性)。 ### 🎯 volatile - 保證可見性,不保證原子性 1. 當寫一個
volatile
變量時,JVM
會把本地內存的變量強制刷新到主內存中 2. 這個寫操作導致其他線程中的緩存無效,其他線程讀,會從主內存讀。volatile
的寫操作對其它線程實時可見。 - 禁止指令重排序 1. 不會對存在依賴關係的指令重排序,例如 a = 1;b = a; a 和b存在依賴關係,不會被重排序 2. 不能影響單線程下的執行結果。比如:a=1;b=2;c=a+b這三個操作,前兩個操作可以重排序,但是c=a+b不會被重排序,因為要保證結果是3
🎯 使用場景
對於一個變量,只有一個線程執行寫操作,其它線程都是讀操作,這時候可以用 volatile
修飾這個變量。
🎯 單例雙重鎖為什麼要用到volatile?
```js public class TestInstance {
private static volatile TestInstance mInstance;
public static TestInstance getInstance(){ //1 if (mInstance == null){ //2 synchronized (TestInstance.class){ //3 if (mInstance == null){ //4 mInstance = new TestInstance(); //5 } } } return mInstance; } ```
假如沒有用volatile
,併發情況下會出現問題,線程A執行到註釋5 new TestInstance()
的時候,分為如下幾個幾步操作:
- 1、分配內存
- 2、初始化對象
- 3、
mInstance
指向內存
這時候如果發生指令重排,執行順序是132,執行到第3的時候,線程B剛好進來了,並且執行到註釋2,這時候判斷mInstance
不為空,直接使用一個未初始化的對象。所以使用volatile
關鍵字來禁止指令重排序。
👓 volatile 原理
在JVM
底層volatile
是採用內存屏障來實現的,內存屏障會提供3個功能:
- 它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
- 它會強制將緩存的修改操作立即寫到主內存
- 寫操作會導致其它CPU中的緩存行失效,寫之後,其它線程的讀操作會從主內存讀。
👓 volatile 的侷限性
volatile 只能保證可見性,不能保證原子性寫操作對其它線程可見,但是不能解決多個線程同時寫的問題。
👓 Synchronized
👓 使用場景
多個線程同時寫一個變量。
例如售票,餘票是100張,窗口A和窗口B同時各賣出一張票, 假如餘票變量用 volatile
修飾,是有問題的。\
A窗口獲取餘票是100,B窗口獲取餘票也是100,A賣出一張變成99,刷新回主內存,同時B賣出一張變成99,也刷新回主內存,會導致最終主內存餘票是99而不是98。
前面説到volatile
的侷限性,就是多個線程同時寫的情況,這種情況一般可以使用Synchronized。
Synchronized 可以保證同一時刻,只有一個線程可執行某個方法或某個代碼塊。
👓 Synchronized 原理
```js public class SynchronizedTest {
public static void main(String[] args) { synchronized (SynchronizedTest.class) { System.out.println("123"); } method(); }
private static void method() { } } ```
將這段代碼先用javac
命令編譯,再java p -v SynchronizedTest.class
命令查看字節碼,部分字節碼如下
js
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/lanshifu/opengldemo/test/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String 123
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: invokestatic #6 // Method method:()V
26: return
可以看到 4: monitorenter
和 14: monitorexit
,中間是打印的語句。
執行同步代碼塊,首先會執行monitorenter
指令,然後執行同步代碼塊中的代碼,退出同步代碼塊的時候會執行monitorexit
指令 。
使用
Synchronized
進行同步,其關鍵就是必須要對對象的監視器monitor
進行獲取,當線程獲取monitor
後才能繼續往下執行,否則就進入同步隊列,線程狀態變成BLOCK,同一時刻只有一個線程能夠獲取到monitor
,當監聽到monitorexit
被調用,隊列裏就有一個線程出隊,獲取monitor
。詳情參考:www.jianshu.com/p/d53bf830f…
每個對象擁有一個計數器,當線程獲取該對象鎖後,計數器就會加一,釋放鎖後就會將計數器減一,所以只要這個鎖的計數器大於0,其它線程訪問就只能等待。
👓 Synchronized鎖的升級
大家對Synchronized
的理解可能就是重量級鎖,但是Java1.6
對 Synchronized
進行了各種優化之後,有些情況下它就並不那麼重,Java1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖。
偏向鎖: 大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。
當一個線程A訪問加了同步鎖的代碼塊時,會在對象頭中存 儲當前線程的id,後續這個線程進入和退出這段加了同步鎖的代碼塊時,不需要再次加鎖和釋放鎖。
輕量級鎖: 在偏向鎖情況下,如果線程B也訪問了同步代碼塊,比較對象頭的線程id不一樣,會升級為輕量級鎖,並且通過自旋的方式來獲取輕量級鎖。
重量級鎖: 如果線程A和線程B同時訪問同步代碼塊,則輕量級鎖會升級為重量級鎖,線程A獲取到重量級鎖的情況下,線程B只能入隊等待,進入BLOCK狀態。
👓 Synchronized 缺點
- 不能設置鎖超時時間
- 不能通過代碼釋放鎖
- 容易造成死鎖
👓 ReentrantLock
上面説到Synchronized
的缺點,不能設置鎖超時時間和不能通過代碼釋放鎖,ReentranLock
就可以解決這個問題。
在多個條件變量和高度競爭鎖的地方,用ReentrantLock更合適,ReentrantLock
還提供了Condition
,對線程的等待和喚醒等操作更加靈活,一個ReentrantLock
可以有多個Condition
實例,所以更有擴展性。
👓 ReentrantLock 的使用
lock 和 unlock
js
ReentrantLock reentrantLock = new ReentrantLock();
System.out.println("reentrantLock->lock");
reentrantLock.lock();
try {
System.out.println("睡眠2秒...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
System.out.println("reentrantLock->unlock");
}
實現可定時的鎖請求:tryLock
```js public static void main(String[] args) { ReentrantLock reentrantLock = new ReentrantLock(); Thread thread1 = new Thread_tryLock(reentrantLock); thread1.setName("thread1"); thread1.start(); Thread thread2 = new Thread_tryLock(reentrantLock); thread2.setName("thread2"); thread2.start(); }
static class Thread_tryLock extends Thread {
ReentrantLock reentrantLock;
public Thread_tryLock(ReentrantLock reentrantLock) {
this.reentrantLock = reentrantLock;
}
@Override
public void run() {
try {
System.out.println("try lock:" + Thread.currentThread().getName());
boolean tryLock = reentrantLock.tryLock(3, TimeUnit.SECONDS);
if (tryLock) {
System.out.println("try lock success :" + Thread.currentThread().getName());
System.out.println("睡眠一下:" + Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("醒了:" + Thread.currentThread().getName());
} else {
System.out.println("try lock 超時 :" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("unlock:" + Thread.currentThread().getName());
reentrantLock.unlock();
}
}
}
```
打印的日誌:
js
try lock:thread1
try lock:thread2
try lock success :thread2
睡眠一下:thread2
try lock 超時 :thread1
unlock:thread1
Exception in thread "thread1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.lanshifu.demo_module.test.lock.ReentranLockTest$Thread_tryLock.run(ReentranLockTest.java:60)
醒了:thread2
unlock:thread2
上面演示了trtLock
的使用,trtLock
設置獲取鎖的等待時間,超過3秒直接返回失敗,可以從日誌中看到結果。 有異常是因為thread1
獲取鎖失敗,不應該調用unlock
。
👓 Condition 條件
```js public static void main(String[] args) {
Thread_Condition thread_condition = new Thread_Condition();
thread_condition.setName("測試Condition的線程");
thread_condition.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread_condition.singal();
}
static class Thread_Condition extends Thread {
@Override
public void run() {
await();
}
private ReentrantLock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
try {
System.out.println("lock");
lock.lock();
System.out.println(Thread.currentThread().getName() + ":我在等待通知的到來...");
condition.await();//await 和 signal 對應
//condition.await(2, TimeUnit.SECONDS); //設置等待超時時間
System.out.println(Thread.currentThread().getName() + ":等到通知了,我繼續執行>>>");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("unlock");
lock.unlock();
}
}
public void singal() {
try {
System.out.println("lock");
lock.lock();
System.out.println("我要通知在等待的線程,condition.signal()");
condition.signal();//await 和 signal 對應
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("unlock");
lock.unlock();
}
}
}
```
運行打印日誌
js
lock
測試Condition的線程:我在等待通知的到來...
lock
我要通知在等待的線程,condition.signal()
unlock
測試Condition的線程:等到通知了,我繼續執行>>>
unlock
複製代碼
上面演示了Condition的 await 和 signal
使用,前提要先lock。
👓 公平鎖與非公平鎖
ReentrantLock
構造函數傳true表示公平鎖。
公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的順序。而非公平鎖就是一種鎖的搶佔機制,是隨機獲得鎖的,可能會導致某些線程一致拿不到鎖,所以是不公平的。
👓 ReentrantLock 注意點
ReentrantLock
使用lock
和unlock
來獲得鎖和釋放鎖unlock
要放在finally
中,這樣正常運行或者異常都會釋放鎖- 使用
condition
的await
和signal
方法之前,必須調用lock
方法獲得對象監視器
👓 併發包
通過上面分析,併發嚴重的情況下,使用鎖顯然效率低下,因為同一時刻只能有一個線程可以獲得鎖,其它線程只能乖乖等待。
Java提供了併發包解決這個問題,接下來介紹併發包裏一些常用的數據結構。
👓 ConcurrentHashMap
我們都知道HashMap是線程不安全的數據結構,HashTable
則在HashMap
基礎上,get
方法和put
方法加上Synchronized
修飾變成線程安全,不過在高併發情況下效率底下,最終被ConcurrentHashMap
替代。
ConcurrentHashMap
採用分段鎖,內部默認有16個桶,get
和put
操作,首先將key計算hashcode
,然後跟16取餘,落到16個桶中的一個,然後每個桶中都加了鎖(ReentrantLock
),桶中是HashMap
結構(數組加鏈表,鏈表過長轉紅黑樹)。
所以理論上最多支持16個線程同時訪問。
👓 LinkBlockingQueue
鏈表結構的阻塞隊列,內部使用多個ReentrantLock
```js /* Lock held by take, poll, etc / private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
/**
* Signals a waiting put. Called only from take/poll.
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
```
源碼不貼太多,簡單説一下LinkBlockingQueue
的邏輯:
- 從隊列獲取數據,如果隊列中沒有數據,會調用
notEmpty.await();
進入等待。- 在放數據進去隊列的時候會調用
notEmpty.signal();
,通知消費者,1中的等待結束,喚醒繼續執行。- 從隊列裏取到數據的時候會調用
notFull.signal();
,通知生產者繼續生產。- 在put數據進入隊列的時候,如果判斷隊列中的數據達到最大值,那麼會調用
notFull.await();
,等待消費者消費掉,也就是等待3去取數據並且發出notFull.signal();
,這時候生產者才能繼續生產。
LinkBlockingQueue
是典型的生產者消費者模式,源碼細節就不多説。
👓 原子操作類:AtomicInteger
內部採用CAS(compare and swap)
保證原子性
舉一個int
自增的例子
js
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet();//自增
源碼看一下
js
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
U 是 Unsafe
,看下 Unsafe#getAndAddInt
```js public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
} ```
通過compareAndSwapInt
保證原子性。
👓 線程通信
該部分摘抄自java中的多線程:線程使用、線程安全、線程通信 \ 本人對於多線程相知不多,目前也在學習中,該部分暫時引用,等後續會加入自己的見解進行改寫 線程有自己的私有空間,但當我多個線程之間相互協作的時候,就需要進行線程間通信方,本節將介紹Java線程之間的幾種通信原理。
🎯鎖與同步
這種方式主要是對全局變量加鎖,即用synchronized
關鍵字對對象或者代碼塊加鎖lock
,來達成線程間通信。
這種方式可詳見上一節線程同步中的例子。
🎯等待/通知機制
基於“鎖”的方式需要線程不斷去嘗試獲得鎖,這會耗費服務器資源。
Java多線程的等待/通知機制是基於Object
類的wait()
方法和notify()
, notifyAll()
方法來實現的,
wait()
方法和notify()
方法必須寫在synchronized
代碼塊裏面:
wait()
和notify()
方法必須通過獲取的鎖對象進行調用,因為wait
就是線程在獲取對象鎖後,主動釋放對象鎖,同時休眠本線程,直到有其它線程調用對象的notify()
喚醒該線程,才能繼續獲取對象鎖,並繼續執行。相應的notify()
就是對對象鎖的喚醒操作,因而必須放在加鎖的synchronized
代碼塊環境內。
notify()
方法會隨機叫醒一個正在等待的線程,而notifyAll()
會叫醒所有正在等待的線程,被喚醒的線程重新在就緒隊列中按照一定算法最終再次被處理機獲得並進行處理,而不是立馬重新獲得處理機。
```js public class mythread {
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadA: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadB: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new ThreadB()).start();
}
}
```
🎯join方法
join()
方法讓當前線程陷入“等待”狀態,等join
的這個線程執行完成後,再繼續執行當前線程。
當主線程創建並啟動了耗時子線程,而主線程早於子線程結束之前結束時,就可以用join
方法等子線程執行完畢後,從而讓主線程獲得子線程中的處理完的某個數據。
join()
方法及其重載方法底層都是利用了wait(long)
這個方法。
```js public class mythread {
static class ThreadA implements Runnable {
@Override
public void run() {
try {
System.out.println("子線程睡一秒");
Thread.sleep(1000);
System.out.println("子線程睡完了一秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("如果不加join方法,這行就會先打印出來");
}
} ```
🎯sleep方法
sleep
方法是Thread
類的一個靜態方法。它的作用是讓當前線程睡眠一段時間:
Thread.sleep(long)
這裏需要強調一下:sleep方法是不會釋放當前的鎖的,而wait方法會。這也是最常見的一個多線程面試題。
sleep
方法和wait
方法的區別:
wait
可以指定時間,也可以不指定;而sleep
必須指定時間。wait
釋放cpu資源,同時釋放鎖;sleep
釋放cpu資源,但是不釋放鎖,所以易死鎖。wait
必須放在同步塊或同步方法中,而sleep
可以再任意位置
🎯ThreadLocal類
ThreadLocal
是一個本地線程副本變量工具類,可以理解成為線程本地變量或線程本地存儲。嚴格來説,ThreadLocal
類並不屬於多線程間的通信,而是讓每個線程有自己“獨立”的變量,線程之間互不影響。
ThreadLocal
類最常用的就是set方法和get方法。示例代碼:
```js
public class mythread {
static class ThreadA implements Runnable {
private ThreadLocal
public ThreadA(ThreadLocal<String> threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
threadLocal.set("A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadA輸出:" + threadLocal.get());
}
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
new Thread(new ThreadA(threadLocal)).start();
}
}
} ```
可以看到,ThreadA
可以存取自己當前線程的一個值。如果開發者希望將類的某個靜態變量(user ID或者transaction ID)
與線程狀態關聯,則可以考慮使用ThreadLocal
,而不是在每個線程中聲明一個私有變量來操作,加“重”線程。
InheritableThreadLocal
是ThreadLocal
的繼承子類,不僅當前線程可以存取副本值,而且它的子線程也可以存取這個副本值。
🎯信號量機制
JDK提供了一個類似於“信號量”功能的類Semaphore
。在多個線程(超過2個)需要相互合作的場景下,我們用簡單的“鎖”和“等待通知機制”就不那麼方便了。這個時候就可以用到信號量。JDK中提供的很多多線程通信工具類都是基於信號量模型的。
🎯管道
管道是基於“管道流”的通信方式。JDK提供了PipedWriter
、 PipedReader
、 PipedOutputStream
、 PipedInputStream
。其中,前面兩個是基於字符的,後面兩個是基於字節流的。
應用場景:管道多半與I/O流相關。當我們一個線程需要先另一個線程發送一個信息(比如字符串)或者文件等等時,就需要使用管道通信了。
```js public class Pipe { static class ReaderThread implements Runnable { private PipedReader reader;
public ReaderThread(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
System.out.println("this is reader");
int receive = 0;
try {
while ((receive = reader.read()) != -1) {
System.out.print((char)receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class WriterThread implements Runnable {
private PipedWriter writer;
public WriterThread(PipedWriter writer) {
this.writer = writer;
}
@Override
public void run() {
System.out.println("this is writer");
int receive = 0;
try {
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException, InterruptedException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
writer.connect(reader); // 這裏注意一定要連接,才能通信
new Thread(new ReaderThread(reader)).start();
Thread.sleep(1000);
new Thread(new WriterThread(writer)).start();
}
}
// 輸出: this is reader this is writer test ```