前言
个人珍藏的80道Java多线程/并发经典面试题,因为篇幅太长,现在先给出1-10的答案解析哈,后面一起完善,并且上传github哈~
「公众号:捡田螺的小男孩」
1. synchronized的实现原理以及锁优化?
synchronized的实现原理
- synchronized作用于「方法」或者「代码块」,保证被修饰的代码在同一时间只能被一个线程访问。
- synchronized修饰代码块时,JVM采用「monitorenter、monitorexit」两个指令来实现同步
- synchronized修饰同步方法时,JVM采用「ACC_SYNCHRONIZED」标记符来实现同步
- monitorenter、monitorexit或者ACC_SYNCHRONIZED都是「基于Monitor实现」的
- 实例对象里有对象头,对象头里面有Mark Word,Mark Word指针指向了「monitor」
- Monitor其实是一种「同步工具」,也可以说是一种「同步机制」。
- 在Java虚拟机(HotSpot)中,Monitor是由「ObjectMonitor实现」的。ObjectMonitor体现出Monitor的工作原理~
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">ObjectMonitor() { _header = NULL; _count = 0; // 记录线程获取锁的次数 _waiters = 0, _recursions = 0; //锁的重入次数 _object = NULL; _owner = NULL; // 指向持有ObjectMonitor对象的线程 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
</pre>
锁优化
在讨论锁优化前,先看看JAVA对象头(32位JVM)中Mark Word的结构图吧~
Mark Word存储对象自身的运行数据,如「哈希码、GC分代年龄、锁状态标志、偏向时间戳(Epoch)」 等,为什么区分「偏向锁、轻量级锁、重量级锁」等几种锁状态呢?
- 偏向锁:在无竞争的情况下,把整个同步都消除掉,CAS操作都不做。
- 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。
- 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
- 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。
有兴趣的朋友们可以看看我这篇文章: Synchronized解析——如果你愿意一层一层剥开我的心[1]
2. ThreadLocal原理,使用注意点,应用场景有哪些?
回答四个主要点:
- ThreadLocal是什么?
- ThreadLocal原理
- ThreadLocal使用注意点
- ThreadLocal的应用场景
ThreadLocal是什么?
ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">//创建一个ThreadLocal变量 static ThreadLocal<String> localVariable = new ThreadLocal<>();
</pre>
ThreadLocal原理
ThreadLocal内存结构图:
由结构图是可以看出:
- Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
- ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
对照着几段关键源码来看,更容易理解一点哈~
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">public class Thread implements Runnable { //ThreadLocal.ThreadLocalMap是Thread的属性 ThreadLocal.ThreadLocalMap threadLocals = null; }
</pre>
ThreadLocal中的关键方法set()和get()
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> public void set(T value) { Thread t = Thread.currentThread(); //获取当前线程t ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap if (map != null) map.set(this, value); //K,V设置到ThreadLocalMap中 else createMap(t, value); //创建一个新的ThreadLocalMap } public T get() { Thread t = Thread.currentThread();//获取当前线程t ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap if (map != null) { //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
</pre>
ThreadLocalMap的Entry数组
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }
</pre>
所以怎么回答「ThreadLocal的实现原理」?如下,最好是能结合以上结构图一起说明哈~
ThreadLocal 内存泄露问题
先看看一下的TreadLocal的引用示意图哈,
弱引用比较容易被回收。因此,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会「造成了内存泄漏问题」。
如何「解决内存泄漏问题」?使用完ThreadLocal后,及时调用remove()方法释放内存空间。
ThreadLocal的应用场景
- 数据库连接池
- 会话管理中使用
3. synchronized和ReentrantLock的区别?
我记得校招的时候,这道面试题出现的频率还是挺高的~可以从锁的实现、功能特点、性能等几个维度去回答这个问题,
- 「锁的实现:」 synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()和unlock()方法配合try/finally 语句块来完成。)
- 「性能:」 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6开始,增加了适应性自旋、锁消除等,两者性能就差不多了。
- 「功能特点:」 ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。
4. 说说CountDownLatch与CyclicBarrier区别
- CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
-
CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。
举个例子吧:
5. Fork/Join框架的理解
Fork/Join框架需要理解两个点,「分而治之」和「工作窃取算法」。
「分而治之」
以上Fork/Join框架的定义,就是分而治之思想的体现啦「工作窃取算法」
把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~
工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。
6. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
看看Thread的start方法说明哈~
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> `/**
- Causes this thread to begin execution; the Java Virtual Machine
- calls the <code>run</code> method of this thread.
- <p>
- The result is that two threads are running concurrently: the
- current thread (which returns from the call to the
- <code>start</code> method) and the other thread (which executes its
- <code>run</code> method).
- <p>
- It is never legal to start a thread more than once.
- In particular, a thread may not be restarted once it has completed
- execution.
- @exception IllegalThreadStateException if the thread was already
started.
- @see #run()
- @see #stop()
*/
public synchronized void start() {
......
}` </pre>
JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~ 「为什么我们不能直接调用run()方法?」 如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。
7. CAS?CAS 有什么缺陷,如何解决?
CAS,Compare and Swap,比较并交换;
CAS有什么缺陷?
「ABA 问题」
可以通过AtomicStampedReference「解决ABA问题」,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。
「循环时间长开销」
很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~
「只能保证一个变量的原子操作。」
可以通过这两个方式解决这个问题:
有兴趣的朋友可以看看我之前的这篇实战文章哈~ CAS乐观锁解决并发问题的一次实践[2]
9. 如何保证多线程下i++ 结果正确?
- 使用循环CAS,实现i++原子操作
- 使用锁机制,实现i++原子操作
- 使用synchronized,实现i++原子操作
没有代码demo,感觉是没有灵魂的~ 如下:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`/**
- @Author 捡田螺的小男孩
*/
public class AtomicIntegerTest {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
testIAdd();
}
private static void testIAdd() throws InterruptedException {
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 1000; i++) {
executorService.execute(() -> {
for (int j = 0; j < 2; j++) {
//自增并返回当前值
int andIncrement = atomicInteger.incrementAndGet();
System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement);
}
});
}
executorService.shutdown();
Thread.sleep(100);
System.out.println("最终结果是 :" + atomicInteger.get());
}
}` </pre>
运行结果:
<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">... 线程:pool-1-thread-1 count=1997 线程:pool-1-thread-1 count=1998 线程:pool-1-thread-1 count=1999 线程:pool-1-thread-2 count=315 线程:pool-1-thread-2 count=2000 最终结果是 :2000
</pre>
10. 如何检测死锁?怎么预防死锁?死锁四个必要条件
死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。如图感受一下:「死锁的四个必要条件:」
- 互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。
- 占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
- 非抢占:不能强行抢占进程中已占有的资源。
- 循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。
「如何预防死锁?」
- 加锁顺序(线程按顺序办事)
- 加锁时限 (线程请求所加上权限,超时就放弃,同时释放自己占有的锁)
- 死锁检测
参考与感谢
牛顿说,我之所以看得远,是因为我站在巨人的肩膀上~ 谢谢以下各位前辈哈~
- 面试必问的CAS,你懂了吗?[3]
- Java多线程:死锁[4]
- ReenTrantLock可重入锁(和synchronized的区别)总结[5]
- 聊聊并发(八)——Fork/Join 框架介绍[6]
个人公众号
- 觉得写得好的小伙伴给个点赞+关注啦,谢谢~
- 如果有写得不正确的地方,麻烦指出,感激不尽。
- 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻
- github地址:https://github.com/whx123/JavaHome
Reference
[1]
Synchronized解析——如果你愿意一层一层剥开我的心: https://juejin.im/post/5d5374076fb9a06ac76da894#comment [2]
CAS乐观锁解决并发问题的一次实践: https://juejin.im/post/5d0616ade51d457756536791 [3]
面试必问的CAS,你懂了吗?: https://blog.csdn.net/v123411739/article/details/79561458 [4]
Java多线程:死锁: https://www.cnblogs.com/xiaoxi/p/8311034.html [5]
ReenTrantLock可重入锁(和synchronized的区别)总结: https://blog.csdn.net/qq838642798/article/details/65441415 [6]
聊聊并发(八)——Fork/Join 框架介绍: https://www.infoq.cn/article/fork-join-introduction
本文使用 mdnice 排版