首页 > 编程语言 >并发编程——JUC并发大厂面试问题

并发编程——JUC并发大厂面试问题

时间:2023-04-04 18:05:37浏览次数:46  
标签:JUC 缓存 变量 synchronized 编程 并发 线程 内存 volatile


摘要

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

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

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

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

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

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

CAS场景下出现ABA问题?

并发编程——JUC并发大厂面试问题_并发编程

  • 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 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

并发编程——JUC并发大厂面试问题_缓存_02

并发编程——JUC并发大厂面试问题_缓存_03

并发编程——JUC并发大厂面试问题_并发编程_04

还有其他的同步手段么?

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

并发编程——JUC并发大厂面试问题_重排序_05

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

并发编程——JUC并发大厂面试问题_缓存_06

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

并发编程——JUC并发大厂面试问题_并发编程_07

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()) {

            }
        }
    }

}

并发编程——JUC并发大厂面试问题_缓存_08

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对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

并发编程——JUC并发大厂面试问题_缓存_09

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底层结构是怎么样子的呢?

并发编程——JUC并发大厂面试问题_数据_10

并发编程——JUC并发大厂面试问题_数据_11

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

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

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

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

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

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

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

并发编程——JUC并发大厂面试问题_并发编程_12

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

并发编程——JUC并发大厂面试问题_缓存_13

并发编程——JUC并发大厂面试问题_重排序_14

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

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

JMM内存模型

并发编程——JUC并发大厂面试问题_重排序_15

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

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

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

volatile的实现原理?

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

MESI(缓存一致性协议)

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

总线嗅探机制

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

总线风暴

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

禁止指令重排序

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

并发编程——JUC并发大厂面试问题_数据_16

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

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

as-if-serial

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

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

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

并发编程——JUC并发大厂面试问题_数据_17

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

并发编程——JUC并发大厂面试问题_并发编程_18

并发编程——JUC并发大厂面试问题_数据_19

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,缓存,变量,synchronized,编程,并发,线程,内存,volatile
From: https://blog.51cto.com/u_13643065/6169220

相关文章

  • SQL Server 数据库T-SQL编程
    1、T-SQL编程通过SQL语句来完成业务的处理,执行编写好的sql语句,就可以完成业务处理。2、局部变量SQLserver中变量分为:局部变量和全局变量。全局变量,在全局可用,系统自定义,用户不可以定义全局变量,用不不可以修改全局变量,全局变量以“@@”开头局部变量就是一个能够拥有......
  • 系统化学习前端之JavaScript(ES6:异步编程)
    前言JavaScript异步编程这块东西比较多,涉及到宏任务和微任务,所以单开一个篇幅梳理一下。同步和异步同步和异步是一种宏观概念,具体表现在JavaScript中,是同步任务和异步任务,即同步函数和异步函数。同步同步指函数在JavaScript同步执行。同步函数执行过程:A函数进入函数调......
  • 学了这么久的高并发编程,连Java中的并发原子类都不知道?
    摘要:保证线程安全是Java并发编程必须要解决的重要问题,本文和大家聊聊Java中的并发原子类,看它如何确保多线程的数据一致性。本文分享自华为云社区《学了这么久的高并发编程,连Java中的并发原子类都不知道?这也太Low了吧》,作者:冰河。今天我们一起来聊聊Java中的并发原子类。在 j......
  • Apache DB Utils教程_编程入门自学教程_菜鸟教程-免费教程分享
    教程简介ApacheCommonsDBUtils入门教程-从基本到高级概念的简单简单步骤熟悉ApacheCommonsDBUtils,其中包括概述,环境设置,第一个应用程序,基本CRUD示例,创建,读取,更新,删除查询,DBUtils对象,QueryRunner,AsyncQueryRunner,ResultSetHandler,BeanHandler,ArrayListHandler,BeanListHandle......
  • 《Python编程快速上手—让繁琐工作自动化》实践项目答案:第六章
    实践项目表格打印编写一个名为printTabel()的函数,它接受字符串的列表的列表,将它显示在组织良好的表格中,每列右对齐,假定所有内层列表都包含同样数目的字符串,例如:你的printTable()函数将打印出:点击查看代码tableData=[['apples','oranges','cherries','banana'],......
  • 高并发系统设计——API网关技术选型
    摘要你的垂直电商系统在经过微服务化拆分之后,已经运行了一段时间了,系统的扩展性得到了很大的提升,也能够比较平稳地度过高峰期的流量了。不过最近你发现,随着自己的电商网站知名度越来越高,系统迎来了一些“不速之客”,在凌晨的时候,系统中的搜索商品和用户接口的调用量,会有激剧的上升,持......
  • 高并发系统设计——负载均衡技术选型
    摘要高并发系统设计的三个通用方法:缓存、异步和横向扩展,不过在实际的工作中,你经常使用的负载均衡的组件应该算是Nginx,它的作用是承接前端的HTTP请求,然后将它们按照多种策略,分发给后端的多个业务服务器上。这样,我们可以随时通过扩容业务服务器的方式,来抵挡突发的流量高峰。与DNS......
  • 函数式编程杂谈
    vivo互联网技术微信公众号作者:张文博比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断演进,逐层推导出复杂的运算。本文通过函数式编程的一些趣味用法来阐述学习函数式编程的奇妙之处。一、编程范式综述编程是为了解决问......
  • 详细介绍Glib 主事件循环轻度分析与编程应用
    glib是一个跨平台、用C语言编写的若干底层库的集合。编写案例最好能够结合glib源码,方便随时查看相关函数定义。glib实现了完整的事件循环分发机制。有一个主循环负责处理各种事件。事件通过事件源描述,常见的事件源文件描述符(文件、管道和socket)超时idle事件当然,也可以自......
  • Tomcat 9.0.26 高并发场景下DeadLock问题排查与修复
    vivo互联网技术微信公众号 作者:黄卫兵、陈锦霞一、Tomcat容器9.0.26版本Deadlock问题1.1问题现象1.1.1 发生Deadlock的背景某接口/get.do压测,3分钟后,成功事务数TPS由1W骤降至0。1.1.2 Tomcat服务器出现大量的CLOSE_WAIT被压测服务器,出现TCPCLOSE_WAIT状态个数在200~......