新版Java面試專題視頻教程——多線程篇②
- 0. 問題匯總
- 0.1 線程的基礎知識
- 0.2 線程中并發安全
- 0.3 線程池
- 0.4 使用場景
- 1.線程的基礎知識
- 2.線程中并發鎖
- 3.線程池
- 3.1 說一下線程池的核心參數(線程池的執行原理知道嘛)
- 3.2 線程池中有哪些常見的阻塞隊列
- ArrayBlockingQueue的LinkedBlockingQueue區別
- 3.3 如何確定核心線程數
- 3.4 線程池的種類有哪些
- 3.5 為什么不建議用Executors創建線程池
- 4.線程使用場景問題
- 4.1 線程池使用場景CountDownLatch、Future(你們項目哪里用到了多線程)
- 4.1.1 CountDownLatch
- 4.1.2 案例一(es數據批量導入)
- 4.1.3 案例二(數據匯總)
- 4.1.4 案例二(異步調用)
- 4.2 如何控制某個方法允許并發訪問線程的數量?
- Semaphore兩個重要的方法
- 5.其他
- 5.1 談談你對ThreadLocal的理解
- 5.1.1 概述
- 5.1.2 ThreadLocal基本使用
- 三個主要方法:
- 5.1.3 ThreadLocal的實現原理&源碼解析
- 5.1.4 ThreadLocal-內存泄露問題
- 強引用、軟引用、弱引用的區別和解析
- 內存泄漏問題
- 6 真實面試還原
- 6.1 線程的基礎知識
- 6.2 線程中并發鎖
- 6.3 線程池
- 6.4 線程使用場景問題
- 6.5 其他
0. 問題匯總
0.1 線程的基礎知識
線程與進程的區別
并行與并發的區別
線程創建的方式有哪些
runnable和callable有什么區別
線程包括哪些狀態
狀態之間是如何變化的
在java中wait和sleep方法的不同
新建三個線程,如何保證它們按順序執行
notify和notifyAll有什么區別
線程的run()和start()有什么區別
如何停止一個正在運行的線程
0.2 線程中并發安全
synchronized關鍵字的底層原理
你談談JMM (Java 內存模型)
CAS你知道嗎
什么是AQS
ReentrantLock的實現原理
synchronized和Lock有什么區別
死鎖產生的條件是什么
如何進行死鎖診斷
請談談你對volatile的理解
聊一下ConcurrentHashMap
導致并發程序出現問題的根本原因是什么
0.3 線程池
說一下線程池的核心參數(線程池的執行原理知道嘛)
線程池中有哪些常見的阻塞隊列
如何確定核心線程數
線程池的種類有哪些
為什么不建議用Executors創建線程池
0.4 使用場景
線程池使用場景(你們項目中哪墜用到了線程池)
如何控制某個方法允許并發訪問線程的數量
談談你對ThreadLocal的理解
1.線程的基礎知識
新版Java面試專題視頻教程——多線程篇①
2.線程中并發鎖
新版Java面試專題視頻教程——多線程篇①
3.線程池
3.1 說一下線程池的核心參數(線程池的執行原理知道嘛)
難易程度:☆☆☆
出現頻率:☆☆☆☆
線程池核心參數主要參考ThreadPoolExecutor這個類的7個參數的構造函數
-
corePoolSize
核心線程數目 -
maximumPoolSize
最大線程數目 = (核心線程+救急線程的最大數目) -
keepAliveTime
生存時間 - 救急線程的生存時間,生存時間內沒有新任務,此線程資源會釋放 -
unit
時間單位 - 救急線程的生存時間單位,如秒、毫秒等 -
workQueue
阻塞隊列 - 當沒有空閑核心線程時,新來任務會加入到此隊列排隊,隊列滿會創建救急線程執行任務 -
threadFactory
線程工廠 - 可以定制線程對象的創建,例如設置線程名字、是否是守護線程等 -
handler
拒絕策略 - 當所有線程都在繁忙,workQueue
也放滿時,會觸發拒絕策略
工作流程
1,任務在提交的時候,首先判斷核心線程數是否已滿,如果沒有滿則直接添加到工作線程執行
2,如果核心線程數滿了,則判斷阻塞隊列是否已滿,如果沒有滿,當前任務存入阻塞隊列
3,如果阻塞隊列也滿了,則判斷線程數是否小于最大線程數,如果滿足條件,則使用臨時線程執行任務
如果核心或臨時線程執行完成任務后會檢查阻塞隊列中是否有需要執行的線程,如果有,則使用非核心線程執行任務
4,如果所有線程都在忙著(核心線程+臨時線程),則走拒絕策略
拒絕策略:
1.AbortPolicy:直接拋出異常,默認策略;
2.CallerRunsPolicy:用調用者所在的線程來執行任務;
3.DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,并執行當前任務;
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;}@Overridepublic void run() {try {LoggerUtils.get("myThread").debug("running..." + this);Thread.sleep(duration);} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic 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 - 當沒有空閑核心線程時,新來任務會加入到此隊列排隊,隊列滿會創建救急線程執行任務
比較常見的有4個,用的最多是ArrayBlockingQueue和LinkedBlockingQueue
- ArrayBlockingQueue:基于數組結構的有界(可指定容量大小)阻塞隊列,FIFO(先進先出)。
- LinkedBlockingQueue:基于鏈表結構的有界阻塞隊列,FIFO。
- DelayedWorkQueue :是一個優先級隊列,它可以保證每次出隊的任務都是當前隊列中執行時間最靠前的
- SynchronousQueue:不存儲元素的阻塞隊列,每個插入操作都必須等待一個移出操作。
ArrayBlockingQueue的LinkedBlockingQueue區別
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默認無界,支持有界 | 強制有界 |
底層是鏈表 | 底層是數組 |
是懶惰的,創建節點的時候添加數據 | 提前初始化Node數組 |
入隊會生成新Node | Node需要是提前創建好的 |
兩把鎖(頭尾) | 一把鎖 |
左邊是LinkedBlockingQueue加鎖的方式,右邊是ArrayBlockingQueue加鎖的方式
- LinkedBlockingQueue讀和寫各有一把鎖,性能相對較好
- ArrayBlockingQueue只有一把鎖,讀和寫公用,性能相對于LinkedBlockingQueue差一些
public class FixedThreadPoolCase {static class FixedThreadDemo implements Runnable{@Overridepublic 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 {//創建一個固定大小的線程池,核心線程數和最大線程數都是3ExecutorService executorService = Executors.newFixedThreadPool(3);for (int i = 0; i < 5; i++) {executorService.submit(new FixedThreadDemo());Thread.sleep(10);}executorService.shutdown();}}
3.3 如何確定核心線程數
難易程度:☆☆☆☆
出現頻率:☆☆☆
在設置核心線程數之前,需要先熟悉一些執行線程池執行任務的類型
- IO密集型任務
一般來說:文件讀寫、DB讀寫、網絡請求等
推薦:核心線程數大小設置為2N+1 (N為計算機的CPU核數)
- CPU密集型任務
一般來說:計算型代碼、Bitmap轉換、Gson轉換等
推薦:核心線程數大小設置為N+1 (N為計算機的CPU核數)
java代碼查看CPU核數
參考回答:
① 高并發、任務執行時間短 -->( CPU核數+1 ),減少線程上下文的切換
② 并發不高、任務執行時間長
- IO密集型的任務 --> (CPU核數 * 2 + 1)
- 計算密集型任務 --> ( CPU核數+1 )
③ 并發高、業務執行時間長,解決這種類型任務的關鍵不在于線程池而在于整體架構的設計,看看這些業務里面某些數據是否能做緩存是第一步,增加服務器是第二步,至于線程池的設置,設置參考(2)
3.4 線程池的種類有哪些
難易程度:☆☆☆
出現頻率:☆☆☆
在java.util.concurrent.Executors類中提供了大量創建連接池的靜態方法,常見就有四種
- 創建使用固定線程數的線程池
- 核心線程數與最大線程數一樣,沒有救急線程 那0L和unit就沒意義了
- 阻塞隊列是LinkedBlockingQueue,最大容量Integer.MAX_VALUE
- 適用場景:適用于任務量已知,相對耗時的任務
- 案例:
public class FixedThreadPoolCase {static class FixedThreadDemo implements Runnable{@Overridepublic 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 {//創建一個固定大小的線程池,核心線程數和最大線程數都是3ExecutorService executorService = Executors.newFixedThreadPool(3);for (int i = 0; i < 5; i++) {executorService.submit(new FixedThreadDemo());Thread.sleep(10);}executorService.shutdown();}
}
- 單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO)執行
- 核心線程數和最大線程數都是1
- 阻塞隊列是LinkedBlockingQueue,最大容量為Integer.MAX_VALUE
- 適用場景:適用于按照順序執行的任務
- 案例:
public class NewSingleThreadCase {static int count = 0;static class Demo implements Runnable {@Overridepublic void run() {count++;System.out.println(Thread.currentThread().getName() + ":" + count);}}public static void main(String[] args) throws InterruptedException {//單個線程池,核心線程數和最大線程數都是1ExecutorService exec = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++) {exec.execute(new Demo());Thread.sleep(5);}exec.shutdown();}
}
- 可緩存線程池
- 核心線程數為0
- 最大線程數是Integer.MAX_VALUE
- 阻塞隊列為SynchronousQueue:不存儲元素的阻塞隊列,每個插入操作都必須等待一個移出操作。
- 適用場景:適合任務數比較密集,但每個任務執行時間較短的情況
- 案例:
public class CachedThreadPoolCase {static class Demo implements Runnable {@Overridepublic void run() {String name = Thread.currentThread().getName();try {//修改睡眠時間,模擬線程執行需要花費的時間Thread.sleep(100);System.out.println(name + "執行完了");} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {//創建一個緩存的線程,沒有核心線程數,最大線程數為Integer.MAX_VALUEExecutorService exec = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {exec.execute(new Demo());Thread.sleep(1);}exec.shutdown();}
}
- 提供了“延遲”和“周期執行”功能的ThreadPoolExecutor。
- 適用場景:有定時和延遲執行的任務
- 案例
public class ScheduledThreadPoolCase {static class Task implements Runnable {@Overridepublic 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 {//按照周期執行的線程池,核心線程數為2,最大線程數為Integer.MAX_VALUEScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);System.out.println("程序開始:" + new Date());/*** schedule 提交任務到線程池中* 第一個參數:提交的任務* 第二個參數:任務執行的延遲時間* 第三個參數:時間單位*/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創建線程池
難易程度:☆☆☆
出現頻率:☆☆☆
參考阿里開發手冊《Java開發手冊-嵩山版》
4.線程使用場景問題
4.1 線程池使用場景CountDownLatch、Future(你們項目哪里用到了多線程)
難易程度:☆☆☆
出現頻率:☆☆☆☆
4.1.1 CountDownLatch
CountDownLatch(閉鎖/倒計時鎖)用來進行線程同步協作,等待所有線程完成倒計時(一個或者多個線程,等待其他多個線程完成某件事情之后才能執行)
- 其中構造參數用來初始化等待計數值
- await() 用來等待計數歸零
- countDown() 用來讓計數減一
案例代碼:
public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {//初始化了一個倒計時鎖 參數為 3CountDownLatch 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+線程池配合去執行
詳細實現流程:
詳細實現代碼,請查看當天代碼
package com.itheima.cdl.service.impl;import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;@Service
@Transactional
@Slf4j
public class ApArticleServiceImpl implements ApArticleService {@Autowiredprivate ApArticleMapper apArticleMapper;@Autowiredprivate RestHighLevelClient client;@Autowiredprivate ExecutorService executorService;private static final String ARTICLE_ES_INDEX = "app_info_article";private static final int PAGE_SIZE = 2000;/*** 批量導入*/@SneakyThrows@Overridepublic void importAll() {//總條數int count = apArticleMapper.selectCount();//總頁數int totalPageSize = count % PAGE_SIZE == 0 ? count / PAGE_SIZE : count / PAGE_SIZE + 1;//開始執行時間long startTime = System.currentTimeMillis();//一共有多少頁,就創建多少個CountDownLatch的計數CountDownLatch countDownLatch = new CountDownLatch(totalPageSize);int fromIndex;List<SearchArticleVo> articleList = null;for (int i = 0; i < totalPageSize; i++) {//起始分頁條數fromIndex = i * PAGE_SIZE;//查詢文章articleList = apArticleMapper.loadArticleList(fromIndex, PAGE_SIZE);//創建線程,做批量插入es數據操作TaskThread taskThread = new TaskThread(articleList, countDownLatch);//執行線程executorService.execute(taskThread);}//調用await()方法,用來等待計數歸零countDownLatch.await();long endTime = System.currentTimeMillis();log.info("es索引數據批量導入共:{}條,共消耗時間:{}秒", count, (endTime - startTime) / 1000);}class TaskThread implements Runnable {List<SearchArticleVo> articleList;CountDownLatch cdl;public TaskThread(List<SearchArticleVo> articleList, CountDownLatch cdl) {this.articleList = articleList;this.cdl = cdl;}@SneakyThrows@Overridepublic void run() {//批量導入BulkRequest bulkRequest = new BulkRequest(ARTICLE_ES_INDEX);for (SearchArticleVo searchArticleVo : articleList) {bulkRequest.add(new IndexRequest().id(searchArticleVo.getId().toString()).source(JSON.toJSONString(searchArticleVo), XContentType.JSON));}//發送請求,批量添加數據到es索引庫中client.bulk(bulkRequest, RequestOptions.DEFAULT);//讓計數減一cdl.countDown();}}
}
4.1.3 案例二(數據匯總)
在一個電商網站中,用戶下單之后,需要查詢數據,數據包含了三部分:訂單信息、包含的商品、物流信息;這三塊信息都在不同的微服務中進行實現的,我們如何完成這個業務呢?
MQ跟多線程異步改寫的區別:MQ主要解決跨進程之間的消息同步問題,將其改寫成了異步 側重于服務間通訊 而多線程主要解決的是當前進程快速響應
package com.itheima.cdl.controller;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;@RestController
@RequestMapping("/order_detail")
@Slf4j
public class OrderDetailController {@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate ExecutorService executorService;@SneakyThrows@GetMapping("/get/detail_new/{id}")public Map<String, Object> getOrderDetailNew() {long startTime = System.currentTimeMillis();Future<Map<String, Object>> f1 = executorService.submit(() -> {Map<String, Object> r =restTemplate.getForObject("http://localhost:9991/order/get/{id}", Map.class, 1);return r;});Future<Map<String, Object>> f2 = executorService.submit(() -> {Map<String, Object> r =restTemplate.getForObject("http://localhost:9991/product/get/{id}", Map.class, 1);return r;});Future<Map<String, Object>> f3 = executorService.submit(() -> {Map<String, Object> r =restTemplate.getForObject("http://localhost:9991/logistics/get/{id}", Map.class, 1);return r;});Map<String, Object> resultMap = new HashMap<>();resultMap.put("order", f1.get());resultMap.put("product", f2.get());resultMap.put("logistics", f3.get());long endTime = System.currentTimeMillis();log.info("接口調用共耗時:{}毫秒",endTime-startTime);return resultMap;}@SneakyThrows@GetMapping("/get/detail/{id}")public Map<String, Object> getOrderDetail() {long startTime = System.currentTimeMillis();Map<String, Object> order = restTemplate.getForObject("http://localhost:9991/order/get/{id}", Map.class, 1);Map<String, Object> product = restTemplate.getForObject("http://localhost:9991/product/get/{id}", Map.class, 1);Map<String, Object> logistics = restTemplate.getForObject("http://localhost:9991/logistics/get/{id}", Map.class, 1);long endTime = System.currentTimeMillis();Map<String, Object> resultMap = new HashMap<>();resultMap.put("order", order);resultMap.put("product", product);resultMap.put("logistics", logistics);log.info("接口調用共耗時:{}毫秒",endTime-startTime);return resultMap;}
}
- 在實際開發的過程中,難免需要調用多個接口來匯總數據,如果所有接口(或部分接口)的沒有依賴關系,就可以使用線程池+future來提升性能
- 報表匯總
4.1.4 案例二(異步調用)
在進行搜索的時候,需要保存用戶的搜索記錄,而搜索記錄不能影響用戶的正常搜索,我們通常會開啟一個線程去執行歷史記錄的保存,在新開啟的線程在執行的過程中,可以利用線程提交任務
package com.itheima.cdl.service.impl;import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {@Autowiredprivate RestHighLevelClient client;private static final String ARTICLE_ES_INDEX = "app_info_article";private int userId = 1102;@Autowiredprivate ApUserSearchService apUserSearchService;/*** 文章搜索* @return*/@Overridepublic List<Map> search(String keyword) {try {SearchRequest request = new SearchRequest(ARTICLE_ES_INDEX);//設置查詢條件BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();//第一個條件if(null == keyword || "".equals(keyword)){request.source().query(QueryBuilders.matchAllQuery());}else {request.source().query(QueryBuilders.queryStringQuery(keyword).field("title").defaultOperator(Operator.OR));//保存搜索歷史apUserSearchService.insert(userId,keyword);}//分頁request.source().from(0);request.source().size(20);//按照時間倒序排序request.source().sort("publishTime", SortOrder.DESC);//搜索SearchResponse response = client.search(request, RequestOptions.DEFAULT);//解析結果SearchHits searchHits = response.getHits();//獲取具體文檔數據SearchHit[] hits = searchHits.getHits();List<Map> resultList = new ArrayList<>();for (SearchHit hit : hits) {//文檔數據Map map = JSON.parseObject(hit.getSourceAsString(), Map.class);resultList.add(map);}return resultList;} catch (IOException e) {throw new RuntimeException("搜索失敗");}}
}
package com.itheima.cdl.service.impl;
import com.itheima.cdl.service.ApUserSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {/*** 保存搜索歷史記錄* @param userId* @param keyword*/@Async("taskExecutor")//異步調用@Overridepublic void insert(Integer userId, String keyword) {//保存用戶記錄 mongodb或mysql//執行業務log.info("用戶搜索記錄保存成功,用戶id:{},關鍵字:{}",userId,keyword);}
}
4.2 如何控制某個方法允許并發訪問線程的數量?
難易程度:☆☆☆
出現頻率:☆☆
Semaphore [?s?m??f?r] 信號量,是JUC包下的一個工具類底層是AQS,我們可以通過其限制執行的線程數量,達到限流的效果
當一個線程執行時先通過其方法進行獲取許可操作,獲取到許可的線程繼續執行業務邏輯,當線程執行完成后進行釋放許可操作,未獲取達到許可的線程進行等待或者直接結束。
Semaphore兩個重要的方法
lsemaphore.acquire(): 請求一個信號量,這時候的信號量個數-1(一旦沒有可使用的信號量,也即信號量個數變為負數時,再次請求的時候就會阻塞,直到其他線程釋放了信號量)
lsemaphore.release():釋放一個信號量,此時信號量個數+1
線程任務類:
public class SemaphoreCase {public static void main(String[] args) {// 1. 創建 semaphore 對象Semaphore semaphore = new Semaphore(3); //容量是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是多線程中對于解決線程安全的一個操作類,它會為每個線程都分配一個獨立的線程副本從而解決了變量并發訪問沖突的問題。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對象中的四種引用類型:強引用、軟引用、弱引用、虛引用
強引用、軟引用、弱引用的區別和解析
- 強引用:
最為普通的引用方式,表示一個對象處于有用且必須的狀態,如果一個對象具有強引用,則GC并不會回收它。即便堆中內存不足了,寧可出現OOM,也不會對其進行回收
- 弱引用:
表示一個對象處于可能有用且非必須的狀態。在GC線程掃描內存區域時,一旦發現弱引用,就會回收到弱引用相關聯的對象。對于弱引用的回收,無關內存區域是否足夠,一旦發現則會被回收
內存泄漏問題
每一個Thread維護一個ThreadLocalMap,在ThreadLocalMap中的Entry對象繼承了WeakReference。其中key為使用弱引用的ThreadLocal實例,value為線程變量的副本
在使用ThreadLocal的時候,強烈建議:務必手動remove 防止內存泄漏
6 真實面試還原
6.1 線程的基礎知識
聊一下并行和并發有什么區別?
候選人:
是這樣的~~
現在都是多核CPU,在多核CPU下
并發是同一時間應對多件事情的能力,多個線程輪流使用一個或多個CPU
并行是同一時間動手做多件事情的能力,4核CPU同時執行4個線程
說一下線程和進程的區別?
候選人:
嗯,好~
進程是正在運行程序的實例,進程中包含了線程,每個線程執行不同的任務
不同的進程使用不同的內存空間,在當前進程下的所有線程可以共享內存空間
線程更輕量,線程上下文切換成本一般上要比進程上下文切換低(上下文切換指的是從一個線程切換到另一個線程)
如果在java中創建線程有哪些方式?
候選人:
在java中一共有四種常見的創建方式,分別是:繼承Thread類、實現runnable接口、實現Callable接口、線程池創建線程。通常情況下,我們項目中都會采用線程池的方式創建線程。
好的,剛才你說的runnable 和 callable 兩個接口創建線程有什么不同呢?
候選人:
是這樣的~
最主要的兩個線程一個是有返回值,一個是沒有返回值的。
Runnable 接口run方法無返回值;Callable接口call方法有返回值,是個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果
還有一個就是,他們異常處理也不一樣。Runnable接口run方法只能拋出運行時異常,也無法捕獲處理;Callable接口call方法允許拋出異常,可以獲取異常信息
在實際開發中,如果需要拿到執行的結果,需要使用Callalbe接口創建線程,調用FutureTask.get()得到可以得到返回值,此方法會阻塞主進程的繼續往下執行,如果不調用不會阻塞。
線程包括哪些狀態,狀態之間是如何變化的?
候選人:
在JDK中的Thread類中的枚舉State里面定義了6中線程的狀態分別是:新建、可運行、終結、阻塞、等待和有時限等待六種。
關于線程的狀態切換情況比較多。我分別介紹一下
當一個線程對象被創建,但還未調用 start 方法時處于 新建狀態,調用了 start 方法,就會由 新建進入 可運行狀態。如果線程內代碼已經執行完畢,由 可運行進入 終結狀態。當然這些是一個線程正常執行情況。
如果線程獲取鎖失敗后,由 可運行進入 Monitor 的阻塞隊列 阻塞,只有當持鎖線程釋放鎖時,會按照一定規則喚醒阻塞隊列中的 阻塞線程,喚醒后的線程進入 可運行狀態
如果線程獲取鎖成功后,但由于條件不滿足,調用了 wait() 方法,此時從 可運行狀態釋放鎖 等待狀態,當其它持鎖線程調用 notify() 或 notifyAll() 方法,會恢復為 可運行狀態
還有一種情況是調用 sleep(long) 方法也會從 可運行狀態進入 有時限等待狀態,不需要主動喚醒,超時時間到自然恢復為 可運行狀態
嗯,好的,剛才你說的線程中的 wait 和 sleep方法有什么不同呢?
候選人:
它們兩個的相同點是都可以讓當前線程暫時放棄 CPU 的使用權,進入阻塞狀態。
不同點主要有三個方面:
第一:方法歸屬不同
sleep(long) 是 Thread 的靜態方法。而 wait(),是 Object 的成員方法,每個對象都有
第二:線程醒來時機不同
線程執行 sleep(long) 會在等待相應毫秒后醒來,而 wait() 需要被 notify 喚醒,wait() 如果不喚醒就一直等下去
第三:鎖特性不同
wait 方法的調用必須先獲取 wait 對象的鎖,而 sleep 則無此限制
wait 方法執行后會釋放對象鎖,允許其它線程獲得該對象鎖(相當于我放棄 cpu,但你們還可以用)
而 sleep 如果在 synchronized 代碼塊中執行,并不會釋放對象鎖(相當于我放棄 cpu,你們也用不了)
好的,我現在舉一個場景,你來分析一下怎么做,新建 T1、T2、T3 三個線程,如何保證它們按順序執行?
候選人:
嗯~~,我思考一下 (適當的思考或想一下屬于正常情況,脫口而出反而太假[背誦痕跡])
可以這么做,在多線程中有多種方法讓線程按特定順序執行,可以用線程類的 join()方法在一個線程中啟動另一個線程,另外一個線程完成該線程繼續執行。
比如說:
使用join方法,T3調用T2,T2調用T1,這樣就能確保T1就會先完成而T3最后完成
在我們使用線程的過程中,有兩個方法。線程的 run()和 start()有什么區別?
候選人:
start方法用來啟動線程,通過該線程調用run方法執行run方法中所定義的邏輯代碼。start方法只能被調用一次。run方法封裝了要被線程執行的代碼,可以被調用多次。
那如何停止一個正在運行的線程呢?
候選人:
有三種方式可以停止線程
第一:可以使用退出標志,使線程正常退出,也就是當run方法完成后線程終止,一般我們加一個標記
第二:可以使用線程的stop方法強行終止,不過一般不推薦,這個方法已作廢
第三:可以使用線程的interrupt方法中斷線程,內部其實也是使用中斷標志來中斷線程
我們項目中使用的話,建議使用第一種或第三種方式中斷線程
6.2 線程中并發鎖
講一下synchronized關鍵字的底層原理?
候選人:
嗯~~好的,
synchronized 底層使用的JVM級別中的Monitor 來決定當前線程是否獲得了鎖,如果某一個線程獲得了鎖,在沒有釋放鎖之前,其他線程是不能或得到鎖的。synchronized 屬于悲觀鎖。
synchronized 因為需要依賴于JVM級別的Monitor ,相對性能也比較低。
好的,你能具體說下Monitor 嗎?
候選人:
monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因
monitor內部維護了三個變量
WaitSet:保存處于Waiting狀態的線程
EntryList:保存處于Blocked狀態的線程
Owner:持有鎖的線程
只有一個線程獲取到的標志就是在monitor中設置成功了Owner,一個monitor中只能有一個Owner
在上鎖的過程中,如果有其他線程也來搶鎖,則進入EntryList 進行阻塞,當獲得鎖的線程執行完了,釋放了鎖,就會喚醒EntryList 中等待的線程競爭鎖,競爭的時候是非公平的。
好的,那關于synchronized 的鎖升級的情況了解嗎?
候選人:
嗯,知道一些(要謙虛)
Java中的synchronized有偏向鎖、輕量級鎖、重量級鎖三種形式,分別對應了鎖只被一個線程持有、不同線程交替持有鎖、多線程競爭鎖三種情況。
重量級鎖:底層使用的Monitor實現,里面涉及到了用戶態和內核態的切換、進程的上下文切換,成本較高,性能比較低。
輕量級鎖:線程加鎖的時間是錯開的(也就是沒有競爭),可以使用輕量級鎖來優化。輕量級修改了對象頭的鎖標志,相對重量級鎖性能提升很多。每次修改都是CAS操作,保證原子性
偏向鎖:一段很長的時間內都只被一個線程使用鎖,可以使用了偏向鎖,在第一次獲得鎖時,會有一個CAS操作,之后該線程再獲取鎖,只需要判斷mark word中是否是自己的線程id即可,而不是開銷相對較大的CAS命令
一旦鎖發生了競爭,都會升級為重量級鎖
好的,剛才你說了synchronized它在高并發量的情況下,性能不高,在項目該如何控制使用鎖呢?
候選人:
嗯,其實,在高并發下,我們可以采用ReentrantLock來加鎖。
嗯,那你說下ReentrantLock的使用方式和底層原理?
候選人:
好的,
ReentrantLock是一個可重入鎖:,調用 lock 方 法獲取了鎖之后,再次調用 lock,是不會再阻塞,內部直接增加重入次數 就行了,標識這個線程已經重復獲取一把鎖而不需要等待鎖的釋放。
ReentrantLock是屬于juc報下的類,屬于api層面的鎖,跟synchronized一樣,都是悲觀鎖。通過lock()用來獲取鎖,unlock()釋放鎖。
它的底層實現原理主要利用 CAS+AQS隊列來實現。它支持公平鎖和非公平鎖,兩者的實現類似
構造方法接受一個可選的公平參數( 默認非公平鎖),當設置為true時,表示公平鎖,否則為非公平鎖。公平鎖的效率往往沒有非公平鎖的效率高。
好的,剛才你說了CAS和AQS,你能介紹一下嗎?
候選人:
好的。
CAS的全稱是: Compare And Swap(比較再交換);它體現的一種樂觀鎖的思想,在無鎖狀態下保證線程操作數據的原子性。
CAS使用到的地方很多:AQS框架、AtomicXXX類
在操作共享變量的時候使用的自旋鎖,效率上更高一些
CAS的底層是調用的Unsafe類中的方法,都是操作系統提供的,其他語言實現
AQS的話,其實就一個jdk提供的類AbstractQueuedSynchronizer,是阻塞式鎖和相關的同步器工具的框架。
內部有一個屬性 state 屬性來表示資源的狀態,默認state等于0,表示沒有獲取鎖,state等于1的時候才標明獲取到了鎖。通過cas 機制設置 state 狀態
在它的內部還提供了基于 FIFO 的等待隊列,是一個雙向列表,其中
tail 指向隊列最后一個元素
head 指向隊列中最久的一個元素
其中我們剛剛聊的ReentrantLock底層的實現就是一個AQS。
synchronized和Lock有什么區別 ?
候選人:
嗯,好的,主要有三個方面不太一樣
第一,語法層面
synchronized 是關鍵字,源碼在 jvm 中,用 c++ 語言實現,退出同步代碼塊鎖會自動釋放
Lock 是接口,源碼由 jdk 提供,用 java 語言實現,需要手動調用 unlock 方法釋放鎖
第二,功能層面
二者均屬于悲觀鎖、都具備基本的互斥、同步、鎖重入功能
Lock 提供了許多 synchronized 不具備的功能,例如獲取等待狀態、公平鎖、可打斷、可超時、多條件變量,同時Lock 可以實現不同的場景,如 ReentrantLock, ReentrantReadWriteLock
第三,性能層面
在沒有競爭時,synchronized 做了很多優化,如偏向鎖、輕量級鎖,性能不賴
在競爭激烈時,Lock 的實現通常會提供更好的性能
統合來看,需要根據不同的場景來選擇不同的鎖的使用。
死鎖產生的條件是什么?
候選人:
嗯,是這樣的,一個線程需要同時獲取多把鎖,這時就容易發生死鎖,舉個例子來說:
t1 線程獲得A對象鎖,接下來想獲取B對象的鎖
t2 線程獲得B對象鎖,接下來想獲取A對象的鎖
這個時候t1線程和t2線程都在互相等待對方的鎖,就產生了死鎖
那如果產出了這樣的,如何進行死鎖診斷?
候選人:
這個也很容易,我們只需要通過jdk自動的工具就能搞定
我們可以先通過jps來查看當前java程序運行的進程id
然后通過jstack來查看這個進程id,就能展示出來死鎖的問題,并且,可以定位代碼的具體行號范圍,我們再去找到對應的代碼進行排查就行了。
請談談你對 volatile 的理解
候選人:
嗯~~
volatile 是一個關鍵字,可以修飾類的成員變量、類的靜態成員變量,主要有兩個功能
第一:保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的,volatile關鍵字會強制將修改的值立即寫入主存。
第二: 禁止進行指令重排序,可以保證代碼執行有序性。底層實現原理是,添加了一個 內存屏障,通過插入內存屏障禁止在內存屏障 前后的指令執行重排序優化本文作者:接《集合相關面試題》
那你能聊一下ConcurrentHashMap的原理嗎?
候選人:
嗯好的,
ConcurrentHashMap 是一種線程安全的高效Map集合,jdk1.7和1.8也做了很多調整。
JDK1.7的底層采用是 分段的數組+ 鏈表 實現
JDK1.8 采用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹。
在jdk1.7中 ConcurrentHashMap 里包含一個 Segment 數組。Segment 的結構和HashMap類似,是一 種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構 的元素,每個 Segment 守護著一個HashEntry數組里的元素,當對 HashEntry 數組的數據進行修 改時,必須首先獲得對應的 Segment的鎖。
Segment 是一種可重入的鎖 ReentrantLock,每個 Segment 守護一個HashEntry 數組里得元 素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment 鎖
在jdk1.8中的ConcurrentHashMap 做了較大的優化,性能提升了不少。首先是它的數據結構與jdk1.8的hashMap數據結構完全一致。其次是放棄了Segment臃腫的設計,取而代之的是采用Node + CAS + Synchronized來保 證并發安全進行實現,synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不沖 突,就不會產生并發 , 效率得到提升
6.3 線程池
線程池的種類有哪些?
候選人:
嗯!是這樣
在jdk中默認提供了4中方式創建線程池
第一個是:newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回 收空閑線程,若無可回收,則新建線程。
第二個是:newFixedThreadPool 創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列 中等待。
第三個是:newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
第四個是:newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任 務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
線程池的核心參數有哪些?
候選人:
在線程池中一共有7個核心參數:
corePoolSize 核心線程數目 - 池中會保留的最多線程數
maximumPoolSize 最大線程數目 - 核心線程+救急線程的最大數目
keepAliveTime 生存時間 - 救急線程的生存時間,生存時間內沒有新任務,此線程資源會釋放
unit 時間單位 - 救急線程的生存時間單位,如秒、毫秒等
workQueue - 當沒有空閑核心線程時,新來任務會加入到此隊列排隊,隊列滿會創建救急線程執行任務
threadFactory 線程工廠 - 可以定制線程對象的創建,例如設置線程名字、是否是守護線程等
handler 拒絕策略 - 當所有線程都在繁忙,workQueue 也放滿時,會觸發拒絕策略
在拒絕策略中又有4中拒絕策略
當線程數過多以后,第一種是拋異常、第二種是由調用者執行任務、第三是丟棄當前的任務,第四是丟棄最早排隊任務。默認是直接拋異常。
如何確定核心線程池呢?
候選人:
是這樣的,我們公司當時有一些規范,為了減少線程上下文的切換,要根據當時部署的服務器的CPU核數來決定,我們規則是:CPU核數+1就是最終的核心線程數。
線程池的執行原理知道嗎?
候選人:
嗯~,它是這樣的
首先判斷線程池里的核心線程是否都在執行任務,如果不是則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊 列里。如果工作隊列滿了,則判斷線程池里的線程是否都處于工作狀態,如果沒有,則創建一個新的工作線程來執行任 務。如果已經滿了,則交給拒絕策略來處理這個任務。
為什么不建議使用Executors創建線程池呢?
候選人:
好的,其實這個事情在阿里提供的最新開發手冊《Java開發手冊-嵩山版》中也提到了
主要原因是如果使用Executors創建線程池的話,它允許的請求隊列默認長度是Integer.MAX_VALUE,這樣的話,有可能導致堆積大量的請求,從而導致OOM(內存溢出)。
所以,我們一般推薦使用ThreadPoolExecutor來創建線程池,這樣可以明確規定線程池的參數,避免資源的耗盡。
6.4 線程使用場景問題
如果控制某一個方法允許并發訪問線程的數量?
候選人:
嗯~~,我想一下
在jdk中提供了一個Semaphore[sem?f??r]類(信號量)
它提供了兩個方法,semaphore.acquire() 請求信號量,可以限制線程的個數,是一個正數,如果信號量是-1,就代表已經用完了信號量,其他線程需要阻塞了
第二個方法是semaphore.release(),代表是釋放一個信號量,此時信號量的個數+1
好的,那該如何保證Java程序在多線程的情況下執行安全呢?
候選人:
嗯,剛才講過了導致線程安全的原因,如果解決的話,jdk中也提供了很多的類幫助我們解決多線程安全的問題,比如:
JDK Atomic開頭的原子類、synchronized、LOCK,可以解決原子性問題
synchronized、volatile、LOCK,可以解決可見性問題
Happens-Before 規則可以解決有序性問題
你在項目中哪里用了多線程?
候選人:
嗯~~,我想一下當時的場景[根據自己簡歷上的模塊設計多線程場景]
參考場景一:
es數據批量導入
在我們項目上線之前,我們需要把數據量的數據一次性的同步到es索引庫中,但是當時的數據好像是1000萬左右,一次性讀取數據肯定不行(oom異常),如果分批執行的話,耗時也太久了。所以,當時我就想到可以使用線程池的方式導入,利用CountDownLatch+Future來控制,就能大大提升導入的時間。
參考場景二:
在我做那個xx電商網站的時候,里面有一個數據匯總的功能,在用戶下單之后需要查詢訂單信息,也需要獲得訂單中的商品詳細信息(可能是多個),還需要查看物流發貨信息。因為它們三個對應的分別三個微服務,如果一個一個的操作的話,互相等待的時間比較長。所以,我當時就想到可以使用線程池,讓多個線程同時處理,最終再匯總結果就可以了,當然里面需要用到Future來獲取每個線程執行之后的結果才行
參考場景三:
《黑馬頭條》項目中使用的
我當時做了一個文章搜索的功能,用戶輸入關鍵字要搜索文章,同時需要保存用戶的搜索記錄(搜索歷史),這塊我設計的時候,為了不影響用戶的正常搜索,我們采用的異步的方式進行保存的,為了提升性能,我們加入了線程池,也就說在調用異步方法的時候,直接從線程池中獲取線程使用
6.5 其他
談談你對ThreadLocal的理解
候選人:
嗯,是這樣的~~
ThreadLocal 主要功能有兩個,第一個是可以實現資源對象的線程隔離,讓每個線程各用各的資源對象,避免爭用引發的線程安全問題,第二個是實現了線程內的資源共享
好的,那你知道ThreadLocal的底層原理實現嗎?
候選人:
嗯,知道一些~
在ThreadLocal內部維護了一個一個 ThreadLocalMap 類型的成員變量,用來存儲資源對象
當我們調用 set 方法,就是以 ThreadLocal 自己作為 key,資源對象作為 value,放入當前線程的 ThreadLocalMap 集合中
當調用 get 方法,就是以 ThreadLocal 自己作為 key,到當前線程中查找關聯的資源值
當調用 remove 方法,就是以 ThreadLocal 自己作為 key,移除當前線程關聯的資源值
好的,那關于ThreadLocal會導致內存溢出這個事情,了解嗎?
候選人:
嗯,我之前看過源碼,我想一下~~
是應為ThreadLocalMap 中的 key 被設計為弱引用,它是被動的被GC調用釋放key,不過關鍵的是只有key可以得到內存釋放,而value不會,因為value是一個強引用。
在使用ThreadLocal 時都把它作為靜態變量(即強引用),因此無法被動依靠 GC 回收,建議主動的remove 釋放 key,這樣就能避免內存溢出。