前言:
大家好,我是良辰丫,今天學習多線程最后一節(jié)內(nèi)容,我們主要去了解信號量,線程安全集合類,Hashtable與ConcurrentHashMap的區(qū)別,多線程常見的面試題,我們需要重點去掌握,??????
??個人主頁:良辰針不戳
??所屬專欄:javaEE初階
??勵志語句:生活也許會讓我們遍體鱗傷,但最終這些傷口會成為我們一輩子的財富。
??期待大家三連,關(guān)注,點贊,收藏。
??作者能力有限,可能也會出錯,歡迎大家指正。
??愿與君為伴,共探Java汪洋大海。
1. 信號量
信號量
,其實就是用來表示可用資源個數(shù),它的本質(zhì)其實是一個計數(shù)器.
- 申請一個資源我們可以稱為p操作.
- 釋放一個資源我們叫做v操作.
其實這和生活中的例子非常相似,有的汽車充電站會有這種計數(shù)器,進去充電,相當于申請了一個充電資源;充滿電斷開電源相當于釋放一個充電資源.
- 所謂的鎖其實也可以看做一個計數(shù)器,加鎖后,計數(shù)器為1,釋放鎖后計數(shù)器為0.
- 信號量是廣義的鎖,不光能管理非0即1的資源,也能管理多個資源.
如果計數(shù)器為0,繼續(xù)申請資源會進入阻塞狀態(tài).
- 創(chuàng)建 Semaphore 示例, 初始化為 4, 表示有 4 個可用資源.
- acquire 方法表示申請資源(P操作), release 方法表示釋放資源(V操作)
- 創(chuàng)建 20 個線程, 每個線程都嘗試申請資源, sleep 1秒之后, 釋放資源. 觀察程序的執(zhí)行效果.
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申請資源");
semaphore.acquire();
System.out.println("獲取到資源");
Thread.sleep(1000);
System.out.println("釋放資源了");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable);
t.start();
}
2. CountDownLatch
一種特別針對專有場景的組件.
同時等待 N 個任務(wù)執(zhí)行結(jié)束
- 就像一場比賽,我們可以約定最后一個人到達終點比賽才會結(jié)束.
- 下載一個大文件,為了提高效率,會分塊傳輸,只有文件全部傳過去文件才會結(jié)束傳輸(可以使用多線程).
- 構(gòu)造 CountDownLatch 實例, 初始化 10 表示有 10 個任務(wù)需要完成.
- 每個任務(wù)執(zhí)行完畢, 都調(diào)用 latch.countDown() . 在 CountDownLatch 內(nèi)部的計數(shù)器同時自減.
- 主線程中使用 latch.await,阻塞等待所有任務(wù)執(zhí)行完畢. 相當于計數(shù)器為 0 了.
public static int num;
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long)Math.random() * 10000);
System.out.println(num++);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
});
for (int i = 0; i < 10; i++) {
new Thread(t).start();
}
// 必須等到 10 人全部回來
latch.await();
System.out.println("比賽結(jié)束");
}
3. 一些常見面試題
1. 線程同步有哪些?
synchronized, ReentrantLock, Semaphore 等都可以用于線程同步.
2. 為什么有了 synchronized 還需要 juc 下的 lock?
以 juc 的 ReentrantLock 為例,
- synchronized 使用時不需要手動釋放鎖. ReentrantLock 使用時需要手動釋放. 使用起來更靈活.
- synchronized 在申請鎖失敗時, 會死等. ReentrantLock 可以通過 trylock 的方式等待一段時間就放棄.
- synchronized 是非公平鎖, ReentrantLock 默認是非公平鎖. 可以通過構(gòu)造方法傳入一個true 開啟公平鎖模式.
- synchronized 是通過 Object 的 wait / notify 實現(xiàn)等待-喚醒. 每次喚醒的是一個隨機等待的線程. ReentrantLock 搭配 Condition 類實現(xiàn)等待-喚醒, 可以更精確控制喚醒某個指定的線程.
3. AtomicInteger 的實現(xiàn)原理是什么?(前面文章有,可參考)
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
4. 信號量是什么?
- 信號量, 用來表示 “可用資源的個數(shù)”. 本質(zhì)上就是一個計數(shù)器.
- 使用信號量可以實現(xiàn) “共享鎖”, 比如某個資源允許 3 個線程同時使用, 那么就可以使用 P 操作作為加鎖, V 操作作為解鎖, 前三個線程的 P 操作都能順利返回, 后續(xù)線程再進行 P 操作就會阻塞等待, 直到前面的線程執(zhí)行了 V 操作.
5. 解釋一下 ThreadPoolExecutor 構(gòu)造方法的參數(shù)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- maximumPoolSize :線程池中能擁有最多線程數(shù).
- keepAliveTime :表示空閑線程的存活時間.
- TimeUnit unit :表示keepAliveTime的單位.
- corePoolSize :線程池中核心線程數(shù)的最大值.
- workQueue :用于緩存任務(wù)的阻塞隊列.
- threadFactory :指定創(chuàng)建線程的工廠.
- handler :表示當 workQueue 已滿,且池中的線程數(shù)達到 maximumPoolSize 時,線程池拒絕添加新任務(wù)時采取的策略。(線程池詳解)
4. 線程安全的集合類
4.1 多線程環(huán)境使用 ArrayList
原來的集合類, 大部分都不是線程安全的.
Vector, Stack, HashTable, 是線程安全的(不建議用), 其他的集合類不是線程安全的(如果多線程下進行使用,可能出現(xiàn)難以預料的問題).
需要多線程下使用這些東西,那么該怎么辦呢?
- 使用鎖,手動保證線程安全,多個線程去修改ArrayList此時可能出現(xiàn)問題,就可以給修改操作進行加鎖.
- 標準庫還提供了一些線程安全版本的集合類,如果需要使用ArrayList,可以使用Vector代替,但是這個關(guān)鍵方法都是帶有synchronized,這是太老的集合類,不建議大家使用.
- Collections.synchronizedList(new ArrayList);synchronizedList 是標準庫提供的一個基于 synchronized 進行線程同步的 List.,synchronizedList 的關(guān)鍵操作上都帶有 synchronized.使用這個殼可以套用你想用的集合類.
- CopyOnWriteArrayList
支持寫時拷貝集合類,線程安全是多個線程修改不同的變量(沒加鎖),修改的時候拷貝一份.如果是多線程讀,由于讀本身就是線程安全,就沒有事;如果此時有一個線程嘗試修改,就會觸發(fā)寫時拷貝;由于這樣的引用操作賦值,本身就是原子的,就可以保證線程安全,不用加鎖,也能完成修改.優(yōu)點
:在讀多寫少的場景下, 性能很高,不需要加鎖競爭.缺點
:占用內(nèi)存較多; 新寫的數(shù)據(jù)不能被第一時間讀到.
4.2 多線程環(huán)境使用隊列
- ArrayBlockingQueue
基于數(shù)組實現(xiàn)的阻塞隊列- LinkedBlockingQueue
基于鏈表實現(xiàn)的阻塞隊列- PriorityBlockingQueue
基于堆實現(xiàn)的帶優(yōu)先級的阻塞隊列- TransferQueue
最多只包含一個元素的阻塞隊列
4.3 多線程環(huán)境使用哈希表
HashMap本身就是線程不安全的
,因此在多線程情況下一般不用.
那么在多線程情況下我們可以使用哪些呢?
- Hashtable
- ConcurrentHashMap
4.3.1 Hashtable
只是簡單的把關(guān)鍵方法加上了 synchronized 關(guān)鍵字.相當于給this(對象本身)加鎖.
- 如果多線程訪問同一個 Hashtable 就會直接造成鎖沖突.
- size 屬性也是通過 synchronized 來控制同步, 也是比較慢的.
- 一旦觸發(fā)擴容, 就由該線程完成整個擴容過程. 這個過程會涉及到大量的元素拷貝, 效率會非常低.
- 一個HashTable只有一把鎖,兩個線程訪問它的任意數(shù)據(jù)都會出現(xiàn)鎖競爭.
4.3.2 ConcurrentHashMap
- 讀操作沒有加鎖(但是使用了 volatile 保證從內(nèi)存讀取結(jié)果), 只對寫操作進行加鎖. 加鎖的方式仍然是用 synchronized, 但是不是鎖整個對象, 而是 “鎖桶” (用每個鏈表的頭結(jié)點作為鎖對象), 大大降低了鎖沖突的概率.
- 充分利用 CAS 特性. 比如 size 屬性通過 CAS 來更新. 避免出現(xiàn)重量級鎖的情況.
- 優(yōu)化了擴容方式: 化整為零
①發(fā)現(xiàn)需要擴容的線程, 只需要創(chuàng)建一個新的數(shù)組, 同時只搬幾個元素過去.
②擴容期間, 新老數(shù)組同時存在.
③后續(xù)每個來操作 ConcurrentHashMap 的線程, 都會參與搬家的過程. 每個操作負責搬運一小部分元素.
④搬完最后一個元素再把老數(shù)組刪掉.
⑤這個期間, 插入只往新數(shù)組加.
⑥這個期間, 查找需要同時查新數(shù)組和老數(shù)組- ConcurrentHashMap中每個哈希桶都有一把鎖,只有兩個線程訪問的恰好是同一個哈希桶上的數(shù)據(jù)時才會出現(xiàn)鎖沖突.
4.3.3 Hashtable與ConcurrentHashMap的區(qū)別(重點)
- 加鎖粒度不同(觸發(fā)鎖沖突的頻率),HashTable是針對整個哈希表加鎖,任何的增刪查改操作都會觸發(fā)加鎖,也都有可能出現(xiàn)鎖競爭.(其實沒必要加鎖那么勤快,會嚴重降級效率)
- HashTable插入元素時,根據(jù)key計算hash值(數(shù)組下標),把這個新的元素掛到對應(yīng)的下標鏈表上.(HashMap鏈表太長的時候(注意是HashMap)還會把鏈表變成紅黑樹).
兩個線程插入兩個元素是否會出現(xiàn)線程安全問題?
兩個線程修改不同的變量不會出現(xiàn)線程安全;雖然沒有線程安全問題,但是由于鎖是加到this上,仍然會針對同一個鎖對象產(chǎn)生鎖競爭,產(chǎn)生阻塞等待.![]()
- ConcurrentHashMap中每個鏈表的頭結(jié)點作為一把鎖,每次進行操作都是針對鏈表的頭結(jié)點進行加鎖,操作不同的鏈表就是針對不同的鎖加鎖,這樣就不會產(chǎn)生鎖沖突.這樣就導致大部分加鎖操作實際是沒有鎖沖突的,此時這里加鎖操作的開銷就非常小了.
![]()
- 無鎖編程(升級機制),更充分的利用了CAS機制,比如獲取/更新元素的個數(shù),就可以直接使用CAS完成,不必加鎖.CAS也能保證線程安全,往往比鎖更高效,但是這個操作不經(jīng)常用,使用范圍沒有鎖那么廣泛.
- 優(yōu)化了擴容策略,對于HashTable,如果元素太多,就會涉及到擴容操作,出現(xiàn)負載因子就需要進行擴容操作.擴容需要申請內(nèi)存空間,搬運元素(把元素從舊的哈希表上刪除,插到新的哈希表上);但是如果元素非常多,搬運一次,成本非常高,這就會導致put操作非常卡頓. CocurrentHashmap策略,化整為零,并不會試圖一次性搬運所有的元素,每次只搬運一小部分.
put觸發(fā)擴容的時候,就會直接創(chuàng)建更大的內(nèi)存空間,一部分進行搬運(速度較快),此時相當于有兩份哈希表,插入元素的時候,直接在新表操作;刪除元素刪舊表的;查找的時候新舊表都查.
5. ConcurrentHashMap相關(guān)面試題
1. ConcurrentHashMap的讀是否要加鎖?
讀操作沒有加鎖. 目的是為了進一步降低鎖沖突的概率. 為了保證讀到剛修改的數(shù)據(jù), 搭配了volatile 關(guān)鍵字.
2. 介紹下 ConcurrentHashMap的鎖分段技術(shù)?
- 這個是 Java1.7 中采取的技術(shù). Java1.8 中已經(jīng)不再使用了. 簡單的說就是把若干個哈希桶分成一個"段" (Segment), 針對每個段分別加鎖. 目的也是為了降低鎖競爭的概率.
- 當兩個線程訪問的數(shù)據(jù)恰好在同一個段上的時候, 才觸發(fā)鎖競爭.
3. ConcurrentHashMap在jdk1.8做了哪些優(yōu)化?
- 取消了分段鎖, 直接給每個哈希桶(每個鏈表)分配了一個鎖(就是以每個鏈表的頭結(jié)點對象作為鎖對象).
- 將原來 數(shù)組 + 鏈表 的實現(xiàn)方式改進成 數(shù)組 + 鏈表 / 紅黑樹 的方式. 當鏈表較長的時候(大于等于8 個元素)就轉(zhuǎn)換成紅黑樹.
4. ) Hashtable和HashMap、ConcurrentHashMap 之間的區(qū)別?
- HashMap: 線程不安全. key 允許為 null.
- Hashtable: 線程安全. 使用 synchronized 鎖 Hashtable 對象, 效率較低. key 不允許為 null.
- ConcurrentHashMap: 線程安全. 使用 synchronized 鎖每個鏈表頭結(jié)點, 鎖沖突概率低, 充分利用CAS 機制. 優(yōu)化了擴容方式. key 不允許為 null.
看到這里,我們的多線程知識點就要進入尾聲了,接下來我們總結(jié)幾個多線程和鎖常見的面試考點.
6. 多線程常見面試題
1. 談?wù)?volatile關(guān)鍵字的用法?
volatile 能夠保證內(nèi)存可見性. 強制從主內(nèi)存中讀取數(shù)據(jù). 此時如果有其他線程修改被 volatile 修飾的變量, 可以第一時間讀取到最新的值.
2. Java多線程是如何實現(xiàn)數(shù)據(jù)共享的?
JVM 把內(nèi)存分成了這幾個區(qū)域:方法區(qū), 堆區(qū), 棧區(qū), 程序計數(shù)器.
其中堆區(qū)這個內(nèi)存區(qū)域是多個線程之間共享的.
只要把某個數(shù)據(jù)放到堆內(nèi)存中,可以讓多個線程都訪問到.
3. Java創(chuàng)建線程池的接口是什么?參數(shù) LinkedBlockingQueue 的作用是什么?
創(chuàng)建線程池主要有兩種方式(需要掌握):
- 通過 Executors 工廠類創(chuàng)建. 創(chuàng)建方式比較簡單, 但是定制能力有限.
- 通過 ThreadPoolExecutor 創(chuàng)建. 創(chuàng)建方式比較復雜, 但是定制能力強.
LinkedBlockingQueue
表示線程池的任務(wù)隊列. 用戶通過 submit / execute 向這個任務(wù)隊列中添加任務(wù), 再由線程池中的工作線程來執(zhí)行任務(wù).
4. Java線程共有幾種狀態(tài)?狀態(tài)之間怎么切換的?
- NEW: 安排了工作, 還未開始行動. 新創(chuàng)建的線程, 還沒有調(diào)用 start 方法時處在這個狀態(tài).
- RUNNABLE: 可工作的. 又可以分成正在工作中和即將開始工作. 調(diào)用 start 方法之后, 并正在CPU 上運行/在即將準備運行 的狀態(tài).
- BLOCKED: 使用 synchronized 的時候, 如果鎖被其他線程占用, 就會阻塞等待, 從而進入該狀態(tài).
- WAITING: 調(diào)用 wait 方法會進入該狀態(tài).
- TIMED_WAITING: 調(diào)用 sleep 方法或者 wait(超時時間) 會進入該狀態(tài).
- TERMINATED: 工作完成了. 當線程 run 方法執(zhí)行完畢后, 會處于這個狀態(tài).
5. 在多線程下,如果對一個數(shù)進行疊加,該怎么做?
- 使用 synchronized / ReentrantLock 加鎖
- 使用 AtomInteger 原子操作
6. Servlet是否是線程安全的?
Servlet 本身是工作在多線程環(huán)境下. 如果在 Servlet 中創(chuàng)建了某個成員變量, 此時如果有多個請求到達服務(wù)器, 服務(wù)器就會多線程進行操作, 是可能出現(xiàn)線程不安全的情況的.
7. Thread和Runnable的區(qū)別和聯(lián)系?
- Thread 類描述了一個線程.
- Runnable 描述了一個任務(wù).
- 在創(chuàng)建線程的時候需要指定線程完成的任務(wù), 可以直接重寫 Thread 的 run 方法, 也可以使用Runnable 來描述這個任務(wù)
8. 多次start一個線程會怎么樣?
第一次調(diào)用 start 可以成功調(diào)用. 后續(xù)再調(diào)用 start 會拋出 java.lang.IllegalThreadStateException 異常.
9. 有synchronized兩個方法,兩個線程分別同時調(diào)用這個方法,會發(fā)生什么呢?
synchronized 加在非靜態(tài)方法上, 相當于針對當前對象加鎖.文章來源:http://www.zghlxwxcb.cn/news/detail-426863.html
- 如果這兩個方法屬于同一個實例:
線程1 能夠獲取到鎖, 并執(zhí)行方法. 線程2 會阻塞等待, 直到線程1 執(zhí)行完畢, 釋放鎖, 線程2 獲取到鎖之后才能執(zhí)行方法內(nèi)容.- 如果這兩個方法屬于不同實例:兩者能并發(fā)執(zhí)行, 互不干擾
10.線程與進程的區(qū)別?文章來源地址http://www.zghlxwxcb.cn/news/detail-426863.html
- 進程是包含線程的. 每個進程至少有一個線程存在,即主線程。
- 進程和進程之間不共享內(nèi)存空間. 同一個進程的線程之間共享同一個內(nèi)存空間.
- 進程是系統(tǒng)分配資源的最小單位,線程是系統(tǒng)調(diào)度的最小單位。
到了這里,關(guān)于【多線程進階】信號量,線程安全集合類,Hashtable與ConcurrentHashMap的區(qū)別,多線程常見的面試題的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!