新版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,这样就能避免内存溢出。