前言

并发编程是面试中必问的知识点之一,所以本文整理了一些最为常见的并发面试题,一起来看吧~

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的工作原理~

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 ;}

ObjectMonitor的几个关键属性 _count、_recursions、_owner、_WaitSet、 _EntryList 体现了monitor的工作原理

锁优化

在讨论锁优化前,先看看JAVA对象头(32位JVM)中Mark Word的结构图吧~

Mark Word存储对象自身的运行数据,如「哈希码、GC分代年龄、锁状态标志、偏向时间戳(Epoch)」 等,为什么区分「偏向锁、轻量级锁、重量级锁」等几种锁状态呢?

在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为「重量级锁」。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略。

  • 偏向锁:在无竞争的情况下,把整个同步都消除掉,CAS操作都不做。

  • 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。

  • 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式

  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

举个例子,买门票进动物园。老师带一群小朋友去参观,验票员如果知道他们是个集体,就可以把他们看成一个整体(锁租化),一次性验票过,而不需要一个个找他们验票。

  • 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。

有兴趣的朋友们可以看看我这篇文章:Synchronized解析——如果你愿意一层一层剥开我的心[1]

2. ThreadLocal原理,使用注意点,应用场景有哪些?

回答四个主要点:

  • ThreadLocal是什么?

  • ThreadLocal原理

  • ThreadLocal使用注意点

  • ThreadLocal的应用场景

ThreadLocal是什么?

ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();

ThreadLocal原理

ThreadLocal内存结构图:

由结构图是可以看出:

  • Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。

  • ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。

对照着几段关键源码来看,更容易理解一点哈~

public class Thread implements Runnable {//ThreadLocal.ThreadLocalMap是Thread的属性ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal中的关键方法set()和get()

    public void set(T value) {Thread t = Thread.currentThread(); //获取当前线程tThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMapif (map != null)map.set(this, value); //K,V设置到ThreadLocalMap中elsecreateMap(t, value); //创建一个新的ThreadLocalMap}public T get() {Thread t = Thread.currentThread();//获取当前线程tThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMapif (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();}

ThreadLocalMap的Entry数组

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;}}
}

所以怎么回答「ThreadLocal的实现原理」?如下,最好是能结合以上结构图一起说明哈~

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。

  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。

  • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

ThreadLocal 内存泄露问题

先看看一下的TreadLocal的引用示意图哈,

ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用,如下

弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。

弱引用比较容易被回收。因此,如果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 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

  • synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition()方法实现。

  • ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁。

4. 说说CountDownLatch与CyclicBarrier区别

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;

  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

举个例子吧:

  • CountDownLatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。

  • CyclicBarrier:多名短跑运动员要开始田径比赛,只有等所有运动员准备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。

5. Fork/Join框架的理解

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join框架需要理解两个点,「分而治之」「工作窃取算法」

「分而治之」

以上Fork/Join框架的定义,就是分而治之思想的体现啦

「工作窃取算法」

把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~

工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

6. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

看看Thread的start方法说明哈~

    /*** 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() {......}

JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~ 「为什么我们不能直接调用run()方法?」如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。

7. CAS?CAS 有什么缺陷,如何解决?

CAS,Compare and Swap,比较并交换;

CAS 涉及3个操作数,内存地址值V,预期原值A,新值B;如果内存位置的值V与预期原A值相匹配,就更新为新值B,否则不更新

CAS有什么缺陷?

「ABA 问题」

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

可以通过AtomicStampedReference「解决ABA问题」,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。

「循环时间长开销」

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。

很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

「只能保证一个变量的原子操作。」

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

可以通过这两个方式解决这个问题:

  • 使用互斥锁来保证原子性;

  • 将多个变量封装成对象,通过AtomicReference来保证原子性。

有兴趣的朋友可以看看我之前的这篇实战文章哈~CAS乐观锁解决并发问题的一次实践[2]

9. 如何保证多线程下i++ 结果正确?

  • 使用循环CAS,实现i++原子操作

  • 使用锁机制,实现i++原子操作

  • 使用synchronized,实现i++原子操作

没有代码demo,感觉是没有灵魂的~ 如下:

/***  @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());}}

运行结果:

...
线程: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

10. 如何检测死锁?怎么预防死锁?死锁四个必要条件

死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。如图感受一下:「死锁的四个必要条件:」

  • 互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。

  • 占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。

  • 非抢占:不能强行抢占进程中已占有的资源。

  • 循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。

「如何预防死锁?」

  • 加锁顺序(线程按顺序办事)

  • 加锁时限 (线程请求所加上权限,超时就放弃,同时释放自己占有的锁)

  • 死锁检测

参考与感谢

牛顿说,我之所以看得远,是因为我站在巨人的肩膀上~ 谢谢以下各位前辈哈~

  • 面试必问的CAS,你懂了吗?[3]

  • Java多线程:死锁[4]

  • ReenTrantLock可重入锁(和synchronized的区别)总结[5]

  • 聊聊并发(八)——Fork/Join 框架介绍[6]

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

往期推荐

被问哭了,一位小姐姐的阿里面经!(附部分答案)

不要一把梭了,这才是SQL优化的正确姿势!|原创干货

关注下方二维码,每一天都有干货!

最常见并发面试题整理!(速度收藏)相关推荐

  1. 常见的面试题整理 -python

    常见的面试题整理 在这里插入代码片 #二分查找def binarySearch(alist, item):first=0;last=len(alist)-1;while first <= las ...

  2. 金九银十面试怒拿6个offer——测试开发面试题整理

    金九银十面试怒拿6个offer--测试开发面试题整理 1.软件测试的流程是什么? 2.测试用例主要有哪些元素? 3.软件测试有什么策略和阶段? 4.黑盒测试和白盒测试是什么?二者有什么区别? 5.软件 ...

  3. iOS开发面试题整理

    前言 本文借鉴整理了iOS高级开发常见的面试题,并且分博客一一分析,希望能和大家一起进步学习. 欢迎大家关注我的 Github?以及相关博客 Github.io 简书 大家的鼓励是我前进的动力? iO ...

  4. 后端开发面试题整理 2019

    在一个公众号看到了下面的面试题整理,觉得不错,就在这篇博客记录下来,寒假这段时间会抽空去逐个解决这些面试问题,并用博客逐个记录这些面试题的答案,请关注后续---- JAVA集合题 Collection ...

  5. 【面试题】测试工程师/自动化测试/测试开发面试题整理

    [自己做个笔记,大家有需要的可以看看,有问题欢迎指出] 文章目录 基础面试题 1.设计用例的方法.依据有哪些 2.软件的生命周期 3.测试流程 4.bug生命周期 5.你在测试中发现了一个bug,但是 ...

  6. 前端开发面试题整理(摘抄)

    1.Doctype作用?标准模式与兼容模式各有什么区别? 声明位于位于HTML文档中的第一行,告知浏览器的解析器用什么文档标准解析这个文档.DOCTYPE不存在或格式不正确会导致文档以兼容模式呈现.标 ...

  7. python运维面试题_PYTHON运维开发面试题整理

    1.请使用Linux系统命令统计出establish状态链接数有多少? netstat -an |grep ESTABLISHED |wc -l -a (all)显示所有选项,默认不显示LISTEN相 ...

  8. 腾讯后台开发面试题--整理1

    可参照这篇博客一起看:http://blog.chinaunix.net/uid-29867011-id-4508083.html linux和os: netstat tcpdump ipcs ipc ...

  9. 2020网易大数据开发面试题整理 (提前批) (持续更新)

    博主近期在整理各大厂面试题,每天会更新一点,没有答案的题目,表示博主也不知道,有答案的题目也不一定正确,希望各位同学自主判断,如果博主有写错的地方,欢迎在评论区补充 1.spark数据倾斜怎么引起的? ...

最新文章

  1. java k线绘制,用Java绘制K线图[Java编程]
  2. 智能记忆功能nest_如何设置和安装Nest Protect智能烟雾报警器
  3. 1941套站点模版,终生收藏,个个精品
  4. linux磁盘fio压力测试,fio命令 – 对磁盘进行压力测试和验证
  5. SQL Server 连接查询(内连接查询)
  6. java spfa_SPFA算法简介
  7. cordova 美洽_phonegap-cordova-美洽客服插件-ios
  8. 逻辑斯蒂回归与梯度下降法
  9. 完整百度登录源代码(2015年1月27日更新)
  10. SAP OOALV-SD模块实战开发案例(增删改查)
  11. html图片右边加竖线,插入Html竖线代码
  12. 【魔店】拼多多店群玩法是什么?如何运营店铺?
  13. 软件体系结构与设计模式——课程总体介绍(01-03)
  14. 攻防世界 [简单] 凯撒大帝在培根里藏了什么
  15. 利用 element ui 进度条实现渐变 条纹进度条
  16. 江湖救急(处理域名未备案网站问题)
  17. 替换Android手机的开机动画,安卓技术宅系列之修改手机开机动画
  18. Linux命令查询服务器名称和型号
  19. 【OpenCV入门到精通之五】视频固定位置叠加图片或者另一个视频
  20. 魔力宝贝手机版服务器暂时无法登陆,《魔力宝贝手机版》服务器全面开启!

热门文章

  1. linux文件系统概念目录结构
  2. 【转】mip-semi-fixed 走走又停停
  3. 你不知道的js中关于this绑定机制的解析[看完还不懂算我输]
  4. spark DAGScheduler、TaskSchedule、Executor执行task源码分析
  5. 用vim + xdebug 来追踪thinkphp的执行过程
  6. NFS部署及优化(一)
  7. Linux学习第三步(Centos7安装mysql5.7数据库)
  8. 形象易懂讲解算法I——小波变换
  9. hadoop和kerberos的整合总结
  10. [置顶] 任务三 主题、样式