一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

2年前 (2022) 程序员胖胖胖虎阿
302 0 0

Java 并发面试题总结(2022最新版)

关于作者
🐶 程序猿周周
⌨️ 短视频小厂BUG攻城狮
🤺 如果文章对你有帮助,记得关注、点赞、收藏,一键三连哦,你的支持将成为我最大的动力


关于本文
本文是 Java 面试总结系列的第9️⃣篇文章,该专栏将整理和梳理笔者作为 Java 后端程序猿在日常工作以及面试中遇到的实际问题,通过这些问题的系统学习,也帮助笔者顺利拿到阿里、字节、华为、快手等Offer,也祝愿大家能够早日斩获自己心仪的Offer。由于笔者能力有限,如若有错误或疏忽还望各位大佬们不吝指出…

标题 地址
MySQL数据库面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/122910606
Redis面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/122934938
计算机网络面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/122973684
操作系统面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/122994599
Linux面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/122994862
Spring面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123016872
Java基础面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123080189
Java集合面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123171501
Java并发面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123266624
Java虚拟机面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123412605
Java异常面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123462676
设计模式面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123490442
Dubbo面试题总结(2022版) https://blog.csdn.net/adminpd/article/details/123538243

文章目录

  • 1 并发基础
      • 1、并发编程三要素是什么?
      • 2、什么是 Happen-before 原则?
  • 2 线程和线程池
    • 2.1 线程
      • 1、Java 线程有几种状态?
      • 2、Java 线程状态是如何切换的?
      • 3、Java 如何创建一个线程?
      • 4、Callable 和 Runnable 区别?
    • 2.2 线程池
      • 3、为什么使用线程池?
      • 4、线程池有哪些参数?
      • 5、讲讲线程增长的过程?
      • 6、线程池有哪些拒绝策略?
      • 7、线程池有哪些工作队列?
      • 8、几种常见线程池和使用场景?
      • 9、shutdown 和 shutdownNow 区别?
      • 10、execute 和 submit 区别?
      • 线程数量该如何选择?
  • 3 锁实现
    • 3.1 锁分类
      • 1、Java 中常见的锁?
    • 3.2 关键字锁
      • 1、synchronized 的用法?
      • 2、synchronized 的实现原理?
      • 3、JVM 对 synchronized 的优化?
      • 4、synchronized 锁是否可以降级?
      • 5、JMM 内存模型?
      • 6、什么是缓存一致性协议(MESI)?
      • 7、volatile 底层是如何实现的?
      • 8、volatile 与 synchronized 的区别?
      • 9、什么是内存屏障?
    • 3.2 Lock
      • 1、Lock 和 synchronized 区别?
    • 3.3 AQS 和 CAS
      • 1、什么是 CAS?
      • 2、什么是 Unsafe 类?
      • 3、CAS 有哪些缺点?
      • 4、什么是 ABA 问题?
      • 5、AQS 是什么?
      • 6、AQS 加锁的原理?
      • 7、AQS 中公平锁和非公平锁?
    • 3.4 Condition
  • 4 ThreadLocal
      • 1、ThreadLocal 是什么?
      • 2、ThreadLocal 为了解决什么问题?
      • 3、如何实现子父线程之间数据共享?
      • 4、ThreadLocal 内存泄漏的原因?
      • 5、为何使用弱引用?
  • 5 Concurrent
      • 1、聊一下 Semaphore?
      • 2、AtomicInteger 如何保证线程安全?
      • 3、Countdownlatch 和 Cyclicbarrier 区别?

1 并发基础

1、并发编程三要素是什么?

  • 原子性:指的是一个操作不能再继续拆分,要么一次操作完成,要么就是不执行;
  • 可见性:指的是一个变量在被一个线程更改后,其他的线程能立即看到最新的值;
  • 有序性:指的是程序的执行按照代码的先后顺序执行。

至于为什么会提出这三个要素,我理解这是因为 Java 编程中如果满足了这 3 个特性,程序的并发操作就是线程安全的。

对于关键字 synchronized,即保证了原子性,也保证了可见性和有序性。而关键字 volatile 无法保证原子性。

2、什么是 Happen-before 原则?

2 线程和线程池

2.1 线程

1、Java 线程有几种状态?

在 Java 中,线程主要分为六种状态:

  • 初始(NEW):新创建了一个线程对象,但还没有调用 start() 方法;
  • 运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”;

线程对象创建后,其它线程(如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU的 使用权,此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。

  • 阻塞(BLOCKED):表示线程阻塞于锁;
  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
  • 超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时间后自行返回;
  • 终止(TERMINATED):表示该线程已经执行完毕。

2、Java 线程状态是如何切换的?

先来看一下 Java 线程的状态图:

一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

3、Java 如何创建一个线程?

1)继承 Thread 类,重写 run 方法,然后通过start()方法去启动线程;

2)实现 Runnable 接口;

3)使用 ThreadPoolExecutor 线程池。

PS 如何创建一个进程?

在 Java 中可以通过两种方式来创建进程:

1)第一种方式是通过 Runtime.exec() 方法来创建一个进程;

2)第二种方法是通过 ProcessBuilder 的 start 方法来创建进程。

4、Callable 和 Runnable 区别?

2.2 线程池

3、为什么使用线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。

因为创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。

使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,执行多个任务。提高程序效率的同时还方便管理以及限流

4、线程池有哪些参数?

  • corePoolSize:线程池中的核心线程数量,即使在没有用的时候,也不会被回收(设置了 allowCoreThreadTimeOut 除外);
  • maximumPoolSize:线程池中可以容纳的最大线程的数量;
  • keepAliveTime:线程池中除了核心线程之外的其它线程的最长可以保留的时间;
  • util:空闲线程存活时间单位,即 keepAliveTime 的时间单位;
  • workQueue:工作队列,任务可以储存在任务队列中等待被执行,执行的是 FIFO 原则(先进先出);
  • threadFactory:创建线程的线程工厂类,可以用来设定线程名、是否为 daemon 线程等;
  • handler:拒绝策略,可以在任务满了之后采用一定策略拒绝执行某些任务。

5、讲讲线程增长的过程?

主要是理解 corePoolSize、maximumPoolSize 和 workQueue 之间的关系。

1)当线程池中线程数量小于 corePoolSize 核心线程数时,新提交的任务就会创建一个新线程执行,即使线程池中存在空闲线程;

2)当线程池中线程数量达到 corePoolSize 核心线程数后,新提交的任务就会被放到 workQueue 工作队列中,等待被线程池调度执行;

3)当 workQueue 也满了后,且满足当前线程数量是否小于 maximumPoolSize 最大线程数量时,新提交任务将继续创建新的线程执行;

3)若线程数量达到 maximumPoolSize 最大线程数量,则新提交任务交予 handler 拒绝策略处理。

6、线程池有哪些拒绝策略?

在线程池 ThreadPoolExecutor 中,已经包含四种处理策略。

  • AbortPolicy:直接丢弃任务,并抛出 RejectedExecutionException 异常,也是线程池默认采用的策略;
  • CallerRunsPolicy:在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任务;
  • DiscardOleddestPolicy:抛弃进入队列最早的那个任务,也是即将被执行的任务,然后尝试把这次拒绝的任务放入队列;
  • DiscardPolicy:直接丢弃任务,不予任何处理。

除了 JDK 默认提供的四种拒绝策略,我们可以通过实现 RejectedExecutionHandler 接口根据自己的业务需求去自定义拒绝策略。

7、线程池有哪些工作队列?

ThreadPoolExecutor 线程池的工作队列可以分为两大类:无界队列有界队列

  • ArrayBlockingQueue

是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

  • LinkedBlockingQueue

一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。newFixedThreadPool 使用了这个队列。

容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。

  • SynchronousQueue

一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。newCachedThreadPool 线程池使用了该队列。

  • PriorityBlockingQueue

一个具有优先级的无限阻塞队列。

  • DelayQueue

是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool 线程池使用了这个队列。

无界队列不存在饱和的问题,但是其问题是当请求持续高负载的话,任务会无脑的加入工作队列,那么很可能导致内存等资源溢出或者耗尽。而有界队列不会带来高负载导致的内存耗尽的问题,但是有引发工作队列已满情况下,新提交的任务如何管理的难题,这就是线程池工作队列饱和策略要解决的问题。

8、几种常见线程池和使用场景?

  • newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

  • newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

  • newWorkStealingPool

一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用 cpu 数量的线程来并行执行。

9、shutdown 和 shutdownNow 区别?

shutdown 和 shutdownNow 均可用于关闭线程池,但

  • shutdown

当我们调用 shutdown 方法后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

  • shutdownNow

当调用 shutdownNow 方法后,会向正在执行的全部任务发出 interrupt() 停止执行信号,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。

10、execute 和 submit 区别?

虽然二者在线程池中都可以提交任务,但却有着完全不同的实现:

  • 接收参数

execute() 只能接受 Runnable 类型的任务;而 submit() 可接受 Runnable 和 Callable 两种类型任务,只是 Runnable 返回值均为 void,所以使用 Future 的 get() 获得的还是 null。

  • 返回值

由 Callable 和 Runnable 的区别可知,execute() 没有返回值,submit() 可以有返回值。

  • 异常处理

execute() 是 Runnable 接口的实现,所以只能使用 try-catch 来捕获 CheckedException,并通过实现 UncaughtExceptionHande 接口处理 UncheckedException,即和普通线程的处理方式完全一致。

而 submit() 仅需通过捕获 Future.get 抛出的异常,即可让线程调用者感知到内部的 Checked 或者 Unchecked Exception。

线程数量该如何选择?

3 锁实现

3.1 锁分类

1、Java 中常见的锁?

Java 提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。我们可以根据不同的特性分为一下几类:

  • 乐观锁和悲观锁
  • 自旋锁和自适应自旋锁
  • 无锁、偏向锁、轻量级锁以及重量级锁
  • 公平锁和非公平锁
  • 可重入锁和非可重入锁
  • 独享锁和共享锁

一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

3.2 关键字锁

1、synchronized 的用法?

synchronized 是 Java 中的一个关键字,可以用来修饰方法或代码块,使用方法也很简单,就是在方法或代码块前加 synchronized 修饰符。只是二者的作用范围不同。

  • 修饰普通方法

但需要注意,虽然可以使用 synchronized 修饰方法,但 synchronized 并不属于方法定义的一部分,因此 synchronized 关键字不能被继承。即子类重写父类同步方法默认情况下是不同步的。

虽然子类方法不同步,但还可以在子类方法中调用父类中相应的方法以达到同步目的。

还有,也并非所有方法都可以被 synchronized 的修饰,比如接口方法和构造方法。

  • 修饰静态方法

静态方法是属于类的而非某一个对象,故 synchronized 修饰的静态方法将会锁定是这个类下的所有对象。

同时,synchronized 在修饰代码块时可以将锁加到特定的一个类上,这样加锁和静态方法加锁是一样,所有对象共用一把锁。

synchronized(ClassName.class) {
     // todo
}

总结一下,无论 synchronized 关键字加在方法还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象的锁;如果 synchronized 作用的对象是一个静态方法或一个类,则该类所有的对象共用同一把类锁。

2、synchronized 的实现原理?

我们知道 synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个线程可以进入到临界区,同时它还可以保证共享变量的内存可见性。那 Java 是如何实现这一功能的呢?

首先,Java 中的每一个对象都可以作为锁,这是 synchronized 实现同步的基础。当一个线程访问同步代码块时,先要获得对应的对象锁才能执行同步代码,当退出或者抛出异常时必须要释放锁。

一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

通过反编译得知,同步代码块和同步方法的实现略有不同,其中同步代码块是使用 monitorenter 和 monitorexit 指令实现的,而同步方法依靠的是方法修饰符上的 ACCSYNCHRONIZED 标志实现。

ref
ref

3、JVM 对 synchronized 的优化?

JDK 1.6 中对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

  • 锁膨胀

锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。他们会随着竞争的激烈而逐渐升级。

  • 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在 JIT 编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。

  • 锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。

  • 自适应自选

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

而自适应自旋锁相当于是对自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

ref

4、synchronized 锁是否可以降级?

《并发编程的艺术》中提到了锁只能升级而无法降级,但从其它资料了解到,锁是可以降级的,只不过条件比较苛刻。

这取决于各家 JVM 的实现,如我们最常用的 HotSpot JVM 其实就支持锁降级,但是锁升降级效率较低,如果频繁升降级的话对性能就会造成很大影响。重量级锁降级发生于 STW 阶段,降级对象为仅仅能被 VMThread 访问而没有其他 JavaThread 访问的对象。

5、JMM 内存模型?

**Java 内存模型(Java Memory Model,JMM)**是 Java 虚拟机规范定义的,用来屏蔽掉 Java 程序在各种不同的硬件和操作系统对内存的访问的差异。

常常和 Java 内存区域混淆,这是两个不同的概念。

Java 内存区域是指 JVM 运行时将数据分区域存储,简单的说就是不同的数据放在不同的地方。通常又叫****运行时数据区域**。

而 JMM 定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。规定了不同线程如何以及何时可以看到其他线程写入共享变量的值以及如何在必要时同步对共享变量的访问。

一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

JMM 定义了两个区域:

  • 主内存

JVM 规定所有的变量都必须在主内存(RAM)中产生,为了方便理解,可以认为是堆区。可以与物理机的主内存(RAM)相比,只不过物理机的主内存(RAM)是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。

  • 工作内存

虚拟机中每个线程都有自己的工作内存,这是由 JMM 抽象出来的,该内存是线程私有的。为了方便理解,可以认为是虚拟机栈,可以说是高速缓存。线程的工作内存保存了线程需要的变量在主内存中的副本。

理论上 Java 栈和堆数据都存储在物理主内存中,但随着 CPU 运算其数据的副本可能被高速缓存或者寄存器持有。

ref

6、什么是缓存一致性协议(MESI)?

我们都知道 CPU 在执行指令时需要从内存获取指令和所需的数据,但是 CPU 的速度要远大于内存速度,所以 CPU 直接从内存中存取数据要等待一定时间周期,造成资源的浪费并且影响性能。

这时候就需要引入 CPU 高速缓存(Cache Memory),现在常见的 CPU 的缓存结构为三级缓存,CPU 缓存通常被分成了三个级别:L1,L2,L3。级别越小越接近 CPU,所以速度也更快,同时也代表着容量越小。

CPU 缓存是由一组称之为**缓存行(Cache Line)**的固定大小的数据块组成的,缓存行是缓存中可以分配的最小存储单位,通常是 64 字节。

引入了 CPU 高速缓存后,提升了效率,但是同时也会引发缓存与主存不一致的问题。

ref

7、volatile 底层是如何实现的?

此部分将在 JVM 章节讲解。

8、volatile 与 synchronized 的区别?

  • 线程是否阻塞

volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

  • 作用范围

volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别上。

  • 原子性

volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以同时保证变量的修改可见性和原子性。

  • 虚拟机优化

volatile 修饰的变量不会被编译器优化;synchronized标 修饰变量可以被编译器优化。

9、什么是内存屏障?

3.2 Lock

1、Lock 和 synchronized 区别?

  • 存在层面

synchronized 是 Java 的一个关键字,在 JVM 层面实现;而 Lock 是一个类。

  • 是否阻塞

synchronized 会造成未获得锁的线程阻塞,而 Lock 啥情况而定,不同实现类不同。

  • 锁的释放

通过 synchronized 获取到的锁在同步代码执行完后会自动释放,如果线程出现异常也会释放锁;而 Lock 锁必须手动释放,否则容易造成死锁。

  • 锁状态

synchronized 的锁无法判断锁的状态,而 Lock 支持。

  • 锁类型

synchronized 是可重入、不可中断和非公平的锁;而 Lock 是可重入、可判断以及公平和非公平均可的锁。

3.3 AQS 和 CAS

1、什么是 CAS?

CAS(Compare and Swap),即比较并交换。CAS 是一条 CPU 并发原语。

CAS 是一种无锁算法,共有 3 个操作数,内存值 V、旧的预期值 A 以及要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则不执行。

关于原语

原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致问题。

2、什么是 Unsafe 类?

3、CAS 有哪些缺点?

  • 循环时间长,开销大(因为当 CAS 失败后,会一直尝试);
  • 只能保证一个共享变量的原子操作。(对多个共享变量操作时,循环 CAS 无法保证操作的原子性,只能用加锁来保证);
  • 存在 ABA 问题。

4、什么是 ABA 问题?

当第一个线程执行 CAS(V,E,U) 操作,在获取到当前变量 V,准备修改为新值 U 前,另外两个线程已连续修改了两次变量 V 的值,使得该值又恢复为旧值,这样我们就无法正确判断这个变量是否已被修改过。

如何解决

AtomicStampedReference:是一个带有时间戳的对象引用,在每次修改后,不仅会设置新值还会记录更改的时间。
AtomicMarkableReference:维护的是一个boolean值的标识,这种方式并不能完全防止ABA问题的发生,只能减少ABA发生的概率。

5、AQS 是什么?

AQS 全称 AbstractQueuedSynchronizer,中文名抽象队列同步器。AQS 是 JAVA 并发包的基础类,如说我们常用的 ReentrantLock、ReentrantReadWriteLock 等底层都是基于 AQS 实现的。

6、AQS 加锁的原理?

先来看看 AQS 对象内部有哪些属性:

  • state:int 类型核心变量,代表了加锁的状态。初始状态下,state 为 0.

  • exclusiveOwnerThread:Thread 类型,用来记录当前加锁的是哪个线程,初始值为 null。

  • head:头结点,把它当做当前持有锁的线程可能更好理解。

  • tail:尾接单,每个新的节点进来,都插入到最后,也就形成了一个等待队列的链表。

接下来我们使用 ReentrantLock的lock 为例,当线程调用 ReentrantLock 的 lock() 方法尝试进行加锁,实际就是用 CAS 操作将 state 值从 0 变为 1,同时加锁成功后,将加锁线程设置成当前线程。

如何实现锁互斥

1)当另一个线程 B 加锁时,也会尝试使用 CAS 操作将 state 从 0 变为 1,但此时因为线程 A 已经获取到锁且 state 已经是 1,则线程 B 加锁失败。

2)接着线程 B 会检查加锁线程变量记录的加锁线程是否是自己。

3)如果不是,则线程 B 会将自己放入 AQS 中的一个等待队列,等待线程 A 释放锁后重新尝试加锁。

一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

如何实现可重入

其实每次线程加锁时会判断一下当前加锁线程是否就是自己,那么是自己就可以可重入多次加锁,每次加锁就是把 state 的值给累加 1,释放锁时就递减 1。当 state 减到 0 时就彻底释放锁,即将加锁线程重置为 null。

ref

7、AQS 中公平锁和非公平锁?

在 AQS 中非公平体现在于当线程 A 释放锁,唤醒队列中第一个线程 B,此时刚好有线程 C 尝试抢锁,则 C 获取锁。

从代码实现来看,公平锁和非公平锁主要有两处不同:

1)非公平锁在调用 lock() 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回;

2)非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire() 方法,在 tryAcquire() 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

3.4 Condition

4 ThreadLocal

1、ThreadLocal 是什么?

从名字我们就可以知道 ThreadLocal 叫做线程变量,意思是其包装的属性属于当前线程,该变量对其他线程而言是隔离的。

ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富:

  • 在进行对象跨层传递的时候,使用 ThreadLocal 可以避免多次传递,打破层次间的约束。

  • 线程间数据隔离

  • 进行事务操作,用于存储线程事务信息。

  • 数据库连接,Session会话管理。

2、ThreadLocal 为了解决什么问题?

ThreadLocal 是为了解决对象不能被多线程共享访问的问题,通过 threadLocal.set() 方法将对象实例保存在每个线程自己所拥有的 threadLocalMap 中,这样的话每个线程都使用自己的对象实例,彼此不会影响从而达到了隔离的作用,这样就解决了对象在被共享访问时带来的线程安全问题。

3、如何实现子父线程之间数据共享?

使用 InheritableThreadLocal 可以实现父线程生成的变量需要传递到子线程中进行使用。

4、ThreadLocal 内存泄漏的原因?

先来看看 ThreadLocal、Thread 以及 ThreadLocalMap 三者的关系。

一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

首先是因为 ThreadLocal 是基于 ThreadLocalMap 实现的,其中 ThreadLocalMap 的 Entry 继承了 WeakReference ,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说, Entry 中的 key 是一个弱引用类型,对于弱引用来说,它只能存活到下次 GC 之前。

如果此时一个线程调用了 ThreadLocalMap 的 set 设置变量,当前的 ThreadLocalMap 就会新增一条记录,但由于发生了一次垃圾回收,这样就会造成一个结果: key 值被回收掉了,但是 value 值还在内存中,而且如果线程一直存在的话,那么它的 value 值就会一直存在。

这样被垃圾回收掉的 key 就会一直存在一条引用链: Thread -> ThreadLocalMap -> Entry -> Value,就是因为这条引用链的存在,就会导致如果 Thread 还在运行,那么 Entry 不会被回收,进而 value 也不会被回收掉,但是 Entry 里面的 key 值已经被回收掉了。这只是一个线程,如果再来一个线程,又来一个线程,多了之后就会造成内存泄漏。

解决办法

方法很简单,那就是使用完 key 值之后,将 value 值通过 remove 方法 remove 掉,这样的话内存中就不会有 value 值了,也就防止了内存泄漏。

5、为何使用弱引用?

从前面的图我们可以清楚的知道,在 ThreadLocal 内部维护着 ThreadLocalMap ,而它的 Entry 则继承自 WeakReference 的 ThreadLocal ,其中 Entry 的 k 为 ThreadLocal , v 为 Object ,在调用 super(k) 时就将 ThreadLocal 实例包装成了一个 WeakReference。

源码中提到:To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys。

即如果 key 采用强引用的话,如果引用的 ThreadLocal 对象(对应 ThreadLocalRef)被回收了,但是 ThreadLocalMap 还持有对 ThreadLocal 的强引用,如果没有 remove 的话, 在 GC 时进行可达性分析, ThreadLocal 依然可达,这样就不会对 ThreadLocal 进行回收,但是我们期望的是引用的 ThreadLocal 对象被回收。

5 Concurrent

1、聊一下 Semaphore?

Semaphore 是线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。通过 Semaphore 可以控制同时访问资源的线程个数。

2、AtomicInteger 如何保证线程安全?

我们知道 volatile 可以保证变量的内存可见性以及禁止指令重排,但使用 volatile 修饰的变量依旧无法保证线程安全,比如多线程累加操作,这是因为我们无法保证对变量的操作时原子性的。

因此,AtomicInteger 通过 CAS 操作更高效实现了线程安全。

3、Countdownlatch 和 Cyclicbarrier 区别?

  • Countdownlatch 一个线程(或者多个), 等待另外 N 个线程完成某个事情之后才能执行。

CountDownLatch 是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减 1。当计数器值到达 0 时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

  • Cyclicbarrier N 个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

Cyclicbarrier 的 await() 方法每被调用一次,计数便会减少 1,并阻塞住当前线程。当计数减至 0 时,阻塞解除,所有在此 CyclicBarrier 上面阻塞的线程开始运行。在这之后,如果再次调用 await() 方法,计数就又会变成 N-1,新一轮重新开始,这便是 Cyclicbarrier 的含义所在。

CyclicBarrier.await() 方法带有返回值,用来表示当前线程是第几个到达这个 Barrier 的线程。

此外

CyclicBarrier 的循环特性和构造函数所接受的 Runnable 参数也是 CountDownLatch 所不具备的。

相关文章

暂无评论

暂无评论...