摘要

现如今,不管是应届毕业生还是工作了三五年之内的工程师,在面试招聘的时候JUC并发编程的是必须掌握的一个技能,否者你将会被面试官玩弄。本博文将整理有关于的JUC的大厂面试问题和答案。帮助大家在面试过程中能够回答面试官问题的一二。同时本人也总结相关的面试问题的在相关文档中,如果有需要的小伙伴,请关注文档,本人将不断的更新的JUC并发编程的相关面试问题。帮助小伙伴早日进入大厂。

一、JUC并发面试问题与答案

JUC并发编程面试问题 · 语雀

CAS是怎么实现线程安全的?

线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

举个栗子:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“庄小焱”,拿到值了,我们准备修改成name=“傻逼”,在修改之前我们判断一下,原来的name是不是等于“庄小焱”,如果被其他线程修改就会发现name不等于“庄小焱”,我们就不进行操作,如果原来的值还是庄小焱,我们就把name修改为“傻逼”,至此,一个流程就结束了。

CAS场景下出现ABA问题?

  • 1.线程1读取了数据A
  • 2.线程2读取了数据A
  • 3.线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
  • 4.线程3读取了数据B
  • 5.线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
  • 6.线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值

循环时间长开销大的问题:是因为CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。

只能保证一个共享变量的原子操作:CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不行了,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。

乐观锁在项目开发中的实践,有么?

有的就比如我们在很多订单表,流水表,为了防止并发问题,就会加入CAS的校验过程,保证了线程的安全,但是看场景使用,并不是适用所有场景,他的优点缺点都很明显。

那开发过程中ABA你们是怎么保证的?

加标志位,例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。举个栗子∶现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段vision。

之前不能防止ABA的正常修改:

update table set value = newValue where value = #{oldValue}//oldValue就是我们执行前查询出来的值

带版本号能防止ABA的修改:

update table set value = newValue , vision = vision + 1 where value = #{oldValue} and vision = #{vision}//判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样

除了版本号,像什么时间戳,还有JUC工具包里面也提供了这样的类,想要扩展的小伙伴可以去了解一下。

聊一下悲观锁?

我们先聊下JVM层面的synchronized:synchronized加锁,synchronized是最常用的线程同步手段之一,上面提到的CAS是乐观锁的实现,synchronized就是悲观锁了。

它是如何保证同一时刻只有一个线程可以进入临界区呢?

synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

以前我们一直锁synchronized是重量级的锁,为啥现在都不提了?

但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重,Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。针对synchronized获取锁的方式,JVM使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

还有其他的同步手段么?

ReentrantLock但是在介绍这玩意之前,我觉得我有必要先介绍AQS(AbstractQueuedSynchronizer) 。AQS:也就是队列同步器,这是实现 ReentrantLock的基础。AQS有一个state标记位,值为1时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表。

当获得锁的线程需要等待某个条件时,会进入condition 的等待队列,等待队列可以有多个。当condition条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。ReentrantLock就是基于AQS 实现的,如下图所示,ReentrantLock内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。和ReentrantLock实现方式类似,Semaphore也是基于AQS的,差别在于ReentrantLock是独占锁,Semaphore是共享锁。

从图中可以看到,ReentrantLock里面有一个内部类Sync,Sync继承AQS (AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

package 计算机程序算法分类.单例设计模式;/*** @Classname Lock* @Description TODO* @Date 2021/5/10 21:39* @Created by xjl*/
public class Lock {/*** @description TODO  修饰实例方法,对当前实例对象this加锁* @param: null* @date: 2021/5/10 21:41* @return:* @author: xjl*/public class Synchronized1 {public synchronized void husband() {}}/*** @description TODO 修饰静态方法,对当前类的Class对象加锁* @param: null* @date: 2021/5/10 21:42* @return:* @author: xjl*/public class Synchronized2 {public void husband() {synchronized (Synchronized2.class) {}}}/*** @description TODO 制定几所对象 给对象加锁* @param: null* @date: 2021/5/10 21:45* @return:* @author: xjl*/public class Synchronized3 {public void husband() {synchronized (new Synchronized3()) {}}}}

synchronized和lock的区别?

synchronized 竞争锁时会一直等待

ReentrantLock 可以尝试获取锁,并得到获取结果

synchronized 获取锁无法设置超时

ReentrantLock 可以设置获取锁的超时时间

synchronized 无法实现公平锁

ReentrantLock 可以满足公平锁,即先等待先获取到锁

synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll()

ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法

synchronized 是JVM 层面实现的

ReentrantLock 是 JDK 代码层面实现

synchronized 在加锁代码块执行完或者出现异常,自动释放锁。

ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放

synchronized和volatile的区别?

synchronized

volatile

synchronized 可以修饰类、方法、变量。

volatile 是变量修饰符

而 synchronized则可以保证变量的修改可见性和原子性

volatile 仅能实现变量的修改可见性,不能保证原子性

synchronized 可能会造成线程的阻塞

volatile 不会造成线程的阻塞

synchronized标记的变量可以被编译器优化

volatile标记的变量不会被编译器优化

Synchronized是重量级的锁

volatile关键字是线程同步的轻量级实现

ThreadLocal

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring的事务主要是ThreadLocal和AOP去做实现的,我这里提一下,大家知道每个线程自己的链接是靠ThreadLocal保存的就好了,继续的细节我会在Spring章节细说的,暖么?

之前我们上线后发现部分用户的日期居然不对了,排查下来是 SimpLeDatarormat的错误,调用了SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SImpleuataromaLpaS把S5先调用Calendar.clear () ,然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat ?所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat ,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

package 计算机程序算法分类.单例设计模式;/*** @Classname ThreadLocalTest* @Description TODO* @Date 2021/5/11 10:11* @Created by xjl*/
public class ThreadLocalTest {public static void main(String[] args) {ThreadLocal<String> localName = new ThreadLocal<>();localName.set("庄小焱");String mame = localName.get();localName.remove();}public void set(T value) {Thread t = Thread.currentThread();//获取当前线程ThreadLocalMap map = getMap(t);//获取ThreadLocalmap对象if (map != null) {//校验是否为空map.set(this, value);//部位空的时候设置的值} else {createMap(t, value);//为空的时候创建一个map对象}}
}

ThreadLocalMap底层结构是怎么样子的呢?

为什么需要数组呢?没有了链表怎么解决Hash冲突呢?

用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。

那么是不是说ThreadLocal的实例以及其值存放在栈上呢?

其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有)﹐而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

如果我想共享线程的ThreadLocal数据怎么办?

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal 的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。在子线程中我是能够正常输出那一行日志的,这也是我之前面试视频提到过的父子线程数据传递的问题。

传递的逻辑很简单,我在开头Thread代码提到threadLocals的时候,你们再往下看看我刻意放了另外一个变量:

那为什么ThreadLocalMap的key要设计成弱引用?

key不设置成弱引用的话就会造成和entry中value—样内存泄漏的场景。补充一点:ThreadLocal的不足,我觉得可以通过看看netty的fastThreadLocal来弥补,大家有兴趣可以康康。

JMM内存模型

其实早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence) 。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。Java内存模型(JavaMemoryModel))描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量,存储到内存和从内存中读取变量这样的底层细节。

为啥加锁可以解决可见性问题呢?

因为某一个线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从王内存拷贝共学效重最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的。

volatile的实现原理?

每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写会了,他其他已经读取的线程的变量副本就会失效了,需要都数据进行操作又要再次去主内存中读取了。volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

MESI(缓存一致性协议)

当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,发出信号通知其他CPU将该变量的缓存行置为无效状态,当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

总线嗅探机制

每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

总线风暴

由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分

禁止指令重排序

什么是重排序?:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?

—般重排序可以分为如下三种:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;指令级并行的重排序。
  • 指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

那Volatile是怎么保证不会被执行重排序的呢?

内存屏障:java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

需要注意的是: volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。

happens-before

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。如果现在我的变了falg变成了false,那么后面的那个操作,一定要知道我变了。聊了这么多,我们要知道Volatile是没办法保证原子性的,一定要保证原子性,可以使用其他方法。

无法保证原子性

就是一次操作,要么完全成功,要么完全失败。假设现在有N个线程对同一个变量进行累加也是没办法保证结果是对的,因为读写这个过程并不是原子性的。要解决也简单,要么用原子类,比如AtomicInteger,要么加锁(记得关注Atomi c的底层)。

volatile与synchronized的区别

volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。

volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。

volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

总结

  • 1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步。
  • 2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
  • 3. volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
  • 4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主存中读取。
  • 5. volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。
  • 6. volatile可以使得long和double的赋值是原子的。
  • 7. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点︰吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

博文参考

并发编程——JUC并发大厂面试问题相关推荐

  1. 并发编程——JUC并发编程知识脑图

    摘要 并发编程在软件编程中尤为突出和重要,在当今面试或工作中也是不可缺少的.作为一名高级java开发工程师,并发编程的技能已经成为了重要的一项.本博文将详细介绍并发编程中的知识点和知识脑图,帮助大家更 ...

  2. 多线程进阶(并发编程JUC)

    多线程进阶(并发编程JUC) 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 并发编程JUC 1. 基础知识 什么是JUC(Java并发包)?并发编程的本质(充分利用CPU的资 ...

  3. 突击并发编程JUC系列-ReentrantReadWriteLock

    突击并发编程JUC系列演示代码地址: https://github.com/mtcarpenter/JavaTutorial 本章节将学习 ReentrantReadWriteLock(读写锁),Re ...

  4. 并发编程JUC系列及部分问题

    什么是 CAS 吗? CAS(Compare And Swap)指比较并交换.CAS算法CAS(V, E, N)包含 3 个参数,V 表示要更新的变量,E 表示预期的值,N 表示新值.在且仅在 V 值 ...

  5. 【檀越剑指大厂--并发编程】并发编程总结

    并发编程 一.并发基础 1.什么是并行和并发? 并行,表示两个线程同时(同一时间)做事情. 并发,表示一会做这个事情,一会做另一个事情,存在着调度. 单核 CPU 不可能存在并行(微观上). 2.什么 ...

  6. (2021-02-04)并发编程简介-并发编程(1)

    关于并发编程,不光是面试经常问到,在实际的操作过程中,也会经常用到.所以一来是为了加深自己的印象,二来也希望能和大家公共学习.不对的地方请斧正,谢谢! 1.程序.进程.线程的关联和区别? 这是个老生常 ...

  7. Java并发编程:并发容器之CopyOnWriteArrayList(转载)

    Java并发编程:并发容器之CopyOnWriteArrayList(转载) 原文链接: http://ifeve.com/java-copy-on-write/ Copy-On-Write简称COW ...

  8. 【Java并发编程】并发编程大合集

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容 ...

  9. 【转】Java并发编程:并发容器之ConcurrentHashMap

    JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了 ...

最新文章

  1. python语言怎么用-科学家们是如何使用 Python 语言的?
  2. PyQt4编程之简短地做出多个选择框
  3. 如何使ArrayList 线程安全
  4. 【Elasticsearch】 海量 分词器 在线地址 与文档
  5. P1066 2^k进制数
  6. SpringBoot + Mybatis 多模块( module )项目搭建教程
  7. Angular2 的 View Encapsulation(样式封装)
  8. 【优化布局】基于matlab GUI遗传算法求解PCB元器件布局优化问题【含Matlab源码 694期】
  9. 修改linux的防火墙
  10. 智慧档案馆之区级档案库房温湿度智能化监控系统案例(一)
  11. 清华姚班和100个“张小龙” | 中国AI天才养成计划
  12. java单例模式——双重检查
  13. 戴尔计算机无法安装Win10,示例戴尔无法开机如何重装win10
  14. 交换游戏(状态压缩、记忆化搜索)
  15. 关于H5唤起地图导航小结
  16. 51单片机数码管表白
  17. 子元素padding一般不会撑开父元素盒子大小
  18. 2017中山大学计算机考研科目,2017年中山大学0812计算机科学与技术考研参考书目推荐...
  19. 网站如何诊断,完整的网站诊断方法!
  20. 铲雪车(snow) UVA10203 Snow Clearing 题解 优化版

热门文章

  1. PTA 10-98 3-1-(a) 查询电影“M3”中的男影星
  2. 解决Mac Preview不能打开eps文件
  3. 【Leetcode】516. 最长回文子序列
  4. idea改类名快捷键_学习IntelliJ IDEA之快捷键设置
  5. python登录网站后爬取数据_快速爬取登录网站数据
  6. python加密解密 sha256_Python下实现的RSA加密/解密及签名/验证功能示例
  7. 昆虫与钢琴:岩井俊雄
  8. Android 定位服务和位置权限
  9. 网易评论盖楼php,DedeCMS评论引用美化:仿腾讯/网易盖楼效果
  10. Ubuntu20.04搜狗输入法官方安装指南实操