文章目录
- 1、创建线程的三种方式
- 2、线程的状态
- 3、线程的上下文切换
- 4、run和start的区别
- 5、sleep和wait区别
- 6、虚假唤醒,精确唤醒
- 7、两阶段终止模式
- 8、多线程下的线程安全问题
- 9、如何解决线程安全问题
- 10、synchornized的原理
- 11、锁升级的机制
- 12、锁消除
- 13、批量重偏向
- 14、park和unpark
- 15、死锁的条件
- 16、锁饥饿的问题
- 17、ReentrantLock
- 18、java内存模型的组成?三大特性?
- 19、volatile关键字的作用
- 20、什么是读写屏障?
- 21、synchronized能否保证JMM的三大特性
- 22、手写单例模式,懒汉式,饿汉式,双检锁模式【放在最后,一定要自己手写一遍】
- 23、什么是CAS,乐观锁机制?
- 24、CAS的ABA问题
- 25、原子累加器的原理?
- 26、什么是缓存行的伪共享问题?
- 27、什么是不可变对象?如何设计一个不可变类
- 28、什么是享元模式?在哪些地方有应用?
- 29、线程池的七大参数和工作原理?
- 30、拒绝策略?
- 31、阻塞队列?
- 32、常见的线程池有哪些?
- 33、线程数量设置多少比较合适?
- 34、线程中一些api的区别:
- 35、如何处理线程池中的异常:
- 36、手写线程池,了解AQS源码
根据笔记总结JUC并发编程面试题,只讲重点
1、创建线程的三种方式
关键点在于三者间的区别,runnable和callable都是经过thread包装的,不同的是runnable把创建线程和执行任务进行了分离。callable通过futureTask获取任务的结果。
2、线程的状态
在操作系统层面是5种,java的api层面是6种。
创建状态(new),运行状态(run或start)【引出4、run和start的区别
】、阻塞状态(synchronized没有竞争到锁)、等待状态(例如wait方法)、有时限的等待状态(例如wait方法带有时间参数)、结束状态。
3、线程的上下文切换
涉及到cpu任务调度和时间片,程序计数器的概念。在多线程的环境下,某个线程运行时cpu的时间片用尽,就会发生线程上下文切换问题,程序计数器会将当前线程的操作步骤记录,等到再次轮到该线程执行时,就从记录的位置继续执行【引出8、多线程下的线程安全问题
】,上下文切换不一定只发生在时间片结束,主动调用yield也可以,但是调度器可能忽略该提示。
4、run和start的区别
run是在主线程中执行,start是另启动一个线程。当调用start()方法时,Java虚拟机会在一个新的线程中调用该线程对象的run()方法。
5、sleep和wait区别
sleep不会释放锁,是thread层面的(当其他线程通过interrupt方法打断正在sleep的线程,sleep方法会抛出interruptedException【引出7、两阶段终止模式】
,wait会释放锁,是对象层面的,wait通常和notify和notifyAll一起运用达到线程间的通信【引出6、虚假唤醒,精确唤醒的问题
】
6、虚假唤醒,精确唤醒
如果有两个线程同时使用wait方法,主线程使用notify只会随机唤醒其中的一个,可能会导致不是自己想要唤醒的线程被唤醒。推荐使用notifyAll唤醒所有,或者使用reentranlock的newCondition.await()配合signal()方法精确唤醒。
7、两阶段终止模式
核心思想在于Sleep+设置打断标记。
isInterrupted():用于检查调用该方法的线程的中断状态。
interrupted():查询当前线程的中断状态,并清除中断状态标志。
8、多线程下的线程安全问题
关键点在于,很多操作在字节码的层面不是原子性的,比如i++,在底层是分为了四步,在执行这四步的过程中,线程发生上下文的切换。读取操作不会造成线程安全问题,多线程对方法中的局部变量的写也不会造成线程安全问题,因为栈和栈之间是独立的。
9、如何解决线程安全问题
加锁(乐观锁和悲观锁)…【从这一条可以引出下面十条面试题
】
10、synchornized的原理
首先,使用synchornized需要指定一个对象,如果对象不一样,那么锁是不会生效的。其次,synchornized还可以解决可见性和有序性问题,前提是代码块必须完全被synchornized控制。
表层原理:被synchornized保护的代码块,即使时间片结束,其他线程发现有锁,也无法对资源进行操作,会陷入阻塞(任务调度器不会把时间片分配给阻塞的线程)注意:synchornized不是把并行变成串行
底层原理:Monitor(监视器)机制。Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。(使用synchronized的动作让对象和Monitor关联。)
Dog dog = new Dog();
synchronized (dog){
}
11、锁升级的机制
无锁->偏向锁->轻量级锁->重量级锁
无锁状态不用解释,没加锁就是无锁。
当有A、B两个线程要访问同一个代码块时,B线程不知道什么原因一直阻塞,A线程就可以反复进入代码块,相当于还是单线程的情况,
,JVM 会在对象头里记录下A线程的 ID,之后如果该线程再次请求相同的锁,不需要再进行加锁和解锁操作,直接进入同步块。
就在这个时候B线程解除了阻塞,JVM 会撤销偏向锁,并将锁状态升级为轻量级锁,尝试通过CAS(Compare And Swap)操作将锁对象头的内容(原偏向锁或无锁状态)更新为当前线程的锁记录(Lock Record)。
如果争抢锁失败,或者此时又有更多的线程来竞争锁,锁会升级为重量级锁(走synchronized的流程)。
12、锁消除
如果 JVM 能够确定某个对象不会在多个线程之间共享(也就是不会逃逸到当前线程之外),那么该对象上的锁就没有必要存在,JVM 会在编译时直接将该锁操作去除,这就是锁消除。【相关面试题:jvm:逃逸分析
】
13、批量重偏向
当 JVM 检测到某个锁对象频繁被不同线程使用时,而这些线程都是相对固定的一组线程,那么 JVM 会进行一次批量重偏向操作,将这些对象的偏向锁重定向到新的线程组,而不是逐个撤销偏向锁。
批量重偏向的触发条件是 JVM 检测到同一批对象频繁进行偏向锁撤销,但是不是属于恶性竞争的情况(有一种情况,虽然是多线程环境,A和B线程要访问某个代码块,但是实际上是A先访问,然后B再访问,这时偏向了线程A的对象仍有机会重新偏向B)如果转换成强竞争,就会执行锁的批量撤销,并将锁升级为轻量级锁或重量级锁。
14、park和unpark
park:挂起线程 unpark:唤醒线程 和wait notify的区别,park和unpark是Thread层面的,不需要配合对象锁使用,并且可以精确唤醒。还可以先unpark再park(这种情况本次park不会生效)。
@Slf4j(topic = "c.Demo1")
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
//park案例
//wait,notify和notifyAll必须配合Object Monitor一起使用,park unpark不用
//park unpark可以精准到线程单位进行唤醒
//park unpark可以先unpark
Thread t1 = new Thread(() -> {
log.info("start");
try {
// Thread.sleep(1000);
//可以先unpark再park
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("park");
LockSupport.park();
log.info("resume");
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("unpark");
LockSupport.unpark(t1);
}
}
和sleep的区别是等待时会释放锁。
15、死锁的条件
同一个资源只能被A或者B同一时间持有,并且A在持有资源时等待B释放另一个资源,B同理,以及A和B不能强制占有对方的资源。如何定位死锁?使用jconsole控制台连接当前进程,在线程选项卡检测死锁。【引出17、ReentrantLock
】
16、锁饥饿的问题
是非公平锁模式下的问题,典型的非公平锁(synchronized),即一个线程可能会多次竞争到锁,而有的线程则一直获取不到锁。【引出17、ReentrantLock
】
17、ReentrantLock
ReentrantLock是一种可重入锁,同一个线程可以多次获取同一个对象而不会被阻塞。并且支持公平锁和非公平锁两种模式。并且需要手动地加锁和解锁,还提供了条件变量(condition),可以在某个线程await后使用singal方法精确唤醒。
ReentrantLock如何解决死锁问题?
利用tryLock方法,tryLock是尝试获取锁,如果获取不到就直接返回false,不会一直死等,陷入阻塞。还可以设置超时时间。
18、java内存模型的组成?三大特性?
JMM的三大特性 原子性,可见性,有序性
内存模型分为主内存和工作内存。主内存线程共享,每个线程都有自己的工作内存。存储主内存的副本。可能会导致什么问题?数据不一致的问题。
如果某个线程需要频繁从主内存中读取相同的值,JIT编译器就会将值缓存到该线程自己的工作内存中,主内存中的值一旦发生改变,在没有干预的情况下该线程读取到的还是自己工作内存中的值【引出19、volatile关键字的作用
】
19、volatile关键字的作用
被volatile修饰的变量,某个线程必须每次从主存中读取该变量的值,所以保证了可见性。同时volatile关键字也可以保证有序性【引出20、读写屏障
】。不能保证原子性【引出21、synchronized能否保证JMM的三大特性?
】
20、什么是读写屏障?
是volatile引出的概念。被volatile关键字修饰的变量,写指令后会加入写屏障,读指令前会加入读屏障。保证在写屏障之前对共享变量的改动都同步到主存,在读屏障之后的读取都是读主存中最新的数据。
21、synchronized能否保证JMM的三大特性
synchronized可以保证JMM的三大特性,在某个线程获取到锁时,会清空自己工作内存的值,并重新从主存中同步一份,释放锁时,会将自身的改动同步到主存中,从而保证了可见性和有序性。而synchronized本身就可以保证原子性。以上的前提是代码块要完全被synchronized关键字控制。
22、手写单例模式,懒汉式,饿汉式,双检锁模式【放在最后,一定要自己手写一遍】
23、什么是CAS,乐观锁机制?
CAS的意思是比较并交换,首先会从共享变量中获取旧值,在修改值之前,还会从主存中获取最新的值,并且判断旧值和现在主存中最新的值是否一致,如果一致则代表修改期间,没有别的线程对共享变量进行修改,则可以将共享变量的值设置为修改后的值。前提是共享变量需要被volatile修饰。CAS就是乐观锁的一种体现。(在读取数据,操作数据时不会加锁,而是在更新数据时才会检查)【引出24、CAS的ABA问题
】
24、CAS的ABA问题
即:CAS操作无法感知其他线程是否将共享变量修改,又改回去的情况。解决方式:AtomicStampedReference定义版本号。在进行CAS时不仅会检查旧值和主存中最新的值,还会检查版本号是否相同。
static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<String>("A",0);
//....
//获取值
String reference = atomicStampedReference.getReference();
//获取版本号 0
int stamp = atomicStampedReference.getStamp();
// 在后续的操作中,除了需要获取值,还需要获取版本号。
atomicStampedReference.compareAndSet(reference,"C",stamp,stamp+1)
如果不需要关心版本号,可以使用AtomicMarkableReference标记是否进行过修改。
AtomicMarkableReference<Bag> reference = new AtomicMarkableReference<>(bag,true);
25、原子累加器的原理?
底层使用的是分段锁,使用了一个或多个累加单元(称为Cell),每个单元存储部分的累加值。当多个线程同时更新时,它们会分散到不同的单元上进行操作,减少对单个累加单元的争夺。最终累加结果:当需要获取最终的累加值时,LongAdder.sum() 会将所有单元中的部分值加总,返回累加的最终结果。【有可能会接着问你CurrentHashMap分段锁和AQS的原理,还会引出26、什么是缓存行的伪共享问题
】
26、什么是缓存行的伪共享问题?
首先要知道缓存行是什么:现代CPU为了提高性能,引入了多级缓存(如L1、L2、L3)。每个缓存以缓存行为单位存储数据,通常一个缓存行的大小是64字节。当一个线程读取内存中的数据时,CPU会将包含该数据的整个缓存行加载到缓存中,这样可以减少频繁访问内存的开销。
重点:当某个线程修改了共享缓存行中的数据,其他线程对应缓存中的数据将标记为无效,触发缓存行的同步或重新加载。
从而引出了伪共享问题:
多个线程操作不同的变量,这些变量存储在不同的内存位置上。但是这些变量共享同一个缓存行(存在于多线程的环境下)
尽管每个线程访问的变量本身是独立的,但由于它们位于同一个缓存行中,缓存一致性协议导致每次修改都会影响其他线程,形成不必要的缓存行同步,导致性能下降。这种现象称为伪共享,因为实际上这些线程并没有真正共享同一个变量。
解决方式:通过引入额外的填充字段,使得每个变量占用独立的缓存行。
27、什么是不可变对象?如何设计一个不可变类
不可变类的典型是:String。类和方法都被final修饰,不能让子类继承,重写。并且进行防御性拷贝,每次都返回一个新的字符串对象,保证原有的对象不被修改。防御性拷贝属于深度拷贝
28、什么是享元模式?在哪些地方有应用?
本质:对于一定范围内相同对象的重复使用。例如Long的valueOf方法。(-128~127)
29、线程池的七大参数和工作原理?
核心线程数,最大线程数,应急线程存活时间,时间单位,拒绝策略,线程工厂,阻塞队列。【从此引出下面n条面试题
】
工作原理:首先创建线程到核心线程数量,核心线程数量满了,其他的任务放入阻塞队列,阻塞队列也满了,创建剩下任务数量的应急线程直到核心线程数+应急线程数 = 最大线程数,超过这个线程数量的任务就执行丢弃策略。【引出30、拒绝策略
】
30、拒绝策略?
丢弃等待时间最久的,由发起任务的线程执行,丢弃任务并抛出异常,直接丢弃不作处理,还可以自定义拒绝策略。
31、阻塞队列?
首先要明确什么是阻塞队列?是一种多线程模式下的生产者-消费者模型,生产者将消息放入阻塞队列,消费者从阻塞队列获取消息,当阻塞队列满时,生产者会阻塞,并唤醒消费者,当阻塞队列为空时,消费者会阻塞并唤醒生产者。
常见的阻塞队列有:ArrayBlockingQueue:基于数组的有界阻塞队列,队列的容量在创建时固定,不能动态扩展。它支持公平锁机制,即可以选择按插入顺序公平地处理线程。
LinkedBlockingQueue:可以指定容量也可以不指定。(最大容量受到内存限制)
PriorityBlockingQueue:没有指定容量,支持优先级排序的无界阻塞队列,队列中的元素会根据自然顺序或者自定义的比较器进行排序。
32、常见的线程池有哪些?
重点:【这一条需要结合真实项目说明,不能死背面试题。如何运用线程池,设置参数,并且可以配合CompletableFuture使用】
单线程的线程池,固定大小线程池…
什么场景用到的单线程线程池?比如需要手动执行定时任务,某个定时任务执行的时间要10分钟,但是不能页面上没反应,应该点了执行就直接返回,就可以另外开一个单线程的线程池,提交任务,先返回结果,然后让后台的另一个线程继续执行任务。
什么场景用到的固定大小的线程池?
从第三方系统拉取数据,避免系统资源耗尽,设置5个线程【引出问题33、线程数量设置多少比较合适?
】。
33、线程数量设置多少比较合适?
CPU密集型:复杂的数学运算、图像处理等,线程主要在使用 CPU 进行计算,并非处理外部资源 CPU 核心数 + 1
I/O 密集型:主要是 I/O 操作,如文件读写、数据库查询、网络请求等,线程通常会在等待外部资源(磁盘、网络)时处于空闲状态。
线程数 ≈ CPU 核心数 * (1 + 线程等待时间 / 线程工作时间)
34、线程中一些api的区别:
submit和execute:submit可以得到返回的结果,而execute不关心结果。submit底层还是依靠execute工作。
invokeAll和invokeAny: invokeAll会阻塞当前线程,等待所有任务执行完成后返回结果。如果有某个任务发生异常,不会返回后面任务的结果。
invokeAny:会阻塞当前线程,但是会返回最先完成任务的结果,如果最先完成任务抛出异常,则会返回第二个完成任务的结果。
shutDown和shutDownNow:shutDown:主线程不会阻塞,已经提交任务的线程会执行完。
shutDown:不会接受新的任务,同时会将队列中的任务返回。
35、如何处理线程池中的异常:
使用try-catch处理,或利用future的get()方法。
36、手写线程池,了解AQS源码
有条件可以手写实现一个简单的线程池,以及阅读AQS非公平锁模式的源码。