Java-并发高频面试题-2

接着之前的Java-并发高频面试题

7. synchronized的实现原理是怎么样的?

首先我们要知道synchronized它是解决线程安全问题的一种方式,而具体是怎么解决的呢?主要是通过加锁的方式来解决

在底层实现上来看 是通过 monitorenter、monitorexit两个指令来实现同步monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。

那么synchronized是如何保证原子性的呢?

synchronized保证原子性主要就是通过上面说的monitorenter、monitorexit两个指令,当执行到monitoreter的时候要先获得锁,而执行monitorexit的时候则要释放锁,

这样一来通过两个指令就可以保证被synchronized修饰的代码同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。所以说synchronized是可以保证原子性的

那么synchronized是如何保证可见性的呢?

  1. 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
  2. 线程加锁后,其它线程无法获取主内存中的共享变量。
  3. 线程解锁前,必须把共享变量的最新值刷新到主内存中。
    简单说来就是被synchronized修饰的代码,为了保证可见性,它在对变量解锁之前,会强制将变量的值同步回主内存中,这样在解锁之后,其他线程也就可以访问到被修改后的值;

那么synchronized是如何保证有序性的呢?

首先我们要知道有序性的定义就是按照代码的先后顺序来去执行,那么synchronized基于monitorenter 和monitorexit加锁后本身就是会单个线程来去运行,而按照as-if-sieal语义来看,编译器和执行器即便在进行指令重排 也必须要保证单线程的执行结果不能改变

所以总结一下:因为被synchronized修饰的代码,同一个时间只能被一个线程访问,也就是单线程执行的,所以是天然可以保证有序性的

8. 锁升级?synchronized锁优化介绍下?

如果要想清楚锁升级,需要先知道不同锁的状态,这个锁状态其实就是java对象头中的mark word 标记字段,而这块结构会随着锁的状态的变化而变化。

如下图所示:

再具体细看 Mark Word存储的是对象自身的 运行数据,如哈希码、GC分代年龄、锁状态标志、偏向时间戳(Epoch) 等信息。

而从上图也可以看出因为mark word状态的不同又可以区分出无锁,偏向锁,轻量级锁,重量级锁,我们依次介绍下吧

无锁:简单来说就是mark word中锁状态为01时,同时在低三位不是偏向锁的状态

偏向锁:就是mark word 中锁状态为01 同时低三位是偏向锁状态

轻量级锁:mark word中锁状态为00标志

重量级锁:mark word中锁状态为10标志

那么他们之间是怎么转化升级的呢?

锁升级方向:无锁-->偏向锁---> 轻量级锁---->重量级锁,这个方向基本上是不可逆的。

首先呢其实是无锁状态 也就是不加锁;

之后当一个线程第一次访问对象时,jvm它其实会在对象头中设置该线程的thread ID,然后将对象的状态位设置为偏向锁,设置成偏向锁以后,如果有其他线程来访问该对象,那么就会检查该对象的偏向锁状态标志,如果说和自己的线程ID相同那么就会直接获取到锁,而如果不同,那么偏向锁就会升级为轻量级锁状态,而轻量级锁下,jvm会将mark word复制一份到线程栈中,并在对象头中存储线程栈的指针。

然后如果有其他线程来访问该对象时,会发现该对象已经处于轻量级锁状态,那么就会尝试用cas操作将对象头中的指针替换成自己的指针。如果能够替换成功,也就会获取到锁;如果替换失败,表示已经有其他线程获取了锁,那么这个轻量级锁就会升级为重量级锁。

如果锁一旦升级为重量级锁,其实也就是最笨重原始的加锁,会基于objectMonitor中的等待队列排队加锁,并会在对象头中记录指向等待队列的指针,也就是说在这种情况下 如果一个线程想要加锁,就需要先进入等待队列,等待锁被释放,当锁被释放以后,jvm会从等待队列中选择一个线程来唤醒,然后线程会进入就绪状态,等待重新获取对象的锁

9.synchronized和ReentrantLock的区别有哪些?

其实大致可以从锁的实现,使用,性能,功能特点来展开去描述
锁的实现 :synchronized是关键字,是基于JVM来去实现的,而ReentrantLock是类,是基于java api来去实现的
锁的使用 :synchronized作为关键字 是可以修饰方法和同步代码块,会自动释放锁,而ReentrantLock 作为类是通过lock、unlock方法来完成加锁和解锁,需要手动释放锁。
性能 :synchronized在早期版本中性能不如ReentrantLock,但是在1.6后增加了适应性自旋、锁消除等,两者性能就差不多了
功能特点 :Reentrantlock支持公平锁和非公平锁,但是synchronized是非公平锁,(即线程是否要排队加锁)

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

ReentrantLock提供了能够中断等待锁的线程机制,即通过lock.lockInterruptibly()来实现该机制

10.什么是AQS?

简单来说AQS 就是抽象队列同步器,是java并发编程底层的核心实现类,

AQS 的核心思想是利用一个双向队列来保存等待锁的线程,同时利用一个 state 变量来表示锁的状态。AQS 的同步器可以分为独占模式和共享模式两种。

独占模式是指同一时刻只允许一个线程获取锁,常见的实现类有 ReentrantLock;

共享模式是指同一时刻允许多个线程同时获取锁,常见的实现类有 Semaphore、CountDownLatch、CyclicBarrier 等。

我们首先看下独占模式的原理

简单说就是独占模式下AQS 维护了一个同步队列,和一个state变量,队列中会保存了所有等待获取锁的线程。如果有多个线程同时再执行加锁操作时,会通过cas操作才更新AQS内部的state的值,如果某一个线程加锁成功会将state的值更改为1,同时aqs中会指定加锁线程为当前线程,与此同时别的线程加锁失败会进入等待队列

一旦加锁线程释放锁就会将state的值设为0,同时将加锁线程更新为null,而加锁线程也会通知排在等待队列中的头元素

同时 独占模式又分为公平锁和非公平锁:默认非公平锁
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。

共享模式的原理

AQS 维护了一个等待队列和一个共享计数器 。共享计数器表示当前允许获取锁的线程数,当一个线程尝试获取锁时,如果当前允许获取锁的线程数已经达到了最大值,则将该线程加入到等待队列中,并挂起线程,等待其他线程释放锁或者共享计数器增加
当锁被释放时,会从等待队列中取出一个线程,使其获取锁,同时将它从队列中移除,唤醒该线程继续执行

11 保证线程安全的手段有哪些?

在 Java 中,多线程并发操作同一个共享变量时,就可能会发生线程安全问题。

在 Java 中保证线程安全的常用手段有以下三个:
锁机制 :在 Java 中,锁机制主要有两种:synchronized 关键字和 Lock 接口。synchronized 关键字是 Java 中最基本的锁机制,它可以用来修饰方法或代码块,以实现对共享资源的互斥访问。而 Lock 接口是 Java5 中新增的一种锁机制,它提供了比 synchronized 更强大、更灵活的锁定机制,例如可重入锁、读写锁等;

使用线程安全的容器:如 **ConcurrentHashMap、Hashtable、Vector。**需要注意的是,线程安全的容器底层通常也是使用锁机制实现的;

使用本地变量ThreadLocal :线程本地变量是一种特殊的变量,它只能被同一个线程访问。在 Java 中,线程本地变量可以通过 ThreadLocal 类来实现。每个 ThreadLocal 对象都可以存储一个线程本地变量,而且每个线程都有自己的一份线程本地变量副本,因此不同的线程之间互不干扰。

12 ThreadLocal是什么?

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

而ThreadLocal用起来并不麻烦,就是声明该变量,然后通过set 写入和get获取就可以进行操作

那你在工作中有用到ThreadLocal吗?

threadLocal的场景很多 比如说可以基于threadlocal做用户信息的上下文存储,以及cookie,session等的隔离,还有比如数据库连接池 可以交给ThreadLoca进行管理,保证当前线程的操作都是同一个Connnection

ThreadLocal怎么实现的呢?实现原理是啥?

其实TheadLocal本身并不存储数据,真正存储数据的是Threadlocalmap,

也就是说每个线程都会有一个属于自己的ThreadLocalMap,

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

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

ThreadLocal 内存泄露是怎么回事?

因为 ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。

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

所以说 弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。

那怎么解决内存泄漏问题呢?

其实 很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。

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

key设计成弱引用同样是为了防止内存泄漏。

假如key被设计成强引用,如果ThreadLocal 的引用即指针被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLocal,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。

13 线程死锁的条件有哪些?

死锁的产生需要满足以下 4 个条件:
互斥条件:也就是说在一段时间内某个锁资源只能被一个运算单元所占用。

请求和保持条件:说白了就是当前线程A占用着某个资源a,但是又去请求其他资源b,而资源b又恰好被其他线程B占用着不释放,就是处于这种请求保持条件

不可剥夺条件:指运算单元已获得的资源,在未使用完之前,不能被剥夺。

环路等待条件 :指在发生死锁时,必然存在运算单元和资源的环形链,即运算单元正在等待另一个运算单元占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况。

只有以上 4 个条件同时满足,才会造成死锁。

死锁的常用解决方案有以下两个
按照顺序加锁 :尝试让所有线程按照同一顺序获取锁,从而避免死锁。
设置获取锁的超时时间:尝试获取锁的线程在规定时间内没有获取到锁,就放弃获取锁,避免因为长时间等待锁而引起的死锁。

死锁排查工具有哪些?

常见的工具有以下几个:
jstack :可以查看 Java 应用程序的线程状态和调用堆栈,可用于发现死锁线程的状态。
jconsole 和 JVisualVM:这些是 Java 自带的监视工具,可以用于监视线程、内存、CPU 使用率等信息,从而帮助排查死锁问题。

14 CAS了解吗?有哪些实现?以及存在哪些问题?

CAS叫做CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性的。

CAS 指令包含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变量的新值 C。

只有当内存中地址 A 的值等于 B 时,才能将内存中地址 A 的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的

具体实现:在 Java 中,CAS 操作被封装在 Atomic 类中,例如 AtomicInteger 类就是利用了 CAS 操作来实现线程安全的自增操作。同时,Java 还提供了一些工具类来支持 CAS 操作,例如 Unsafe 类,它提供了一些原始的 CAS 操作方法,供 JVM 内部使用

CAS存在的问题
1. ABA问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况

那该如何解决ABA问题呢?

加版本号或者时间戳,说白了就是每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。

Java提供了AtomicStampReference类,它的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。

2. 只能保证一个变量的原子操作

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

怎么解决只能保证一个变量的原子操作问题?

可以考虑改用锁来保证操作的原子性

可以考虑合并多个变量,将多个变量封装成一个对象,java中可以用AtomicReference来保证原子性

3. 无限循环导致性能降低

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

那么如何来解决循环问题呢?可以通过设置自旋次数,即超过一定次数,就停止自旋

15. AtomicInteger 的原理?

说白了 就是基于CAS来实现的,在底层是用Unsafe类的实例来进行添加操作,而Unsafe类中的compareAndSwapInt其实就是一个native方法,基于CAS来操作int类型变量。

java 复制代码
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
相关推荐
小筱在线33 分钟前
SpringCloud微服务实现服务熔断的实践指南
java·spring cloud·微服务
luoluoal38 分钟前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
ChinaRainbowSea44 分钟前
十三,Spring Boot 中注入 Servlet,Filter,Listener
java·spring boot·spring·servlet·web
小游鱼KF1 小时前
Spring学习前置知识
java·学习·spring
扎克begod1 小时前
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
java·开发语言·python
青灯文案11 小时前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
我就是程序猿1 小时前
tomcat的配置
java·tomcat
阳光阿盖尔1 小时前
EasyExcel的基本使用——Java导入Excel数据
java·开发语言·excel
二十雨辰1 小时前
[苍穹外卖]-12Apache POI入门与实战
java·spring boot·mybatis
程序员皮皮林1 小时前
开源PDF工具 Apache PDFBox 认识及使用(知识点+案例)
java·pdf·开源·apache