參考資料:
Av86373641:黑馬程序員 - 全面深入學(xué)習(xí)java并發(fā)編程
-
進(jìn)程和線程
并行并發(fā)
并發(fā):同一個時間段交叉運(yùn)行多個線程
并行:同一個時間點(diǎn)運(yùn)行多個線程線程的創(chuàng)建方式
- 覆蓋run方法
- 編寫一個Runnable接口對象然后給到Thread對象
- FutureTask和Callable接口(Callable比Runnable多一個返回值,并且可以拋出異常),可以返回值,F(xiàn)utureTask創(chuàng)建完也要給到Thread,然后調(diào)用Thread就可以,因為FutureTask也實現(xiàn)了Runnable接口。我們看下FutureTask對runnable的實現(xiàn)就可以知道,它是調(diào)用了Callable對象,處理了異常和返回值。
public void run() {
if (state != NEW ||
!RUNNER.compareAndSet(this, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
前兩者推薦推薦用第二種
方法1 是把線程和任務(wù)合并在了一起,方法2 是把線程和任務(wù)分開了
用 Runnable 更容易與線程池等高級 API 配合
用 Runnable 讓任務(wù)類脫離了 Thread 繼承體系,更靈活
// 構(gòu)造方法的參數(shù)是給線程指定名字,推薦
Thread t1 = new Thread("t1") {
@Override
// run 方法內(nèi)實現(xiàn)了要執(zhí)行的任務(wù)
public void run() {
log.debug("hello");
}
};
t1.start();
// 創(chuàng)建任務(wù)對象
Runnable task2 = () -> log.debug("hello");
// 參數(shù)1 是任務(wù)對象; 參數(shù)2 是線程名字,推薦
Thread t2 = new Thread(task2, "t2");
t2.start();
Thread和Runnable的關(guān)系
有了Runnable可以把線程和任務(wù)分開來,Runnable可以更容易與線程池等高級API配合,更加靈活。棧幀
棧,每個函數(shù)有一個棧幀
每個棧由多個棧幀(Frame)組成,對應(yīng)著每次方法調(diào)用時所占用的內(nèi)存
每個線程只能有一個活動棧幀,對應(yīng)著當(dāng)前正在執(zhí)行的那個方法
- Thread方法
join:調(diào)用的線程等待被調(diào)用的線程運(yùn)行結(jié)束,主要用于同步。
yield:讓出當(dāng)前線程,從running到runnable
interrupt:打斷其他線程的運(yùn)行
如果被打斷線程正在 sleep,wait,join 會導(dǎo)致被打斷的線程拋出 InterruptedException,并清除 打斷標(biāo)記 如果打斷的正在運(yùn)行的線程,則會設(shè)置 打斷標(biāo)記 ;park 的線程被打斷,也會設(shè)置 打斷標(biāo)記
- 守護(hù)線程
總結(jié):依附其他現(xiàn)成的存在而存在,比如垃圾回收器
默認(rèn)情況下,Java 進(jìn)程需要等待所有線程都運(yùn)行結(jié)束,才會結(jié)束。有一種特殊的線程叫做守護(hù)線程,只要其它非守
護(hù)線程運(yùn)行結(jié)束了,即使守護(hù)線程的代碼沒有執(zhí)行完,也會強(qiáng)制結(jié)束。
-
java的線程狀態(tài)
waiting是join的時候的狀態(tài)
blocked是等待鎖的時候的狀態(tài) 臨界區(qū)
一個程序運(yùn)行多個線程本身是沒有問題的
問題出在多個線程訪問共享資源
多個線程讀共享資源其實也沒有問題
在多個線程對共享資源讀寫操作時發(fā)生指令交錯,就會出現(xiàn)問題
一段代碼塊內(nèi)如果存在對共享資源的多線程讀寫操作,稱這段代碼塊為臨界區(qū)臨界區(qū)出問題的解決方案
阻塞式的解決方案:synchronized,Lock
非阻塞式的解決方案:原子變量
- synchronized的兩種用法
雖然 java 中互斥和同步都可以采用 synchronized 關(guān)鍵字來完成,但它們還是有區(qū)別的:
互斥是保證臨界區(qū)的競態(tài)條件發(fā)生,同一時刻只能有一個線程執(zhí)行臨界區(qū)代碼
同步是由于線程執(zhí)行的先后、順序不同、需要一個線程等待其它線程運(yùn)行到某個點(diǎn)
synchronized用于互斥
要注意兩個都要枷鎖,并且要鎖住同一個對象。成員變量和靜態(tài)變量是否線程安全?
如果它們沒有共享,則線程安全
如果它們被共享了,根據(jù)它們的狀態(tài)是否能夠改變,又分兩種情況
如果只有讀操作,則線程安全
如果有讀寫操作,則這段代碼是臨界區(qū),需要考慮線程安全局部變量是否線程安全?(重點(diǎn)關(guān)注P66)
局部變量是線程安全的
但局部變量引用的對象則未必
如果該對象沒有逃離方法的作用訪問,它是線程安全的
如果該對象逃離方法的作用范圍,需要考慮線程安全常見線程安全類
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的類線程安全類方法的組合
Hashtable table = new Hashtable();
// 線程1,線程2
if( table.get("key") == null) {
table.put("key", value);
}
開閉原則
閉原則(final private)可以增加類的安全性,比如String設(shè)置為final就是。(P66)-
monitor鎖,管程/監(jiān)視器
鎖優(yōu)化(這些措施都是java虛擬機(jī)自動操作的,雖然可能可以配置)
上面的monitor是重型鎖,還有輕量鎖和偏向鎖。
偏向鎖:這個鎖的前提是鎖真的主要是其中一個線程在用。
synchronized最開始加的是輕量級鎖,后面有人來了,進(jìn)行鎖膨脹才加為重量級鎖,鎖膨脹是為了后面的線程有等待隊列。
鎖膨脹:輕量級鎖變?yōu)橹亓考夋i,鎖膨脹是為了后面的線程有等待隊列。
自旋優(yōu)化:空轉(zhuǎn)檢查,避免因為進(jìn)入阻塞隊列帶來的上下文切換,多核CPU才有用。自旋失敗的時候才進(jìn)行阻塞。單核的時候其實是利用任務(wù)隊列當(dāng)做隊列,單核其實就很不好,因為自旋優(yōu)化就是為了減少任務(wù)切換。
偏向鎖:輕量級鎖在沒有競爭的時候,每次重入仍需要執(zhí)行CAS操作。在對象一開始的時候就是使用的偏向鎖,如果后面有任意一次競爭或者偏向的改變,即使解鎖了,重新加鎖,都不是偏向鎖了?;蛘哒{(diào)用wait,notify的時候也會被撤銷,終身禁用。因為wait,notify只有重量級鎖才有。
批量重偏向:如果發(fā)現(xiàn)在t2線程內(nèi)因為偏向不同而從偏向鎖轉(zhuǎn)向輕量級鎖太多之后(即撤銷偏向鎖),后面把這些對象和鎖都偏向于t2線程。
批量撤銷:如果撤銷偏向鎖的次數(shù)太多,那么后面對于同一個類的對象再也不用偏向鎖了。
鎖消除:JIT即時編譯器會優(yōu)化,如果非必要會消除鎖。-
wait¬ify
進(jìn)入synchronized代碼片段之后(記住wait,notify調(diào)用的這個必要條件),調(diào)用wait的線程會進(jìn)入waitset進(jìn)行等待,并且放棄鎖,這時處于waiting狀態(tài),等待notify/notifyAll后進(jìn)入EntryList等待調(diào)度,這時處于blocked狀態(tài)。這意味著A在wait之后,B進(jìn)入synchronized代碼,B調(diào)用notify,A還是不會馬上執(zhí)行,至少得等待B退出synchronized代碼(或者調(diào)用wait放棄鎖),因為A在EntryList等待鎖,而B沒有退出synchronized代碼就沒有釋放鎖。
wait,notify都必須在sychronized里面才可以。
兩者的使用:wait表示要滿足一定的條件,notify表示條件已經(jīng)滿足 wait¬ify使用搭配
synchronized(lock) {
while(條件不成立) {
lock.wait();
}
// 干活
}
//另一個線程
synchronized(lock) {
lock.notifyAll();
}
- join的實現(xiàn)
join是用wait實現(xiàn)的,wait等待線程死掉。
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
park&unpark
與 Object 的 wait & notify 相比
wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以線程為單位來【阻塞】和【喚醒】線程,而 notify 只能隨機(jī)喚醒一個等待線程,notifyAll
是喚醒所有等待線程,就不那么【精確】
park & unpark 可以先 unpark,而 wait & notify 不能先 notify-
ReentrantLock可重入鎖
lock:得不到鎖死等。
lockInterruptly可打斷:在獲取鎖的時候可以打斷,退出阻塞,得不到鎖而返回。
tryLock:得不到鎖立即返回,或者可以設(shè)置超時時間。
synchronized只能是死等,相當(dāng)于只是lock。
但是synchronized會自動釋放鎖,包括發(fā)生異常的時候。 Synchronized和ReentrantLock等價代碼
Synchronized{臨界代碼段}
ReentrantLock lock = new ReentrantLock();
lock.lock ();
try {
臨界代碼段
}finally{
lock.unlock();
}Java 內(nèi)存模型
JMM 即 Java Memory Model,它定義了主存、工作內(nèi)存抽象概念,底層對應(yīng)著 CPU 寄存器、緩存、硬件內(nèi)存、CPU 指令優(yōu)化等。
JMM 體現(xiàn)在以下幾個方面
原子性 - 保證指令不會受到線程上下文切換的影響
可見性 - 保證指令不會受 cpu 緩存的影響
有序性 - 保證指令不會受 cpu 指令并行優(yōu)化的影響volatile
保證可見性和有序性,并不能保證原子性。synchronized三者都可以。volatile原理
volatile 的底層實現(xiàn)原理是內(nèi)存屏障,Memory Barrier(Memory Fence)
對 volatile 變量的寫指令后會加入寫屏障
對 volatile 變量的讀指令前會加入讀屏障
可見性:
寫屏障(sfence)保證在該屏障之前的,對共享變量的改動,都同步到主存當(dāng)中
而讀屏障(lfence)保證在該屏障之后,對共享變量的讀取,加載的是主存中最新數(shù)據(jù)
有序性:
寫屏障會確保指令重排序時,不會將寫屏障之前的代碼排在寫屏障之后
讀屏障會確保指令重排序時,不會將讀屏障之后的代碼排在讀屏障之前happens-before
happens-before 規(guī)定了對共享變量的寫操作對其它線程的讀操作可見,它是可見性與有序性的一套規(guī)則總結(jié),拋開以下 happens-before 規(guī)則,JMM 并不能保證一個線程對共享變量的寫,對于其它線程對該共享變量的讀可見。
-
無鎖并發(fā)(樂觀鎖)
CAS:compare and save
-
CAS
變量必須用volatile修飾,不然不能保證獲得最新的
為什么無鎖效率比較高
上下文切換的損耗比較高。
無鎖情況下,即使重試失敗,線程始終在高速運(yùn)行,沒有停歇,而 synchronized 會讓線程在沒有獲得鎖的時候,發(fā)生上下文切換,進(jìn)入阻塞。打個比喻,線程就好像高速跑道上的賽車,高速運(yùn)行時,速度超快,一旦發(fā)生上下文切換,就好比賽車要減速、熄火,等被喚醒又得重新打火、啟動、加速... 恢復(fù)到高速運(yùn)行,代價比較大
但無鎖情況下,因為線程要保持運(yùn)行,需要額外 CPU 的支持,CPU 在這里就好比高速跑道,沒有額外的跑道,線程想高速運(yùn)行也無從談起,雖然不會進(jìn)入阻塞,但由于沒有分到時間片,仍然會進(jìn)入可運(yùn)行狀態(tài),還是會導(dǎo)致上下文切換。CAS特點(diǎn)
結(jié)合 CAS 和 volatile 可以實現(xiàn)無鎖并發(fā),適用于線程數(shù)少、多核 CPU 的場景下。
CAS 是基于樂觀鎖的思想:最樂觀的估計,不怕別的線程來修改共享變量,就算改了也沒關(guān)系,我吃虧點(diǎn)再重試唄。
synchronized 是基于悲觀鎖的思想:最悲觀的估計,得防著其它線程來修改共享變量,我上了鎖你們都別想改,我改完了解開鎖,你們才有機(jī)會。
CAS 體現(xiàn)的是無鎖并發(fā)、無阻塞并發(fā),請仔細(xì)體會這兩句話的意思
因為沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一
但如果競爭激烈,可以想到重試必然頻繁發(fā)生,反而效率會受影響不可變類
不可變類可以解決資源共享的問題-
內(nèi)存模型
-
可見性
-
有序性
指令優(yōu)化的時候會進(jìn)行重排,但是有些重排在多線程的情況下會出錯。
-
如何保證可見性
-
double-check locking
-
happens-before
享元模式flyway
wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects-
線程池繼承體系
Scheduled修飾的線程池表示這個線程池有定時執(zhí)行等功能。
ThreadPoolExecutor參數(shù)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize 核心線程數(shù)目 (最多保留的線程數(shù))
maximumPoolSize 最大線程數(shù)目
keepAliveTime 生存時間 - 針對救急線程
unit 時間單位 - 針對救急線程
workQueue 阻塞隊列
threadFactory 線程工廠 - 可以為線程創(chuàng)建時起個好名字
handler 拒絕策略
下面幾個是基于ThreadPoolExecutor的各種線程池
- newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心線程數(shù) == 最大線程數(shù)(沒有救急線程被創(chuàng)建),阻塞隊列是無界的,可以放任意數(shù)量的任務(wù),適用于任務(wù)量已知,相對耗時的任務(wù)。
- newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心線程數(shù)是0, 最大線程數(shù)是 Integer.MAX_VALUE,救急線程的空閑生存時間是 60s,意味著1. 全部都是救急線程(60s 后可以回收)2. 救急線程可以無限創(chuàng)建。
隊列采用了 SynchronousQueue 實現(xiàn)特點(diǎn)是,它沒有容量,沒有線程來取是放不進(jìn)去的(一手交錢、一手交貨)。但是在不超過最大線程數(shù)的情況下,每次都會新建新的線程。
這種對比newFixedThreadPool就是另外一種極端,沒有固定線程,每次需要多少就創(chuàng)建多少,不需要有一定容量的隊列來存儲任務(wù)。
- newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用場景:希望多個任務(wù)排隊執(zhí)行。線程數(shù)固定為 1,任務(wù)數(shù)多于 1 時,會放入無界隊列排隊。任務(wù)執(zhí)行完畢,這唯一的線程也不會被釋放。
區(qū)別:
自己創(chuàng)建一個單線程串行執(zhí)行任務(wù),如果任務(wù)執(zhí)行失敗而終止那么沒有任何補(bǔ)救措施,而線程池還會新建一個線程,保證池的正常工作
Executors.newSingleThreadExecutor() 線程個數(shù)始終為1,不能修改
FinalizableDelegatedExecutorService 應(yīng)用的是裝飾器模式,只對外暴露了 ExecutorService 接口,因此不能調(diào)用 ThreadPoolExecutor 中特有的方法。Executors.newFixedThreadPool(1) 初始時為1,以后還可以修改對外暴露的是 ThreadPoolExecutor 對象,可以強(qiáng)轉(zhuǎn)后調(diào)用 setCorePoolSize 等方法進(jìn)行修改。
- invoke和execute的區(qū)別
// 執(zhí)行任務(wù)
void execute(Runnable command);
// 提交任務(wù) task,用返回值 Future 獲得任務(wù)執(zhí)行結(jié)果
<T> Future<T> submit(Callable<T> task);
創(chuàng)建線程數(shù)量多少
簡單來說,CPU密集型要創(chuàng)建cpu 核數(shù) + 1個線程差不多,IO密集型要創(chuàng)建多一點(diǎn),因為IO密集型的線程經(jīng)常在IO,CPU占有率并不高,多一點(diǎn)CPU占有率才高。線程池任務(wù)異常
- 主動捕獲
- Future
- SynchronousQueue VS AbstractQueuedSynchronizer(AQS)
SynchronousQueue 同步隊列,放進(jìn)去的時候如果沒有人來取會進(jìn)行阻塞,如果放進(jìn)去已經(jīng)有人來取了,那就不會阻塞。
AbstractQueuedSynchronizer 簡單說就是同步工具的隊列
-
任務(wù)放棄策略
-
讀寫鎖的示例
-
synchronize和aqs的區(qū)別
-
CountDownLatch
- join跟CountdownLatch的區(qū)別
- join比較底層,CountdownLatch比較高層
- join必須等到線程結(jié)束的時候才可以,而CountdownLatch只需調(diào)用。
CopyOnWriteArrayList
CopyOnWriteArrayList 可以實現(xiàn)讀寫并發(fā),但是具有弱一致性,其他的并發(fā)容器一般只做到讀讀并發(fā)。并發(fā)高和一致性是矛盾的。-
線程安全類合集
圖片.png























