??作者簡介:大家好,我是愛發(fā)博客的嗯哼,愛好Java的小菜鳥
??如果感覺博主的文章還不錯的話,請??三連支持??一下博主哦
??社區(qū)論壇:希望大家能加入社區(qū)共同進步
?????個人博客:智慧筆記
文檔說明
在文檔中對所有的面試題都進行了難易程度和出現頻率的等級說明
星數越多代表權重越大,最多五顆星(☆☆☆☆☆) 最少一顆星(☆)
Java多線程相關面試題
1.線程的基礎知識
1.1 線程和進程的區(qū)別?
難易程度:☆☆
出現頻率:☆☆☆
程序由指令和數據組成,但這些指令要運行,數據要讀寫,就必須將指令加載至 CPU,數據加載至內存。在指令運行過程中還需要用到磁盤、網絡等設備。進程就是用來加載指令、管理內存、管理 IO 的。
當一個程序被運行,從磁盤加載這個程序的代碼至內存,這時就開啟了一個進程。
一個進程之內可以分為一到多個線程。
一個線程就是一個指令流,將指令流中的一條條指令以一定的順序交給 CPU 執(zhí)行
Java 中,線程作為最小調度單位,進程作為資源分配的最小單位。在 windows 中進程是不活動的,只是作為線程的容器
二者對比
- 進程是正在運行程序的實例,進程中包含了線程,每個線程執(zhí)行不同的任務
- 不同的進程使用不同的內存空間,在當前進程下的所有線程可以共享內存空間
- 線程更輕量,線程上下文切換成本一般上要比進程上下文切換低(上下文切換指的是從一個線程切換到另一個線程)
1.2 并行和并發(fā)有什么區(qū)別?
難易程度:☆
出現頻率:☆
單核CPU
-
單核CPU下線程實際還是串行執(zhí)行的
-
操作系統(tǒng)中有一個組件叫做任務調度器,將cpu的時間片(windows下時間片最小約為 15 毫秒)分給不同的程序使用,只是由于cpu在線程間(時間片很短)的切換非常快,人類感覺是同時運行的 。
-
總結為一句話就是: 微觀串行,宏觀并行
一般會將這種線程輪流使用CPU的做法稱為并發(fā)(concurrent)
多核CPU
每個核(core)都可以調度運行線程,這時候線程可以是并行的。
并發(fā)(concurrent)是同一時間應對(dealing with)多件事情的能力
并行(parallel)是同一時間動手做(doing)多件事情的能力
舉例:
家庭主婦做飯、打掃衛(wèi)生、給孩子喂奶,她一個人輪流交替做這多件事,這時就是并發(fā)
家庭主婦雇了個保姆,她們一起這些事,這時既有并發(fā),也有并行(這時會產生競爭,例如鍋只有一口,一個人用鍋時,另一個人就得等待)
雇了3個保姆,一個專做飯、一個專打掃衛(wèi)生、一個專喂奶,互不干擾,這時是并行
1.3 創(chuàng)建線程的四種方式
難易程度:☆☆
出現頻率:☆☆☆☆
參考回答:
共有四種方式可以創(chuàng)建線程,分別是:繼承Thread類、實現runnable接口、實現Callable接口、線程池創(chuàng)建線程
詳細創(chuàng)建方式參考下面代碼:
① 繼承Thread類
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread...run...");
}
public static void main(String[] args) {
// 創(chuàng)建MyThread對象
MyThread t1 = new MyThread() ;
MyThread t2 = new MyThread() ;
// 調用start方法啟動線程
t1.start();
t2.start();
}
}
② 實現runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 創(chuàng)建MyRunnable對象
MyRunnable mr = new MyRunnable() ;
// 創(chuàng)建Thread對象
Thread t1 = new Thread(mr) ;
Thread t2 = new Thread(mr) ;
// 調用start方法啟動線程
t1.start();
t2.start();
}
}
③ 實現Callable接口
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyCallable...call...");
return "OK";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 創(chuàng)建MyCallable對象
MyCallable mc = new MyCallable() ;
// 創(chuàng)建F
FutureTask<String> ft = new FutureTask<String>(mc) ;
// 創(chuàng)建Thread對象
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
// 調用start方法啟動線程
t1.start();
// 調用ft的get方法獲取執(zhí)行結果
String result = ft.get();
// 輸出
System.out.println(result);
}
}
④ 線程池創(chuàng)建線程
public class MyExecutors implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 創(chuàng)建線程池對象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors()) ;
// 關閉線程池
threadPool.shutdown();
}
}
1.4 runnable 和 callable 有什么區(qū)別
難易程度:☆☆
出現頻率:☆☆☆
參考回答:
- Runnable 接口run方法沒有返回值;Callable接口call方法有返回值,是個泛型,和Future、FutureTask配合可以用來獲取異步執(zhí)行的結果
- Callalbe接口支持返回執(zhí)行結果,需要調用FutureTask.get()得到,此方法會阻塞主進程的繼續(xù)往下執(zhí)行,如果不調用不會阻塞。
- Callable接口的call()方法允許拋出異常;而Runnable接口的run()方法的異常只能在內部消化,不能繼續(xù)上拋
1.5 線程的 run()和 start()有什么區(qū)別?
難易程度:☆☆
出現頻率:☆☆
start(): 用來啟動線程,通過該線程調用run方法執(zhí)行run方法中所定義的邏輯代碼。start方法只能被調用一次。
run(): 封裝了要被線程執(zhí)行的代碼,可以被調用多次。
1.6 線程包括哪些狀態(tài),狀態(tài)之間是如何變化的
難易程度:☆☆☆
出現頻率:☆☆☆☆
線程的狀態(tài)可以參考JDK中的Thread類中的枚舉State
public enum State {
/**
* 尚未啟動的線程的線程狀態(tài)
*/
NEW,
/**
* 可運行線程的線程狀態(tài)。處于可運行狀態(tài)的線程正在 Java 虛擬機中執(zhí)行,但它可能正在等待來自 * 操作系統(tǒng)的其他資源,例如處理器。
*/
RUNNABLE,
/**
* 線程阻塞等待監(jiān)視器鎖的線程狀態(tài)。處于阻塞狀態(tài)的線程正在等待監(jiān)視器鎖進入同步塊/方法或在調 * 用Object.wait后重新進入同步塊/方法。
*/
BLOCKED,
/**
* 等待線程的線程狀態(tài)。由于調用以下方法之一,線程處于等待狀態(tài):
* Object.wait沒有超時
* 沒有超時的Thread.join
* LockSupport.park
* 處于等待狀態(tài)的線程正在等待另一個線程執(zhí)行特定操作。
* 例如,一個對對象調用Object.wait()的線程正在等待另一個線程對該對象調用Object.notify() * 或Object.notifyAll() 。已調用Thread.join()的線程正在等待指定線程終止。
*/
WAITING,
/**
* 具有指定等待時間的等待線程的線程狀態(tài)。由于以指定的正等待時間調用以下方法之一,線程處于定 * 時等待狀態(tài):
* Thread.sleep
* Object.wait超時
* Thread.join超時
* LockSupport.parkNanos
* LockSupport.parkUntil
* </ul>
*/
TIMED_WAITING,
/**
* 已終止線程的線程狀態(tài)。線程已完成執(zhí)行
*/
TERMINATED;
}
狀態(tài)之間是如何變化的
分別是
- 新建
- 當一個線程對象被創(chuàng)建,但還未調用 start 方法時處于新建狀態(tài)
- 此時未與操作系統(tǒng)底層線程關聯
- 可運行
- 調用了 start 方法,就會由新建進入可運行
- 此時與底層線程關聯,由操作系統(tǒng)調度執(zhí)行
- 終結
- 線程內代碼已經執(zhí)行完畢,由可運行進入終結
- 此時會取消與底層線程關聯
- 阻塞
- 當獲取鎖失敗后,由可運行進入 Monitor 的阻塞隊列阻塞,此時不占用 cpu 時間
- 當持鎖線程釋放鎖時,會按照一定規(guī)則喚醒阻塞隊列中的阻塞線程,喚醒后的線程進入可運行狀態(tài)
- 等待
- 當獲取鎖成功后,但由于條件不滿足,調用了 wait() 方法,此時從可運行狀態(tài)釋放鎖進入 Monitor 等待集合等待,同樣不占用 cpu 時間
- 當其它持鎖線程調用 notify() 或 notifyAll() 方法,會按照一定規(guī)則喚醒等待集合中的等待線程,恢復為可運行狀態(tài)
- 有時限等待
- 當獲取鎖成功后,但由于條件不滿足,調用了 wait(long) 方法,此時從可運行狀態(tài)釋放鎖進入 Monitor 等待集合進行有時限等待,同樣不占用 cpu 時間
- 當其它持鎖線程調用 notify() 或 notifyAll() 方法,會按照一定規(guī)則喚醒等待集合中的有時限等待線程,恢復為可運行狀態(tài),并重新去競爭鎖
- 如果等待超時,也會從有時限等待狀態(tài)恢復為可運行狀態(tài),并重新去競爭鎖
- 還有一種情況是調用 sleep(long) 方法也會從可運行狀態(tài)進入有時限等待狀態(tài),但與 Monitor 無關,不需要主動喚醒,超時時間到自然恢復為可運行狀態(tài)
1.7 新建 T1、T2、T3 三個線程,如何保證它們按順序執(zhí)行?
難易程度:☆☆
出現頻率:☆☆☆
在多線程中有多種方法讓線程按特定順序執(zhí)行,你可以用線程類的join()方法在一個線程中啟動另一個線程,另外一個線程完成該線程繼續(xù)執(zhí)行。
代碼舉例:
為了確保三個線程的順序你應該先啟動最后一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最后完成
public class JoinTest {
public static void main(String[] args) {
// 創(chuàng)建線程對象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入線程t1,只有t1線程執(zhí)行完畢以后,再次執(zhí)行該線程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入線程t2,只有t2線程執(zhí)行完畢以后,再次執(zhí)行該線程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 啟動線程
t1.start();
t2.start();
t3.start();
}
}
1.8 notify()和 notifyAll()有什么區(qū)別?
難易程度:☆☆
出現頻率:☆☆
notifyAll:喚醒所有wait的線程
notify:只隨機喚醒一個 wait 線程
package com.itheima.basic;
public class WaitNotify {
static boolean flag = false;
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock){
while (!flag){
System.out.println(Thread.currentThread().getName()+"...wating...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...flag is true");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock){
while (!flag){
System.out.println(Thread.currentThread().getName()+"...wating...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...flag is true");
}
});
Thread t3 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " hold lock");
lock.notifyAll();
flag = true;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t3.start();
}
}
1.9 在 java 中 wait 和 sleep 方法的不同?
難易程度:☆☆☆
出現頻率:☆☆☆
參考回答:
共同點
- wait() ,wait(long) 和 sleep(long) 的效果都是讓當前線程暫時放棄 CPU 的使用權,進入阻塞狀態(tài)
不同點
-
方法歸屬不同
- sleep(long) 是 Thread 的靜態(tài)方法
- 而 wait(),wait(long) 都是 Object 的成員方法,每個對象都有
-
醒來時機不同
- 執(zhí)行 sleep(long) 和 wait(long) 的線程都會在等待相應毫秒后醒來
- wait(long) 和 wait() 還可以被 notify 喚醒,wait() 如果不喚醒就一直等下去
- 它們都可以被打斷喚醒
-
鎖特性不同(重點)
- wait 方法的調用必須先獲取 wait 對象的鎖,而 sleep 則無此限制
- wait 方法執(zhí)行后會釋放對象鎖,允許其它線程獲得該對象鎖(我放棄 cpu,但你們還可以用)
- 而 sleep 如果在 synchronized 代碼塊中執(zhí)行,并不會釋放對象鎖(我放棄 cpu,你們也用不了)
代碼示例:
public class WaitSleepCase {
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
sleeping();
}
private static void illegalWait() throws InterruptedException {
LOCK.wait();
}
private static void waiting() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
get("t").debug("waiting...");
LOCK.wait(5000L);
} catch (InterruptedException e) {
get("t").debug("interrupted...");
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(100);
synchronized (LOCK) {
main.debug("other...");
}
}
private static void sleeping() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
get("t").debug("sleeping...");
Thread.sleep(5000L);
} catch (InterruptedException e) {
get("t").debug("interrupted...");
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(100);
synchronized (LOCK) {
main.debug("other...");
}
}
}
1.10 如何停止一個正在運行的線程?
難易程度:☆☆
出現頻率:☆☆
參考回答:
有三種方式可以停止線程
- 使用退出標志,使線程正常退出,也就是當run方法完成后線程終止
- 使用stop方法強行終止(不推薦,方法已作廢)
- 使用interrupt方法中斷線程
代碼參考如下:
① 使用退出標志,使線程正常退出。
public class MyInterrupt1 extends Thread {
volatile boolean flag = false ; // 線程執(zhí)行的退出標記
@Override
public void run() {
while(!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建MyThread對象
MyInterrupt1 t1 = new MyInterrupt1() ;
t1.start();
// 主線程休眠6秒
Thread.sleep(6000);
// 更改標記為true
t1.flag = true ;
}
}
② 使用stop方法強行終止
public class MyInterrupt2 extends Thread {
volatile boolean flag = false ; // 線程執(zhí)行的退出標記
@Override
public void run() {
while(!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建MyThread對象
MyInterrupt2 t1 = new MyInterrupt2() ;
t1.start();
// 主線程休眠2秒
Thread.sleep(6000);
// 調用stop方法
t1.stop();
}
}
③ 使用interrupt方法中斷線程。
package com.itheima.basic;
public class MyInterrupt3 {
public static void main(String[] args) throws InterruptedException {
//1.打斷阻塞的線程
/*Thread t1 = new Thread(()->{
System.out.println("t1 正在運行...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
System.out.println(t1.isInterrupted());*/
//2.打斷正常的線程
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
System.out.println("打斷狀態(tài):"+interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
// t2.interrupt();
}
}
2.線程中并發(fā)鎖
2.1 講一下synchronized關鍵字的底層原理?
難易程度:☆☆☆☆☆
出現頻率:☆☆☆
2.1.1 基本使用
如下搶票的代碼,如果不加鎖,就會出現超賣或者一張票賣給多個人
Synchronized【對象鎖】采用互斥的方式讓同一時刻至多只有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住
public class TicketDemo {
static Object lock = new Object();
int ticketNum = 10;
public synchronized void getTicket() {
synchronized (this) {
if (ticketNum <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "搶到一張票,剩余:" + ticketNum);
// 非原子性操作
ticketNum--;
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
ticketDemo.getTicket();
}).start();
}
}
}
2.1.2 Monitor
Monitor 被翻譯為監(jiān)視器,是由jvm提供,c++語言實現
在代碼中想要體現monitor需要借助javap命令查看clsss的字節(jié)碼,比如以下代碼:
public class SyncTest {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
找到這個類的class文件,在class文件目錄下執(zhí)行javap -v SyncTest.class
,反編譯效果如下:
- monitorenter 上鎖開始的地方
- monitorexit 解鎖的地方
- 其中被monitorenter和monitorexit包圍住的指令就是上鎖的代碼
- 有兩個monitorexit的原因,第二個monitorexit是為了防止鎖住的代碼拋異常后不能及時釋放鎖
在使用了synchornized代碼塊時需要指定一個對象,所以synchornized也被稱為對象鎖
monitor主要就是跟這個對象產生關聯,如下圖
Monitor內部具體的存儲結構:
-
Owner:存儲當前獲取鎖的線程的,只能有一個線程可以獲取
-
EntryList:關聯沒有搶到鎖的線程,處于Blocked狀態(tài)的線程
-
WaitSet:關聯調用了wait方法的線程,處于Waiting狀態(tài)的線程
具體的流程:
- 代碼進入synchorized代碼塊,先讓lock(對象鎖)關聯的monitor,然后判斷Owner是否有線程持有
- 如果沒有線程持有,則讓當前線程持有,表示該線程獲取鎖成功
- 如果有線程持有,則讓當前線程進入entryList進行阻塞,如果Owner持有的線程已經釋放了鎖,在EntryList中的線程去競爭鎖的持有權(非公平)
- 如果代碼塊中調用了wait()方法,則會進去WaitSet中進行等待
參考回答:
-
Synchronized【對象鎖】采用互斥的方式讓同一時刻至多只有一個線程能持有【對象鎖】
-
它的底層由monitor實現的,monitor是jvm級別的對象( C++實現),線程獲得鎖需要使用對象(鎖)關聯monitor
-
在monitor內部有三個屬性,分別是owner、entrylist、waitset
-
其中owner是關聯的獲得鎖的線程,并且只能關聯一個線程;entrylist關聯的是處于阻塞狀態(tài)的線程;waitset關聯的是處于Waiting狀態(tài)的線程
2.2 synchronized關鍵字的底層原理-進階
Monitor實現的鎖屬于重量級鎖,你了解過鎖升級嗎?
-
Monitor實現的鎖屬于重量級鎖,里面涉及到了用戶態(tài)和內核態(tài)的切換、進程的上下文切換,成本較高,性能比較低。
-
在JDK 1.6引入了兩種新型鎖機制:偏向鎖和輕量級鎖,它們的引入是為了解決在沒有多線程競爭或基本沒有競爭的場景下因使用傳統(tǒng)鎖機制帶來的性能開銷問題。
2.2.1 對象的內存結構
在HotSpot虛擬機中,對象在內存中存儲的布局可分為3塊區(qū)域:對象頭(Header)、實例數據(Instance Data)和對齊填充
我們需要重點分析MarkWord對象頭
2.2.2 MarkWord
hashcode:25位的對象標識Hash碼
age:對象分代年齡占4位
biased_lock:偏向鎖標識,占1位 ,0表示沒有開始偏向鎖,1表示開啟了偏向鎖
thread:持有偏向鎖的線程ID,占23位
epoch:偏向時間戳,占2位
ptr_to_lock_record:輕量級鎖狀態(tài)下,指向棧中鎖記錄的指針,占30位
ptr_to_heavyweight_monitor:重量級鎖狀態(tài)下,指向對象監(jiān)視器Monitor的指針,占30位
我們可以通過lock的標識,來判斷是哪一種鎖的等級
- 后三位是001表示無鎖
- 后三位是101表示偏向鎖
- 后兩位是00表示輕量級鎖
- 后兩位是10表示重量級鎖
2.2.3 再說Monitor重量級鎖
每個 Java 對象都可以關聯一個 Monitor 對象,如果使用 synchronized 給對象上鎖(重量級)之后,該對象頭的Mark Word 中就被設置指向 Monitor 對象的指針
簡單說就是:每個對象的對象頭都可以設置monoitor的指針,讓對象與monitor產生關聯
2.2.4 輕量級鎖
在很多的情況下,在Java程序運行時,同步塊中的代碼都是不存在競爭的,不同的線程交替的執(zhí)行同步塊中的代碼。這種情況下,用重量級鎖是沒必要的。因此JVM引入了輕量級鎖的概念。
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步塊 A
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步塊 B
}
}
加鎖的流程
1.在線程棧中創(chuàng)建一個Lock Record,將其obj字段指向鎖對象。
2.通過CAS指令將Lock Record的地址存儲在對象頭的mark word中(數據進行交換),如果對象處于無鎖狀態(tài)則修改成功,代表該線程獲得了輕量級鎖。
3.如果是當前線程已經持有該鎖了,代表這是一次鎖重入。設置Lock Record第一部分為null,起到了一個重入計數器的作用。
4.如果CAS修改失敗,說明發(fā)生了競爭,需要膨脹為重量級鎖。
解鎖過程
1.遍歷線程棧,找到所有obj字段等于當前鎖對象的Lock Record。
2.如果Lock Record的Mark Word為null,代表這是一次重入,將obj設置為null后continue。
3.如果Lock Record的 Mark Word不為null,則利用CAS指令將對象頭的mark word恢復成為無鎖狀態(tài)。如果失敗則膨脹為重量級鎖。
2.2.5 偏向鎖
輕量級鎖在沒有競爭時(就自己這個線程),每次重入仍然需要執(zhí)行 CAS 操作。
Java 6 中引入了偏向鎖來做進一步優(yōu)化:只有第一次使用 CAS 將線程 ID 設置到對象的 Mark Word 頭,之后發(fā)現
這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。以后只要不發(fā)生競爭,這個對象就歸該線程所有
static final Object obj = new Object();
public static void m1() {
synchronized (obj) {
// 同步塊 A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步塊 B
m3();
}
}
public static void m3() {
synchronized (obj) {
}
}
加鎖的流程
1.在線程棧中創(chuàng)建一個Lock Record,將其obj字段指向鎖對象。
2.通過CAS指令將Lock Record的線程id存儲在對象頭的mark word中,同時也設置偏向鎖的標識為101,如果對象處于無鎖狀態(tài)則修改成功,代表該線程獲得了偏向鎖。
3.如果是當前線程已經持有該鎖了,代表這是一次鎖重入。設置Lock Record第一部分為null,起到了一個重入計數器的作用。與輕量級鎖不同的時,這里不會再次進行cas操作,只是判斷對象頭中的線程id是否是自己,因為缺少了cas操作,性能相對輕量級鎖更好一些
解鎖流程參考輕量級鎖
2.2.6 參考回答
Java中的synchronized有偏向鎖、輕量級鎖、重量級鎖三種形式,分別對應了鎖只被一個線程持有、不同線程交替持有鎖、多線程競爭鎖三種情況。
描述 | |
---|---|
重量級鎖 | 底層使用的Monitor實現,里面涉及到了用戶態(tài)和內核態(tài)的切換、進程的上下文切換,成本較高,性能比較低。 |
輕量級鎖 | 線程加鎖的時間是錯開的(也就是沒有競爭),可以使用輕量級鎖來優(yōu)化。輕量級修改了對象頭的鎖標志,相對重量級鎖性能提升很多。每次修改都是CAS操作,保證原子性 |
偏向鎖 | 一段很長的時間內都只被一個線程使用鎖,可以使用了偏向鎖,在第一次獲得鎖時,會有一個CAS操作,之后該線程再獲取鎖,只需要判斷mark word中是否是自己的線程id即可,而不是開銷相對較大的CAS命令 |
一旦鎖發(fā)生了競爭,都會升級為重量級鎖
2.3你談談 JMM(Java 內存模型)
難易程度:☆☆☆
出現頻率:☆☆☆
JMM(Java Memory Model)Java內存模型,是java虛擬機規(guī)范中所定義的一種內存模型。
Java內存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問規(guī)則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節(jié)。
特點:
-
所有的共享變量都存儲于主內存(計算機的RAM)這里所說的變量指的是實例變量和類變量。不包含局部變量,因為局部變量是線程私有的,因此不存在競爭問題。
-
每一個線程還存在自己的工作內存,線程的工作內存,保留了被線程使用的變量的工作副本。
-
線程對變量的所有的操作(讀,寫)都必須在工作內存中完成,而不能直接讀寫主內存中的變量,不同線程之間也不能直接訪問對方工作內存中的變量,線程間變量的值的傳遞需要通過主內存完成。
2.4 CAS 你知道嗎?
難易程度:☆☆☆
出現頻率:☆☆
2.4.1 概述及基本工作流程
CAS的全稱是: Compare And Swap(比較再交換),它體現的一種樂觀鎖的思想,在無鎖情況下保證線程操作共享數據的原子性。
在JUC( java.util.concurrent )包下實現的很多類都用到了CAS操作
-
AbstractQueuedSynchronizer(AQS框架)
-
AtomicXXX類
例子:
我們還是基于剛才學習過的JMM內存模型進行說明
- 線程1與線程2都從主內存中獲取變量int a = 100,同時放到各個線程的工作內存中
一個當前內存值V、舊的預期值A、即將更新的值B,當且僅當舊的預期值A和內存值V相同時,將內存值修改為B并返回true,否則什么都不做,并返回false。如果CAS操作失敗,通過自旋的方式等待并再次嘗試,直到成功
- 線程1操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 101 (a++)
- 線程1拿A的值與主內存V的值進行比較,判斷是否相等
- 如果相等,則把B的值101更新到主內存中
- 線程2操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 99(a–)
- 線程2拿A的值與主內存V的值進行比較,判斷是否相等(目前不相等,因為線程1已更新V的值99)
- 不相等,則線程2更新失敗
-
自旋鎖操作
-
因為沒有加鎖,所以線程不會陷入阻塞,效率較高
-
如果競爭激烈,重試頻繁發(fā)生,效率會受影響
-
需要不斷嘗試獲取共享內存V中最新的值,然后再在新的值的基礎上進行更新操作,如果失敗就繼續(xù)嘗試獲取新的值,直到更新成功
2.4.2 CAS 底層實現
CAS 底層依賴于一個 Unsafe 類來直接調用操作系統(tǒng)底層的 CAS 指令
都是native修飾的方法,由系統(tǒng)提供的接口執(zhí)行,并非java代碼實現,一般的思路也都是自旋鎖實現
在java中比較常見使用有很多,比如ReentrantLock和Atomic開頭的線程安全類,都調用了Unsafe中的方法
- ReentrantLock中的一段CAS代碼
2.4.3 樂觀鎖和悲觀鎖
-
CAS 是基于樂觀鎖的思想:最樂觀的估計,不怕別的線程來修改共享變量,就算改了也沒關系,我吃虧點再重試唄。
-
synchronized 是基于悲觀鎖的思想:最悲觀的估計,得防著其它線程來修改共享變量,我上了鎖你們都別想改,我改完了解開鎖,你們才有機會。
2.5 請談談你對 volatile 的理解
難易程度:☆☆☆
出現頻率:☆☆☆
一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
2.5.1 保證線程間的可見性
保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的,volatile關鍵字會強制將修改的值立即寫入主存。
一個典型的例子:永不停止的循環(huán)
package com.itheima.basic;
// 可見性例子
// -Xint
public class ForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
System.out.println("modify stop to true...");
}).start();
foo();
}
static void foo() {
int i = 0;
while (!stop) {
i++;
}
System.out.println("stopped... c:"+ i);
}
}
當執(zhí)行上述代碼的時候,發(fā)現foo()方法中的循環(huán)是結束不了的,也就說讀取不到共享變量的值結束循環(huán)。
主要是因為在JVM虛擬機中有一個JIT(即時編輯器)給代碼做了優(yōu)化。
上述代碼
while (!stop) { i++; }
在很短的時間內,這個代碼執(zhí)行的次數太多了,當達到了一個閾值,JIT就會優(yōu)化此代碼,如下:
while (true) { i++; }
當把代碼優(yōu)化成這樣子以后,及時
stop
變量改變?yōu)榱?code>false也依然停止不了循環(huán)
解決方案:
第一:
在程序運行的時候加入vm參數-Xint
表示禁用即時編輯器,不推薦,得不償失(其他程序還要使用)
第二:
在修飾stop
變量的時候加上volatile
,表示當前代碼禁用了即時編輯器,問題就可以解決,代碼如下:
static volatile boolean stop = false;
2.5.2 禁止進行指令重排序
用 volatile 修飾共享變量會在讀、寫共享變量時加入不同的屏障,阻止其他讀寫操作越過屏障,從而達到阻止重排序的效果
在去獲取上面的結果的時候,有可能會出現4種情況
情況一:先執(zhí)行actor2獲取結果—>0,0(正常)
情況二:先執(zhí)行actor1中的第一行代碼,然后執(zhí)行actor2獲取結果—>0,1(正常)
情況三:先執(zhí)行actor1中所有代碼,然后執(zhí)行actor2獲取結果—>1,1(正常)
情況四:先執(zhí)行actor1中第二行代碼,然后執(zhí)行actor2獲取結果—>1,0(發(fā)生了指令重排序,影響結果)
解決方案
在變量上添加volatile,禁止指令重排序,則可以解決問題
屏障添加的示意圖
- 寫操作加的屏障是阻止上方其它寫操作越過屏障排到volatile變量寫之下
- 讀操作加的屏障是阻止下方其它讀操作越過屏障排到volatile變量讀之上
其他補充
我們上面的解決方案是把volatile加在了int y這個變量上,我們能不能把它加在int x這個變量上呢?
下面代碼使用volatile修飾了x變量
屏障添加的示意圖
這樣顯然是不行的,主要是因為下面兩個原則:
- 寫操作加的屏障是阻止上方其它寫操作越過屏障排到volatile變量寫之下
- 讀操作加的屏障是阻止下方其它讀操作越過屏障排到volatile變量讀之上
所以,現在我們就可以總結一個volatile使用的小妙招:
- 寫變量讓volatile修飾的變量的在代碼最后位置
- 讀變量讓volatile修飾的變量的在代碼最開始位置
2.6 什么是AQS?
難易程度:☆☆☆
出現頻率:☆☆☆
2.6.1 概述
全稱是 AbstractQueuedSynchronizer,是阻塞式鎖和相關的同步器工具的框架,它是構建鎖或者其他同步組件的基礎框架
AQS與Synchronized的區(qū)別
synchronized | AQS |
---|---|
關鍵字,c++ 語言實現 | java 語言實現 |
悲觀鎖,自動釋放鎖 | 悲觀鎖,手動開啟和關閉 |
鎖競爭激烈都是重量級鎖,性能差 | 鎖競爭激烈的情況下,提供了多種解決方案 |
AQS常見的實現類
-
ReentrantLock 阻塞式鎖
-
Semaphore 信號量
-
CountDownLatch 倒計時鎖
2.6.2 工作機制
- 在AQS中維護了一個使用了volatile修飾的state屬性來表示資源的狀態(tài),0表示無鎖,1表示有鎖
- 提供了基于 FIFO 的等待隊列,類似于 Monitor 的 EntryList
- 條件變量來實現等待、喚醒機制,支持多個條件變量,類似于 Monitor 的 WaitSet
- 線程0來了以后,去嘗試修改state屬性,如果發(fā)現state屬性是0,就修改state狀態(tài)為1,表示線程0搶鎖成功
- 線程1和線程2也會先嘗試修改state屬性,發(fā)現state的值已經是1了,有其他線程持有鎖,它們都會到FIFO隊列中進行等待,
- FIFO是一個雙向隊列,head屬性表示頭結點,tail表示尾結點
如果多個線程共同去搶這個資源是如何保證原子性的呢?
在去修改state狀態(tài)的時候,使用的cas自旋鎖來保證原子性,確保只能有一個線程修改成功,修改失敗的線程將會進入FIFO隊列中等待
AQS是公平鎖嗎,還是非公平鎖?
-
新的線程與隊列中的線程共同來搶資源,是非公平鎖
-
新的線程到隊列中等待,只讓隊列中的head線程獲取鎖,是公平鎖
比較典型的AQS實現類ReentrantLock,它默認就是非公平鎖,新的線程與隊列中的線程共同來搶資源
2.5 ReentrantLock的實現原理
難易程度:☆☆☆☆
出現頻率:☆☆☆
2.5.1 概述
ReentrantLock翻譯過來是可重入鎖,相對于synchronized它具備以下特點:
-
可中斷
-
可以設置超時時間
-
可以設置公平鎖
-
支持多個條件變量
-
與synchronized一樣,都支持重入
2.5.2 實現原理
ReentrantLock主要利用CAS+AQS隊列來實現。它支持公平鎖和非公平鎖,兩者的實現類似
構造方法接受一個可選的公平參數(默認非公平鎖),當設置為true時,表示公平鎖,否則為非公平鎖。公平鎖的效率往往沒有非公平鎖的效率高,在許多線程訪問的情況下,公平鎖表現出較低的吞吐量。
查看ReentrantLock源碼中的構造方法:
提供了兩個構造方法,不帶參數的默認為非公平
如果使用帶參數的構造函數,并且傳的值為true,則是公平鎖
其中NonfairSync和FairSync這兩個類父類都是Sync
而Sync的父類是AQS,所以可以得出ReentrantLock底層主要實現就是基于AQS來實現的
工作流程
-
線程來搶鎖后使用cas的方式修改state狀態(tài),修改狀態(tài)成功為1,則讓exclusiveOwnerThread屬性指向當前線程,獲取鎖成功
-
假如修改狀態(tài)失敗,則會進入雙向隊列中等待,head指向雙向隊列頭部,tail指向雙向隊列尾部
-
當exclusiveOwnerThread為null的時候,則會喚醒在雙向隊列中等待的線程
-
公平鎖則體現在按照先后順序獲取鎖,非公平體現在不在排隊的線程也可以搶鎖
2.6 synchronized和Lock有什么區(qū)別 ?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆
參考回答
- 語法層面
- synchronized 是關鍵字,源碼在 jvm 中,用 c++ 語言實現
- Lock 是接口,源碼由 jdk 提供,用 java 語言實現
- 使用 synchronized 時,退出同步代碼塊鎖會自動釋放,而使用 Lock 時,需要手動調用 unlock 方法釋放鎖
- 功能層面
- 二者均屬于悲觀鎖、都具備基本的互斥、同步、鎖重入功能
- Lock 提供了許多 synchronized 不具備的功能,例如獲取等待狀態(tài)、公平鎖、可打斷、可超時、多條件變量
- Lock 有適合不同場景的實現,如 ReentrantLock, ReentrantReadWriteLock
- 性能層面
- 在沒有競爭時,synchronized 做了很多優(yōu)化,如偏向鎖、輕量級鎖,性能不賴
- 在競爭激烈時,Lock 的實現通常會提供更好的性能
2.7 死鎖產生的條件是什么?
難易程度:☆☆☆☆
出現頻率:☆☆☆
死鎖:一個線程需要同時獲取多把鎖,這時就容易發(fā)生死鎖
例如:
t1 線程獲得A對象鎖,接下來想獲取B對象的鎖
t2 線程獲得B對象鎖,接下來想獲取A對象的鎖
代碼如下:
package com.itheima.basic;
import static java.lang.Thread.sleep;
public class Deadlock {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
System.out.println("lock A");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B) {
System.out.println("lock B");
System.out.println("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
System.out.println("lock B");
try {
sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A) {
System.out.println("lock A");
System.out.println("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
控制臺輸出結果
此時程序并沒有結束,這種現象就是死鎖現象…線程t1持有A的鎖等待獲取B鎖,線程t2持有B的鎖等待獲取A的鎖。
2.8 如何進行死鎖診斷?
難易程度:☆☆☆
出現頻率:☆☆☆
當程序出現了死鎖現象,我們可以使用jdk自帶的工具:jps和 jstack
步驟如下:
第一:查看運行的線程
第二:使用jstack查看線程運行的情況,下圖是截圖的關鍵信息
運行命令:jstack -l 46032
其他解決工具,可視化工具
- jconsole
用于對jvm的內存,線程,類 的監(jiān)控,是一個基于 jmx 的 GUI 性能監(jiān)控工具
打開方式:java 安裝目錄 bin目錄下 直接啟動 jconsole.exe 就行
- VisualVM:故障處理工具
能夠監(jiān)控線程,內存情況,查看方法的CPU時間和內存中的對 象,已被GC的對象,反向查看分配的堆棧
打開方式:java 安裝目錄 bin目錄下 直接啟動 jvisualvm.exe就行
2.10 ConcurrentHashMap
難易程度:☆☆☆
出現頻率:☆☆☆☆
ConcurrentHashMap 是一種線程安全的高效Map集合
底層數據結構:
-
JDK1.7底層采用分段的數組+鏈表實現
-
JDK1.8 采用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹。
(1) JDK1.7中concurrentHashMap
數據結構
- 提供了一個segment數組,在初始化ConcurrentHashMap 的時候可以指定數組的長度,默認是16,一旦初始化之后中間不可擴容
- 在每個segment中都可以掛一個HashEntry數組,數組里面可以存儲具體的元素,HashEntry數組是可以擴容的
- 在HashEntry存儲的數組中存儲的元素,如果發(fā)生沖突,則可以掛單向鏈表
存儲流程
- 先去計算key的hash值,然后確定segment數組下標
- 再通過hash值確定hashEntry數組中的下標存儲數據
- 在進行操作數據的之前,會先判斷當前segment對應下標位置是否有線程進行操作,為了線程安全使用的是ReentrantLock進行加鎖,如果獲取鎖是被會使用cas自旋鎖進行嘗試
(2) JDK1.8中concurrentHashMap
在JDK1.8中,放棄了Segment臃腫的設計,數據結構跟HashMap的數據結構是一樣的:數組+紅黑樹+鏈表
采用 CAS + Synchronized來保證并發(fā)安全進行實現
-
CAS控制數組節(jié)點的添加
-
synchronized只鎖定當前鏈表或紅黑二叉樹的首節(jié)點,只要hash不沖突,就不會產生并發(fā)的問題 , 效率得到提升
2.11 導致并發(fā)程序出現問題的根本原因是什么
難易程度:☆☆☆
出現頻率:☆☆☆
Java并發(fā)編程三大特性
-
原子性
-
可見性
-
有序性
(1)原子性
一個線程在CPU中操作不可暫停,也不可中斷,要不執(zhí)行完成,要不不執(zhí)行
比如,如下代碼能保證原子性嗎?
以上代碼會出現超賣或者是一張票賣給同一個人,執(zhí)行并不是原子性的
解決方案:
1.synchronized:同步加鎖
2.JUC里面的lock:加鎖
(3)內存可見性
內存可見性:讓一個線程對共享變量的修改對另一個線程可見
比如,以下代碼不能保證內存可見性
解決方案:
-
synchronized
-
volatile(推薦)
-
LOCK
(3)有序性
指令重排:處理器為了提高程序運行效率,可能會對輸入代碼進行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結果和代碼順序執(zhí)行的結果是一致的
還是之前的例子,如下代碼:
解決方案:
- volatile
3.線程池
3.1 說一下線程池的核心參數(線程池的執(zhí)行原理知道嘛)
難易程度:☆☆☆
出現頻率:☆☆☆☆
線程池核心參數主要參考ThreadPoolExecutor這個類的7個參數的構造函數
-
corePoolSize 核心線程數目
-
maximumPoolSize 最大線程數目 = (核心線程+救急線程的最大數目)
-
keepAliveTime 生存時間 - 救急線程的生存時間,生存時間內沒有新任務,此線程資源會釋放
-
unit 時間單位 - 救急線程的生存時間單位,如秒、毫秒等
-
workQueue - 當沒有空閑核心線程時,新來任務會加入到此隊列排隊,隊列滿會創(chuàng)建救急線程執(zhí)行任務
-
threadFactory 線程工廠 - 可以定制線程對象的創(chuàng)建,例如設置線程名字、是否是守護線程等
-
handler 拒絕策略 - 當所有線程都在繁忙,workQueue 也放滿時,會觸發(fā)拒絕策略
工作流程
1,任務在提交的時候,首先判斷核心線程數是否已滿,如果沒有滿則直接添加到工作線程執(zhí)行
2,如果核心線程數滿了,則判斷阻塞隊列是否已滿,如果沒有滿,當前任務存入阻塞隊列
3,如果阻塞隊列也滿了,則判斷線程數是否小于最大線程數,如果滿足條件,則使用臨時線程執(zhí)行任務
如果核心或臨時線程執(zhí)行完成任務后會檢查阻塞隊列中是否有需要執(zhí)行的線程,如果有,則使用非核心線程執(zhí)行任務
4,如果所有線程都在忙著(核心線程+臨時線程),則走拒絕策略
拒絕策略:
1.AbortPolicy:直接拋出異常,默認策略;
2.CallerRunsPolicy:用調用者所在的線程來執(zhí)行任務;
3.DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,并執(zhí)行當前任務;
4.DiscardPolicy:直接丟棄任務;
參考代碼:
public class TestThreadPoolExecutor {
static class MyTask implements Runnable {
private final String name;
private final long duration;
public MyTask(String name) {
this(name, 0);
}
public MyTask(String name, long duration) {
this.name = name;
this.duration = duration;
}
@Override
public void run() {
try {
LoggerUtils.get("myThread").debug("running..." + this);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "MyTask(" + name + ")";
}
}
public static void main(String[] args) throws InterruptedException {
AtomicInteger c = new AtomicInteger(1);
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
3,
0,
TimeUnit.MILLISECONDS,
queue,
r -> new Thread(r, "myThread" + c.getAndIncrement()),
new ThreadPoolExecutor.AbortPolicy());
showState(queue, threadPool);
threadPool.submit(new MyTask("1", 3600000));
showState(queue, threadPool);
threadPool.submit(new MyTask("2", 3600000));
showState(queue, threadPool);
threadPool.submit(new MyTask("3"));
showState(queue, threadPool);
threadPool.submit(new MyTask("4"));
showState(queue, threadPool);
threadPool.submit(new MyTask("5",3600000));
showState(queue, threadPool);
threadPool.submit(new MyTask("6"));
showState(queue, threadPool);
}
private static void showState(ArrayBlockingQueue<Runnable> queue, ThreadPoolExecutor threadPool) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
List<Object> tasks = new ArrayList<>();
for (Runnable runnable : queue) {
try {
Field callable = FutureTask.class.getDeclaredField("callable");
callable.setAccessible(true);
Object adapter = callable.get(runnable);
Class<?> clazz = Class.forName("java.util.concurrent.Executors$RunnableAdapter");
Field task = clazz.getDeclaredField("task");
task.setAccessible(true);
Object o = task.get(adapter);
tasks.add(o);
} catch (Exception e) {
e.printStackTrace();
}
}
LoggerUtils.main.debug("pool size: {}, queue: {}", threadPool.getPoolSize(), tasks);
}
}
3.2 線程池中有哪些常見的阻塞隊列
難易程度:☆☆☆
出現頻率:☆☆☆
workQueue - 當沒有空閑核心線程時,新來任務會加入到此隊列排隊,隊列滿會創(chuàng)建救急線程執(zhí)行任務
比較常見的有4個,用的最多是ArrayBlockingQueue和LinkedBlockingQueue
1.ArrayBlockingQueue:基于數組結構的有界阻塞隊列,FIFO。
2.LinkedBlockingQueue:基于鏈表結構的有界阻塞隊列,FIFO。
3.DelayedWorkQueue :是一個優(yōu)先級隊列,它可以保證每次出隊的任務都是當前隊列中執(zhí)行時間最靠前的
4.SynchronousQueue:不存儲元素的阻塞隊列,每個插入操作都必須等待一個移出操作。
ArrayBlockingQueue的LinkedBlockingQueue區(qū)別
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默認無界,支持有界 | 強制有界 |
底層是鏈表 | 底層是數組 |
是懶惰的,創(chuàng)建節(jié)點的時候添加數據 | 提前初始化 Node 數組 |
入隊會生成新 Node | Node需要是提前創(chuàng)建好的 |
兩把鎖(頭尾) | 一把鎖 |
左邊是LinkedBlockingQueue加鎖的方式,右邊是ArrayBlockingQueue加鎖的方式
- LinkedBlockingQueue讀和寫各有一把鎖,性能相對較好
- ArrayBlockingQueue只有一把鎖,讀和寫公用,性能相對于LinkedBlockingQueue差一些
3.3 如何確定核心線程數
難易程度:☆☆☆☆
出現頻率:☆☆☆
在設置核心線程數之前,需要先熟悉一些執(zhí)行線程池執(zhí)行任務的類型
- IO密集型任務
一般來說:文件讀寫、DB讀寫、網絡請求等
推薦:核心線程數大小設置為2N+1 (N為計算機的CPU核數)
- CPU密集型任務
一般來說:計算型代碼、Bitmap轉換、Gson轉換等
推薦:核心線程數大小設置為N+1 (N為計算機的CPU核數)
java代碼查看CPU核數
參考回答:
① 高并發(fā)、任務執(zhí)行時間短 -->( CPU核數+1 ),減少線程上下文的切換
② 并發(fā)不高、任務執(zhí)行時間長
-
IO密集型的任務 --> (CPU核數 * 2 + 1)
-
計算密集型任務 --> ( CPU核數+1 )
③ 并發(fā)高、業(yè)務執(zhí)行時間長,解決這種類型任務的關鍵不在于線程池而在于整體架構的設計,看看這些業(yè)務里面某些數據是否能做緩存是第一步,增加服務器是第二步,至于線程池的設置,設置參考(2)
3.4 線程池的種類有哪些
難易程度:☆☆☆
出現頻率:☆☆☆
在java.util.concurrent.Executors類中提供了大量創(chuàng)建連接池的靜態(tài)方法,常見就有四種
-
創(chuàng)建使用固定線程數的線程池
-
核心線程數與最大線程數一樣,沒有救急線程
-
阻塞隊列是LinkedBlockingQueue,最大容量為Integer.MAX_VALUE
-
適用場景:適用于任務量已知,相對耗時的任務
-
案例:
public class FixedThreadPoolCase { static class FixedThreadDemo implements Runnable{ @Override public void run() { String name = Thread.currentThread().getName(); for (int i = 0; i < 2; i++) { System.out.println(name + ":" + i); } } } public static void main(String[] args) throws InterruptedException { //創(chuàng)建一個固定大小的線程池,核心線程數和最大線程數都是3 ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executorService.submit(new FixedThreadDemo()); Thread.sleep(10); } executorService.shutdown(); } }
-
-
單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任 務,保證所有任務按照指定順序(FIFO)執(zhí)行
-
核心線程數和最大線程數都是1
-
阻塞隊列是LinkedBlockingQueue,最大容量為Integer.MAX_VALUE
-
適用場景:適用于按照順序執(zhí)行的任務
-
案例:
public class NewSingleThreadCase { static int count = 0; static class Demo implements Runnable { @Override public void run() { count++; System.out.println(Thread.currentThread().getName() + ":" + count); } } public static void main(String[] args) throws InterruptedException { //單個線程池,核心線程數和最大線程數都是1 ExecutorService exec = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { exec.execute(new Demo()); Thread.sleep(5); } exec.shutdown(); } }
-
-
可緩存線程池
-
核心線程數為0
-
最大線程數是Integer.MAX_VALUE
-
阻塞隊列為SynchronousQueue:不存儲元素的阻塞隊列,每個插入操作都必須等待一個移出操作。
-
適用場景:適合任務數比較密集,但每個任務執(zhí)行時間較短的情況
-
案例:
public class CachedThreadPoolCase { static class Demo implements Runnable { @Override public void run() { String name = Thread.currentThread().getName(); try { //修改睡眠時間,模擬線程執(zhí)行需要花費的時間 Thread.sleep(100); System.out.println(name + "執(zhí)行完了"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { //創(chuàng)建一個緩存的線程,沒有核心線程數,最大線程數為Integer.MAX_VALUE ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { exec.execute(new Demo()); Thread.sleep(1); } exec.shutdown(); } }
-
-
提供了“延遲”和“周期執(zhí)行”功能的ThreadPoolExecutor。
-
適用場景:有定時和延遲執(zhí)行的任務
-
案例:
public class ScheduledThreadPoolCase { static class Task implements Runnable { @Override public void run() { try { String name = Thread.currentThread().getName(); System.out.println(name + ", 開始:" + new Date()); Thread.sleep(1000); System.out.println(name + ", 結束:" + new Date()); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { //按照周期執(zhí)行的線程池,核心線程數為2,最大線程數為Integer.MAX_VALUE ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2); System.out.println("程序開始:" + new Date()); /** * schedule 提交任務到線程池中 * 第一個參數:提交的任務 * 第二個參數:任務執(zhí)行的延遲時間 * 第三個參數:時間單位 */ scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS); scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS); scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS); Thread.sleep(5000); // 關閉線程池 scheduledThreadPool.shutdown(); } }
-
3.5 為什么不建議用Executors創(chuàng)建線程池
難易程度:☆☆☆
出現頻率:☆☆☆
參考阿里開發(fā)手冊《Java開發(fā)手冊-嵩山版》
4.線程使用場景問題
4.1 線程池使用場景CountDownLatch、Future(你們項目哪里用到了多線程)
難易程度:☆☆☆
出現頻率:☆☆☆☆
4.1.1 CountDownLatch
CountDownLatch(閉鎖/倒計時鎖)用來進行線程同步協(xié)作,等待所有線程完成倒計時(一個或者多個線程,等待其他多個線程完成某件事情之后才能執(zhí)行)
-
其中構造參數用來初始化等待計數值
-
await() 用來等待計數歸零
-
countDown() 用來讓計數減一
案例代碼:
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//初始化了一個倒計時鎖 參數為 3
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"-begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//count--
latch.countDown();
System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"-begin...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//count--
latch.countDown();
System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"-begin...");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//count--
latch.countDown();
System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
}).start();
String name = Thread.currentThread().getName();
System.out.println(name + "-waiting...");
//等待其他線程完成
latch.await();
System.out.println(name + "-wait end...");
}
}
4.1.2 案例一(es數據批量導入)
在我們項目上線之前,我們需要把數據庫中的數據一次性的同步到es索引庫中,但是當時的數據好像是1000萬左右,一次性讀取數據肯定不行(oom異常),當時我就想到可以使用線程池的方式導入,利用CountDownLatch來控制,就能避免一次性加載過多,防止內存溢出
整體流程就是通過CountDownLatch+線程池配合去執(zhí)行
詳細實現流程:
詳細實現代碼,請查看當天代碼
4.1.3 案例二(數據匯總)
在一個電商網站中,用戶下單之后,需要查詢數據,數據包含了三部分:訂單信息、包含的商品、物流信息;這三塊信息都在不同的微服務中進行實現的,我們如何完成這個業(yè)務呢?
詳細實現代碼,請查看當天代碼
-
在實際開發(fā)的過程中,難免需要調用多個接口來匯總數據,如果所有接口(或部分接口)的沒有依賴關系,就可以使用線程池+future來提升性能
-
報表匯總
4.1.4 案例二(異步調用)
在進行搜索的時候,需要保存用戶的搜索記錄,而搜索記錄不能影響用戶的正常搜索,我們通常會開啟一個線程去執(zhí)行歷史記錄的保存,在新開啟的線程在執(zhí)行的過程中,可以利用線程提交任務
4.1 如何控制某個方法允許并發(fā)訪問線程的數量?
難易程度:☆☆☆
出現頻率:☆☆
Semaphore [?s?m??f?r] 信號量,是JUC包下的一個工具類,我們可以通過其限制執(zhí)行的線程數量,達到限流的效果
當一個線程執(zhí)行時先通過其方法進行獲取許可操作,獲取到許可的線程繼續(xù)執(zhí)行業(yè)務邏輯,當線程執(zhí)行完成后進行釋放許可操作,未獲取達到許可的線程進行等待或者直接結束。
Semaphore兩個重要的方法
lsemaphore.acquire(): 請求一個信號量,這時候的信號量個數-1(一旦沒有可使用的信號量,也即信號量個數變?yōu)樨摂禃r,再次請求的時候就會阻塞,直到其他線程釋放了信號量)
lsemaphore.release():釋放一個信號量,此時信號量個數+1
線程任務類:
public class SemaphoreCase {
public static void main(String[] args) {
// 1. 創(chuàng)建 semaphore 對象
Semaphore semaphore = new Semaphore(3);
// 2. 10個線程同時運行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 3. 獲取許可
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end...");
} finally {
// 4. 釋放許可
semaphore.release();
}
}).start();
}
}
}
5.其他
5.1 談談你對ThreadLocal的理解
難易程度:☆☆☆
出現頻率:☆☆☆☆
5.1.1 概述
ThreadLocal是多線程中對于解決線程安全的一個操作類,它會為每個線程都分配一個獨立的線程副本從而解決了變量并發(fā)訪問沖突的問題。ThreadLocal 同時實現了線程內的資源共享
案例:使用JDBC操作數據庫時,會將每一個線程的Connection放入各自的ThreadLocal中,從而保證每個線程都在各自的 Connection 上進行數據庫的操作,避免A線程關閉了B線程的連接。
5.1.2 ThreadLocal基本使用
三個主要方法:
-
set(value) 設置值
-
get() 獲取值
-
remove() 清除值
public class ThreadLocalTest {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set("itcast");
print(name);
System.out.println(name + "-after remove : " + threadLocal.get());
}, "t1").start();
new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set("itheima");
print(name);
System.out.println(name + "-after remove : " + threadLocal.get());
}, "t2").start();
}
static void print(String str) {
//打印當前線程中本地內存中本地變量的值
System.out.println(str + " :" + threadLocal.get());
//清除本地內存中的本地變量
threadLocal.remove();
}
}
5.1.3 ThreadLocal的實現原理&源碼解析
ThreadLocal本質來說就是一個線程內部存儲類,從而讓多個線程只操作自己內部的值,從而實現線程數據隔離
在ThreadLocal中有一個內部類叫做ThreadLocalMap,類似于HashMap
ThreadLocalMap中有一個屬性table數組,這個是真正存儲數據的位置
set方法
get方法/remove方法
5.1.4 ThreadLocal-內存泄露問題
Java對象中的四種引用類型:強引用、軟引用、弱引用、虛引用
- 強引用:最為普通的引用方式,表示一個對象處于有用且必須的狀態(tài),如果一個對象具有強引用,則GC并不會回收它。即便堆中內存不足了,寧可出現OOM,也不會對其進行回收
- 弱引用:表示一個對象處于可能有用且非必須的狀態(tài)。在GC線程掃描內存區(qū)域時,一旦發(fā)現弱引用,就會回收到弱引用相關聯的對象。對于弱引用的回收,無關內存區(qū)域是否足夠,一旦發(fā)現則會被回收
每一個Thread維護一個ThreadLocalMap,在ThreadLocalMap中的Entry對象繼承了WeakReference。其中key為使用弱引用的ThreadLocal實例,value為線程變量的副本
在使用ThreadLocal的時候,強烈建議:務必手動remove
看完此文章可以看JUC相關面試題測試一下自己掌握情況文章來源:http://www.zghlxwxcb.cn/news/detail-684167.html
- 本文引自黑馬程序員Java面試寶典
往期文章推薦:文章來源地址http://www.zghlxwxcb.cn/news/detail-684167.html
- 緩存穿透、緩存擊穿和緩存雪崩
- 消息中間件相關面試題
- Java集合相關面試題
- Java集合詳解
- 微服務相關面試題
- redis相關面試題
- 圖解 Paxos 算法
- Spring相關面試題
到了這里,關于JUC詳解的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!