java 并发面试题2
1.为什么java内存不可见
java线程是CPU调度的。每个CPU又L1、L2、L3的高速缓存。CPU调度某个线程的时候会将JVM中的数据拉取到高速缓存中。因为现在基本都是多核CPU,所以其他CPU如果也获取了相同的数据并且有写操作发生,就会导致多核之间高速缓存中的数据不一致。
2.什么是JMM
JMM是java内存模型(注意不是内存结构),属于并发编程。
不同的CPU厂商,CPU的实现机制不同。所以在内存和指令上又一些不同。JMM是为了屏蔽硬件和操作系统带来的差异,使得java程序能够在不同的硬件和操作系统下,实现并发编程的原子性、可见性、禁止指令重排列。
是存在CPU和JVM之间的一个规范,这个规范可以将JVM的字节码指令转换为CPU能够识别的一些指令。
3.JAVA里有哪些锁?区别是什么?
面试建议可以从乐观锁和悲观锁的实现方面回答。
乐观锁和悲观锁是两个概念,不是两个具体的锁。JAVA中针对这两种锁做了具体的落地。
乐观锁:认为在操作的时候,没有线程并发操作,如果有并发会操作失败,返回false,成功返回true。不会阻塞、等待,失败了就再试一次。
悲观锁:认为在操作时,会有并发操作。发生并发时,就会先去尝试竞争锁资源,如果拿不到资源,则会将线程挂起、阻塞等待。
JAVA中的实现:
乐观锁:CAS,在JAVA中是以Unsafe类中的native方法形式存在的,到了底层就是CPU支持的原子操作。
悲观锁:synchronized、Lock锁。
4.乐观锁和悲观锁的区别,乐观锁一定好吗?
乐观锁:不会让线程阻塞、挂起,可以让CPU调度执行这个乐观锁,可以直到成功为止。
悲观锁:会在竞争锁资源失败后阻塞、挂起。等待锁资源释放后,才可能唤醒这个线程去竞争资源。
核心区别在于是否会挂起线程,因为挂起线程这个操作在用户态时不能这么操作,需要从用户态转换到内核态,让OS去唤醒挂起的线程。用户态和内核态切换比较耗时。
如果竞争很激烈,导致乐观锁一直失败,那么CPU会一直去调度。此时会浪费CPU资源。
5.CAS有没有加锁,有哪些用的地方?
在JAVA中没有涉及到锁的情况,因为没有涉及到线程的阻塞挂起和唤醒操作。
但是CAS是在CPU是基于某个特定指令去实现的,而CPU是多核的。那么在多个核心都会去对一个变量进行CAS操作时,会添加LOCK前缀指令,可能会基于缓存锁或者总线锁,只让一个CPU执行这个CAS操作。
一般在开发中用不到CAS操作。但是在java并发包下可能会用到,比如synchronized、ReentrantLock、ThreadPoolExecutor、CountDownLatch等
(不知道实现中哪里具体用到了CAS锁?)
6.java中锁的底层实现
1)CAS ,CPU的cmpxchg指令
2)synchronied:
a.对象头中MarkWord,锁升级、无锁、偏向锁、轻量级锁、重量级锁
(如果深入问,要掌握到什么程度?)
3)Lock
a.AQS
(如果深入问,要掌握到什么程度?)
7.为什么HashMap的kv允许是null,而CHM(CurrentHashMap)不允许kv为null
hashMap的使用场景是现成局部使用,不存在多线程并发操作的问题。kv存null不影响当前线程的操作。
CHM设计的目标是在多线程的环境下去使用,如果允许存储null,会存在二义性问题,即:get(k) 得到的的v是null,是获取到了?还是没获取到?多线程操作的情况下,null值,极有可能造成空指针异常。
8.hash冲突有几种解决方式?
链地址法
多次hash法
公共溢出区:将hash表分为基准表和溢出表,但凡出现了hash冲突,就将冲突的数据扔到溢出表里。
开放地址法:有关键字的哈希地址(i),出现冲突时,以这个地址(i)为基准,产生另一个hash地址(i2),若i2还有冲突,则以(i2)为基准再生成一个i3,以此类推,直到产生一个不冲突的地址。
线性探测:顺序往后找hash地址,0有冲突看1,1有冲突看2…
二次平方探测:2,4,8,。。。
9.怎么用runable实现callable的功能
本质就是问futuretask
区别就是是否能抛异常,是否有返回值。
Callable本质是通过futuretask去执行。而在futuretask中有一个成员变量outcome,任务执行过程中抛出的异常或者返回的结果,都会被封装到outcome中,执行者需要结果时则会返回outcome中存储的内容。
10.ThreadLocal的应用场景,key和value是什么?
真正存储数据的是每个thread的threadLocalMap
应用场景(同一个线程中做参数传递):
1)声明事务,service层和mapper层使用的是同一个connection
2)日志
3)在filter中截获token,在controller中使用token。
key:threalLocal本身
value:put的内容
11.子线程如何获取父线程的信息
1)采用共享变量的方式,来做数据传递
2)java提供了inheritableThreadLocal类来实现这个操作
12.java中如何唤醒一个阻塞的线程
java中线程中阻塞的方式只有一种,就是park()所以唤醒的方法有:
1)interrupt方法
2)Unsafe.unpark()
java对阻塞的状态细分了三种:
1)BLOCKED:synchronied(在c++中线程会被唤醒,但是在java层面来看就没有唤醒)
2)WAITING
3)TIMED_WAITING
三个状态的区别不理解
blocked:争夺锁造成的阻塞
waiting:无限期等待,通常是Object.wait()(需在 synchronized 块中调用)Thread.join()(等待目标线程终止)LockSupport.park()(底层工具方法)造成
timed_waiting:线程调用带有超时参数的等待方法,进入有限期等待状态,一般调用如下方法会造成:
Thread.sleep(long millis)
Object.wait(long timeout)
Thread.join(long millis)
LockSupport.parkNanos(long nanos)
LockSupport.parkUntil(long deadline)
13.多个任务同时达到临界点,主线程执行如何实现?
1)减法计数器(CountDownLatch)锁实现
2)thread.join()方法实现
3)FutureTask
4)CompleableFuture
14.如何让20个线程同时开始执行?
CyclicBarrier 加法计数器实现
CyclicBarrier的底层是基于ReentrantLock实现的。到位的线程会基于await挂起,并且丟到Condition单向链表中。等到计数器到0时,会基于signalAll的方法,将所有到位的线程一个一个的唤醒,每个唤醒的线程还需要到AQS的同步队列中获取锁资源,才能继续往下执行,所以他们是存在先后顺序的。
示例代码:
public static void main(String[] args) {
int parties = 4; // 参与线程数
CyclicBarrier barrier = new CyclicBarrier(parties, new Runnable() {
//每一轮执行完都打印一下日志
@Override
public void run() {
System.out.println(“所有线程已到达屏障, 开始汇总结果或进行下一轮准备…”);
}
});
for (int i = 0; i < parties; i++) {new Thread(new Worker(barrier, "Worker-" + i)).start();
}
}
static class Worker implements Runnable {
private final CyclicBarrier barrier;
private final String name;
Worker(CyclicBarrier barrier, String name) {this.barrier = barrier;this.name = name;
}@Override
public void run() {try {for (int i = 0; i < 3; i++) {// 每个线程执行三次任务Thread.sleep((long) (Math.random() * 1000)); // 模拟任务执行时间System.out.println(name + " 第 " + (i + 1) + " 次任务完成");barrier.await(); // 等待所有线程完成当前轮次}} catch (InterruptedException | BrokenBarrierException e) {Thread.currentThread().interrupt();e.printStackTrace();}
}
}
15.CountDownLatch和CyclicBarrier分别适用于什么业务,哪个可以复用,为什么?
CountDownLatch:等多个线程的操作完成,再去执行某个业务。在面试中建议结合项目经历来说。
Cyclicbarrier:等指定个线程都执行完毕,再去执行某个业务。比如拼团。
CyclicBarrier可以复用。
CountDownLatch是基于AQS中的state做计数,每完成一个任务,countDown方法执行后,会对state1,当state为0后,就会唤醒那些基于CountDownLatch执行await的线程。CyclicBarrier是自己搞了一个count属性,每当有一个线程到位之后,就会对count进行–操作。等到count计数到0后,依然会唤醒,可以优先触发一个任务,然后唤醒所有到位的线程。
CyclicBarrier是可以复用的。他提供了一个reset的方法,在这个reset方法中,会将所有之前到位,和即将到位的线程全部唤醒结束,清空当前CyclicBarrier,以便下次使用
16.线程池的执行过程
任务投递之后
1)如果当前线程池的核心线程数不满足设定的核心线程数,那么会创建核心线程去处理投递过来的任务。
2)如果线程个数等于设置的核心线程数,那么会将任务投递到工作队列中排队。
3)如果工作队列的长度大于排队数量,任务会被正常投递到工作队列
4)如果工作队列的任务数和现在排队的任务数一致(即工作队列满了),任务无法投递到工作队列,此时需要创建一个非核心线程来处理刚刚投递过来的任务。
5)创建非核心线程时,还需要判断一下线程个数是否小于设置的最大线程数。小于才会正常创建。
6)如果线程个数等于最大线程数,则会执行拒绝策略。
线程池的执行过程参考图片:
17.线程池中为什么任务队列满了,会创建一个非核心线程去执行要投递的任务,而不是先去执行队列中最靠前的任务?
减少任务提交等待时间。避免先去队列取一个任务出来执行的时间。
18.核心线程和非核心线程有什么区别
没区别,只有在创建的时候会区分。
19.核心线程可以被回收吗?
如果allowCoreThreadTimtOut(默认为false)设置为true,那么只要工作线程的空闲时间超过了最大空闲时间,那么该线程就会被回收。
false:仅对非核心线程生效
true:对所有线程生效
20.线程池连环问
java线程池,5核心、10最大、10队列
第六个任务来了处于什么状态?-仍在队列里(无论核心线程是否空闲)
第十六个任务怎么处理?-创建非核心线程去处理这个任务
第十六个任务来了,核心线程空闲,那么如何处理?-如果队列中十个任务已经被核心线程取走,那么直接加入队列。如果队列中的任务还没有被核心线程取走,则创建非核心线程处理。
队列满了以后执行队列的任务是从头还是尾获取?-从头取
核心线程和非核心线程执行结束后,谁先执行队列里的任务?-谁先空闲了,等待任务,谁就先执行。
21.线程池的工作线程在执行任务的过程中,如何取消任务的执行?
原生的ThreadPoolExecutor在提交普通的Runnable任务时,是无法做到的,需要通过提交FutureTask任务,在任务处理的过程中会记录是哪个线程在处理当前任务。这样可以通过cancel方法,取消该任务。
这样咱们就可以获取到执行当前任务的线程对象执行他的interrupt方法。如果有中断的出口,那就结束了,但是如果没有中断的出口,那即便你中断了,任务也会执行完毕!所以需要catch InterruptedException。
其次也可以在任务还没执行前,通过对任务提供状态的修饰,基于状态来阻止任务执行。
22.线程池参数怎么设置?
线程池参数:
核心线程数
最大线程数
最大空闲时间
空闲时间单位
工作队列
线程工厂
拒绝策略
如何设置,分三步回答
1 根据线上服务器配置去聊:
CPU内核数
内存大小
2 线程池任务处理情况:
CPU密集
IO密集
混合型
3 直接说明核心线程数设置的是多少,以及工作队列多长
核心线程数:与最大线程数保持一致(因为压测得出的核心线程数就是最优解)
可以提前编一个数值,但是要说明这个数值是压测出来的。
如果是混合型,那么50左右没问题,如果是CPU密集的,别太大,与核心数差不多就行。
工作队列:要么ArrayBlockingQueue,要么LinkedBlockingQueue。推荐使用LinkedBlockingQueue。因为底层是队列,且工作队列本身就是增删频繁的情况,所以用这个。长度要考虑并发情况,如果任务体量较大,会造成内存使用率过大。如果任务队列太长,那么任务被处理时,最大的延迟时间能否被接受。
队列长度要直接说是多少,一般是核心线程的2倍左右。
23.设置线程池参数压测时,需要查看哪些指标?
CPU占用率:
IO密集型任务,CPU占用率不用提太多。
混合型或者CPU密集型任务中,需要时刻关注CPU占用率的情况,一般不超过70%都没啥问题,最好控制在50-60。
内存资源:
内存资源自然是线程本身也会占用,一般占1M。并且任务的处理过程中也会占用额外的线程资源。在队列中排队的任务也会占用内存资源。不能让内存资源占据太多。峰值时占据50-60即可。
磁盘资源:
一般不太考虑
吞吐量:越大越好 500个/s
响应时间:越小越好 200ms
还需要查看GC情况,如果任务体量比较大,如果新生代的内存不够,可能导致对象直接进入老年代,或者新生代频繁GC,就可能导致FULL GC频繁。
其他资源:比如数据库连接
优先根据配置自己确认要一个预估的数值,然后开始压测,查看指标情况。
逐步增加并发的数值,以及线程池的参数,去查看性能指标。
如果在逐渐调整数值后,依然无法达到性能要求,根据前面的指标,查看瓶颈在哪里,做优化。
重复上述操作,知道性能满足要求。
24.CopyOnWrite如何保证线程安全,为什么要这么做
基于写入操作前,获取reentrantLock,毕竟写写操作互斥,然后将本地数据复制一份,在复制的内容中完成写操作,在写完之后,用副本覆盖掉本地数据。
虽然会让内存占用量变大,但是读操作和写操作不互斥。当有读线程,则读取本地数据。
适合读多写少的场景。
25.CuncurrentHashMap在红黑树的读写并发会发生什么问题?
红黑树为了保证平衡,在写入数据时,可能会旋转、变色。
如果读写并行执行,可能导致在写操作导致数旋转时,读取到错误的路径,导致数据没有找到。
如果发生了读写同时进行的情况,读操作并不会被阻塞,而是直接去读取双向链表。
注意:
CurrentHashMap的结构
但是读线程如何知道有写线程写读数据?
26.实际项目中有使用过CurrentHashMap吗?
用于缓存一些热点数据。
需要准备项目
27.工作中的死锁如何处理?
死锁的行成条件:
1 不可剥夺
2 环路等待
3 互斥条件
4 请求保持
避免环路等待锁的情况
不要走lock的方式,采用tryLock 获取不到锁就结束的情况
如何定位?
采用jstack、jdk自带工具、arthas