# 面试常见问题
# CountDownLatch和Semaphore的区别和底层原理
CountDownLatch
是一个计数器闭锁,它允许一个或多个线程等待直到某个条件被满足。它通过一个给定的计数器来初始化,这个计数器的操作是原子的,即同时只能有一个线程操作它。 当一个线程调用
await()
方法时,它会阻塞直到其他线程调用countDown()
方法使计数器的值变为零。当计数器值减至零时,所有因调用await()
方法而处于等待状态的线程会被唤醒并继续执行。这种机制只会出现一次,因为计数器不能被重置。如果业务上需要一个可以重置计数次数的版本,可以考虑使用CyclicBarrier
。原理:
a. CountDownLatch实现的tryAcquire只是判断state是否等于0,不等于0就入队等待唤醒
b. 每次调用countDown的时候,会对state减1,减少到0的时候,唤醒等待的线程Semaphore
Semaphore 则是一个信号量,它允许同时最多有
n
个线程使用某个资源
原理:线程通过acquire()
方法获取许可,如果没有可用许可,线程会被阻塞并排队等待。当一个线程通过release()
方法释放了一个许可后,它会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。
a. 基于共享锁实现,默认非公平锁,基于cas实现。初始化时,将state初始化为许可数量
b. 每次获取许可时,判断需要获取的许可数量是否小于剩余的许可总量,小于则扣减state。
c. 若大于剩余许可数量,公平锁时进行等待,当一个线程通过release()
方法释放了一个许可后,它会从AQS中正在排队的第一个线程开始依次唤醒。非公平锁时,直接返回,由业务自行控制。
d. 每次归还许可时,将归还许可数量加至state
# ReentrantLock中tryLock()和lock()方法的区别
- tryLock使用非公平锁+cas实现,多线程之间直接竞争,无等待逻辑,抢到锁则返回true,否则返回false。
- lock有两种实现方式,公平锁和非公平锁,根据初始化ReentrantLock的时候决定,默认非公平锁。公平锁时,抢不到锁会入队等待。
# ReentrantLock中的公平锁和非公平锁的底层实现
- 公平锁的特点是按照请求锁的顺序来分配锁,即先到先得。在ReentrantLock中,通过构造函数可以选择创建一个公平锁。公平锁的底层实现使用了一个FIFO队列(First-In-First-Out),即等待队列。当一个线程请求锁时,如果锁已经被其他线程持有,请求线程会被放入等待队列的末尾,按照请求的顺序等待锁的释放。当锁被释放时,等待队列中的第一个线程会被唤醒并获得锁。
- 非公平锁不考虑请求锁的顺序,它允许新的请求线程插队并尝试立即获取锁,而不管其他线程是否在等待。在ReentrantLock中,默认情况下创建的是非公平锁。非公平锁的底层实现中,有一个等待队列,但它不会严格按照请求的顺序来分配锁,而是根据线程竞争锁的情况来判断是否立即分配给新的请求线程。
- 公平锁和非公平锁的区别在于tryAcquire方法,非公平锁直接进行一次抢占,抢到锁就立即执行,不会进行等待,而公平锁是优先入队等待
# sleep、wait、join、yield的区别
锁池 所有需要竞争同步锁的线程都会放在锁池中,比如当前对象已经被其中一个线程得到,则其他线程需在这个锁池进行等待,当前面的线程释放同步锁后锁池的线程去竞争同步锁,当某个线程得到后会进入就绪队列等待cpu资源分配
等待池 当调用wait方法后,线程会方法哦等待池中,等待池的线程不会去竞争同步锁,只有调用了notify或notifyAll后,等待池的线程才会竞争锁,notify是随机从等待池中选出一个线程放到锁池,而notifyAll是将等待池中的所有线程放入锁池
一.sleep()和wait()
1.sleep是Thread的静态本地方法,wait是Object的本地方法;
2.sleep不会释放锁,但wait会释放锁,将线程转到等待池;
3.sleep不依赖synchronized,但wait必须和synchronized配套使用;
4.sleep不需要唤醒,时间到了自动恢复,但wait比须要被notify或者notifyAll
5.sleep一般用于当前线程的休眠或轮询暂停操作,但wait是多线程的通信
6.sleep会让出CPU执行时间且强制上下文切换,而wait则不一定
二.join()和yield()
yield();线程主动让出cpu,进入到就绪状态重新竞争cpu,但有可能还是该线程获取cpu;
join();线程让出cpu进入阻塞状态,等子线程执行结束后才能继续执行
# Synchronized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录一下当前获取该锁的线程id,该线程下次再次尝试获取锁就可以直接获取到了
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争,就会升级到重量级锁。之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁通过自旋来实现,并不会阻塞线程。
- 如果自旋次数过多仍然没有获取到锁,则会升级到重量级锁,重量级锁会导致线程阻塞
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就不存在唤醒线程,自旋锁通过cas实现
# Synchronized和ReentrantLock的区别
- synchronized是一个关键字,ReentrantLock是一个类
- synchronized可以自动加锁和释放锁,ReentrantLock需要程序员手动加锁、释放锁
- synchronized是jvm级别的锁,ReentrantLock是API层面的锁
- synchronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
- synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock是通过代码中int类型的state来识别锁的状态
- synchronized在底层有一个锁升级的过程
# ThreadLocal的底层原理
- ThreadLocal是java中锁提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻,任意方法汇总获取到缓存数据
- ThreadLocal底层通过ThreadLocalMap来实现,每个Thread对象中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,value为需要缓存的指
- 如果在线程池中使用ThreadLocal不当会造成内存泄露,解决方案就是使用了ThreadLocal对象之后手动进行remove。线程泄露的原因是,每次使用完成ThreadLocal对象后,应该把Entry对象的key和value都回收掉,而线程对象是通过强应用指向ThreadLocalMap,ThreadLocalMap通过强引用指向Entry,由于在线程池中,线程不会被回收,Entry也就不会被回收。
# ThreadLocal内存泄露问题,如何避免
- 每次使用完成ThreadLocal对象后,应该把Entry对象的key和value都回收掉,而线程对象是通过强应用指向ThreadLocalMap,ThreadLocalMap通过强引用指向Entry。如果在线程池中,线程不会被回收,Entry也就不会被回收,所以会内存泄露
- 每次使用完成ThreadLocal对象后,手动进行remove
# Thread和Runnable
- Thread 和 Runnable 是 Java 中用于实现多线程的两种主要方式
- 继承关系
- Thread 是一个类,你可以创建 Thread 类的实例并通过继承来实现多线程。你的类可以直接扩展 Thread 类,并覆盖 run() 方法来定义线程的执行逻辑。
- Runnable 是一个接口,实现了该接口的类可以作为线程的任务被传递给一个线程对象执行。需要通过 Thread 类的构造函数将 Runnable 对象包装成线程。
- 单继承 vs 多实现:因为 Java 是单继承的语言,如果你选择继承 Thread 类,就无法继承其他类。使用 Runnable 接口可以避免这个限制,因为你的类可以实现其他接口或继承其他类。
- 资源消耗:创建一个新的 Thread 实例相对比较消耗资源,因为它涉及到创建新的线程对象。而通过实现 Runnable 接口,可以将多个线程共享同一个 Thread 对象,降低了资源消耗。
- 灵活性:实现 Runnable 接口更加灵活,因为你可以选择将同一个 Runnable 对象传递给多个线程,从而使多个线程执行相同的任务。相比之下,Thread 类的实例只能用于一个线程。
# 阿里一面:如何查看线程死锁
- 使用 jps 和 jstack
- jps 命令用于查看 Java 进程的列表和ID。
- 使用 jstack
查看该进程的线程堆栈信息。
- 使用工具: VisualVM、Arthas
# 阿里一面:线程之间如何进行通讯的
- 线程之间通信是通过共享数据和一些协调机制来实现。常见的有:共享变量、等待/通知机制、网络
# 拓展:进程间如何进行通讯
- 管道
- 匿名管道:用于具有亲缘关系的父子进程间的通信
- 命名管道:具有管道所具有的功能外,还允许无亲缘关系进程间的通信
- 信号:通过某种机制,向进程发送一个信号
- 消息队列:消息的链表,类似mq,只是在内存中实现的
- 共享内存:多个进程访问同一块内存空间
- 信号量:主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。
- 套接字:一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。同一机器中的进程还可以使用Unix domain socket(比如同一机器中 MySQL 中的控制台 mysql shell 和 MySQL 服务程序的连接),这种方式不需要经过网络协议栈,不需要打包拆包、计算校验和维护序号和应答等,比纯粹基于网络的进程间通信肯定效率更高。
# 并发、并行、串行
- 并发:交替执行
- 并行:同时执行
- 串行:按顺序执行
# 并发三大特性
- 原子性:是指一个操作cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行
- 可见性:指的是线程本地修改变量后,需要及时将变量刷回主存
- 有序性:虚拟机在进行代码编译是,对于那些改变顺序之后不会影响最终结果的代码,有可能改变其顺序。但实际上,有些代码重排序后,可能会出现线程安全问题。
# 对线程安全的理解
- 当多个线程访问同一个对象是,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获取到正确的结果,那就是线程安全的
# 京东二面:并发编程三要素
- 原子性:不可分割的操作, 多个操作保持同时成功同时失败
- 有序性:程序后执行的顺序和代码顺序保持一致
- 可见性:一个线程对共享变量的修改,另一个线程里面能看到
# 京东一面:Java死锁如何避免
破坏死锁成立的条件,破坏任何一个即可。
互斥条件 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
请求和保持条件 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
不剥夺条件 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
环路等待条件 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的 P0 正在等待一个 P1 占用的资源;P1正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源
# 京东一面:如果你提交任务时,线程池队列已满,这时会发生什么
- 如果未到达最大线程数,则新创建线程处理任务
- 如果已到达最大线程数,执行拒绝策略,默认丢弃
# 蚂蚁二面:volatile关键字,他是如何保证可见性,有序性
- 对于加了volatile的成员变量,在对这个变量进行修改时,会直接将cpu高速缓存汇总的数据写回主存,对这个变量的读取也是从主存中读取
- 在对volatile修饰的成员变量进行读写时,会插入内存屏障,内存屏障可以达到禁止指令重排序的效果,从而保证有序性
# 蚂蚁一面:简述线程池原理(线程池处理流程),FixedThreadPool用的阻塞队列是什么
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时
- 如果小于核心线程数,即使线程都处理空闲状态,也会创建线程来处理任务
- 当等于核心线程数,队列未满,那么任务会被加进队列
- 如果等于核心线程数,队列已满,并且小于最大线程数,会创建线程来处理任务
- 如果等于核心线程数,队列已满,并且等于最大线程数,那么通过handler中设定的拒绝策略来处理此任务
- 当大于核心线程数书,如果某线程空闲时间超过keepAliveTime,线程会被终止,通过这个来动态调整线程池中的参数
# 说说你对守护线程的理解
- 守护线程:
- 为所有非守护线程提供服务的线程;
- 当进程中没有用户线程后,不管是否有守护线程,进程都会退出。
- 守护线程中产生的子线程也是守护线程
- 守护线程必须在thread.start前设置,否则会抛异常
- 在java线程池中,守护线程会被转换为用户线程
- 应用场景:
- 为了其他线程提供服务支持,比如GC线程
- 在任何情况下,程序结束后,这个线程必须正常且立即关闭,就可以作为守护线程来使用
# 为什么使用线程池,参数解释
- 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗
- 提高响应速度;当有任务之后,直接就有现有的线程可用
- 提高线程的可管理性;线程是稀缺资源,使用线程池可以统一调配 参数
- 核心线程数:常驻内存的线程数
- 最大线程数:最大允许创建的线程数
- 线程存活时间:当超过核心线程数后,线程空闲该时间后,线程会退出
- 线程存活时间单位:时间单位
- 线程工厂:用户生产线程执行任务,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程
- 阻塞队列:用来存放待执行的任务
- 拒绝策略:当任务满了,且到达核心线程数后,会执行该拒绝策略
# 线程池线程复用的原理
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行
# 线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程
- 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前任务了,阻塞队列可以通过阻塞保留当前想要继续入队的队列。
- 阻塞队列可以保证队列中没有任务是阻塞获取任务的线程,使线程进行wait状态,释放cpu状态。
- 阻塞队列自带阻塞和唤醒功能,不需要额外处理
- 先添加队列是为了保证核心线程的利用率;且创建新线程的时候,需要加全局锁,其他线程都进行阻塞,影响整体效率。
# 线程的生命周期及状态
- 线程通常由几个状态组成:new(新建) -> runnable (就绪)-> running(运行中)-> blocked(阻塞) -> terminated(终止)
- 阻塞又分为三种状态
- 等待阻塞:线程在等待池,不会竞争锁,只有在notify和notifyAll被调用时,才会进入锁池
- 同步阻塞:运行中的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则该线程会进入锁池
- 其他阻塞:运行中的线程执行sleep或join方法, 或者发出了I/O请求,jvm会把线程置为阻塞状态。当sleep状态超时、join等待线程终止或超时、或I/O处理完毕是,线程冲入转入就绪状态、
← CPU缓存模型