八股文--JUC(2)
最开始系统时无锁的状态,当有第一个线程来访问时,jvm将对象头的MarkWord锁标志设置为偏向锁,然后将线程id记录到MarkWord里面,这个时候这个线程进入这个同步代码块就不需要其他的同步操作了,就非常的快了;
当第二个线程来抢锁,就会升级到轻量级锁,第二个线程拿不到锁,就会采用cas自旋的方式不断重新尝试获取锁,轻量级锁考虑的时竞争锁线程不多,而且线程持有锁的时间也不长的一个情景。
当第二个线程自旋到一定的次数之后还是没有拿到锁或者有更多线程来抢锁了就会升级为重量级锁,重量级锁加锁就需要调用操作系统的底层mutex,所以每次切换线程都需要操作系统切换内核态,开销很大。
这时候MarkWord的指针就会指向锁监视器monitor。
锁监视器主要是用来负责记录锁的拥有者,记录锁的重入次数,负责线程的阻塞唤醒,可以来说锁监视器就是一个对象有这几个字段:
owner(持有锁的线程)
waitSet(等待池)
Eetrylist(锁池--管理竞争锁失败而阻塞的线程)
recursions(记录锁的重入次数)
其他的不重要
⭐⭐11.synchronized和lock的区别
语法层面:
synchronized是关键字--jvm(c++实现)
Lock是接口--jdk(java实现)
使用synchronized时,程序执行完会自动释放;Lock必须得手动释放
功能层面:
都是悲观锁
Lock提供许多synchronized不具备的功能,比如公平锁。可打断,可超时,多条件变量
Lock的实现:RenntrantLocak,ReentrantReadWritelock(读写锁)
性能层面:
没有竞争时,synchronized性能好
有竞争时,Lock性能好
⭐12.CAS 你知道吗?
CAS全称:Compare And Swap(比较在交换),体现的一种乐观锁的思想,在没有锁的情况保证了线程操作共享数据的原子性。
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功
⭐13.AQS是什么
AQS全称:AbstractQueuedSynchronizer(抽象队列交换器)。他是构建锁或者其他同步组件的基础框架
AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
AQS最核心的就是三大部分:
- 状态:state(volatile修饰);
- 控制线程抢锁和配合的FIFO队列(双向链表);
- 期望协作工具类去实现的获取/释放等重要方法(重写)。
- 线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功
- 线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持有锁,它们都会到FIF
O队列中进行等待, - FIFO是一个双向队列,head属性表示头结点,tail表示尾结点
在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待
14.CAS 和 AQS 有什么关系?
CAS 和 AQS 两者的区别:
CAS是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期值(A)和新值(B)。CAS 操作的逻
辑是,如果内存位置V的值等于预期值A,则将其更新为新值B,否则不做任何操作。整个过程是原子
性的,通常由硬件指令支持,如在现代处理器上,,cmpxchg指令可以实现 CAS 操作。
AQS 是一个用于构建锁和同步器的框架,许多同步器如ReentrantLock、Semaphore丶
CountDownLatch等都是基于 AQS 构建的。AQS 使用一个volatile的整数变量state来表示同步
状态,通过内置的FIFo队列来管理等待线程。它提供了一些基本的操作,如acquire (获取资源)
和release(释放资源),这些操作会修改state的值,并根据state的值来判断线程是否可以获
取或释放资源。AQS 的acquire操作通常会先尝试获取资源,如果失败,线程将被添加到等待队列
中,并阻塞等待。release操作会释放资源,并唤醒等待队列中的线程。
CAS 和 AQS 两者的联系:
CAS 为 AQS 提供原子操作支持:AQS 内部使用 CAS 操作来更新state变量,以实现线程安全的状
态修改。在acquire操作中,当线程尝试获取资源时,会使用 CAS 操作尝试将state从一个值更新
为另一个值,如果更新失败,说明资源已被占用,线程会进入等待队列。在release操作中,当线程
释放资源时,也会使用CAS 操作将state恢复到相应的值,以保证状态更新的原子性。
15.voliatle关键字有什么作用?
保证变量对所有线程的可见性。
当一个变量被声明为volatile时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了volatile变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。
禁止指令重排序优化。
volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序。
1)写-写(Write-Write)屏障:在对volatile变量执行写操作之前,会插入一个写屏障。这确保了在该变量写操作之前的所有普通写操作都已完成,防止了这些写操作被移到volatile写操作之后。
2)读-写(Read-Write)屏障:在对volatile变量执行读操作之后,会插入一个读屏障。它确保了对volatile变量的读操作之后的所有普通读操作都不会被提前到volatile读之前执行,保证了读取到的数据是最新的。
3)写-读(Write-Read)屏障:这是最重要的一个屏障,它发生在volatile写之后和volatile读之前。这个屏障确保了volatile写操作之前的所有内存操作(包括写操作)都不会被重排序到volatile读之后,同时也确保了volatile读操作之后的所有内存操作(包括读操作)都不会被重排序到volatile写之前。
16.ReentrantLock的实现原理
概述
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
- 可中断(可以主动放弃)
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
实现原理
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
工作流程
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
17.synchronized和renentranlock的区别
- 用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
- 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁锁
- 类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
- 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的
18.⭐⭐ThreadLocal
ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。 但是这样还是会存在内存泄露的问题,假如key,ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。
ThreadLocal 是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,这样就可以避免资源竞争带来的多线程的问题,这种解决多线程的安全问题和lock(这里的lock 指通过synchronized 或者Lock 等实现的锁) 是有本质的区别的:
- lock 的资源是多个线程共享的,所以访问的时候需要加锁。
- ThreadLocal 是每个线程都有一个副本,是不需要加锁的。
- lock 是通过时间换空间的做法。
- ThreadLocal 是典型的通过空间换时间的做法
19.⭐⭐ThealLocal的内存泄露问题
- 理解问题背景:首先简要说明ThreadLocal是什么以及它为什么会引发内存泄露。ThreadLocal是Java提供的一种线程局部变量工具,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。但是如果不正确地使用ThreadLocal(例如设置了一个ThreadLocal变量但没有显式清除),可能会导致内存泄露,因为即使线程结束了,只要ThreadLocal对象还在强引用链上,那么与之关联的线程局部变量就不会被垃圾回收。
- 描述内存泄露的原因:
-
- 当一个ThreadLocal不再需要了,但它的Entry(存储于ThreadLocalMap中的键值对)没有被移除。
- 如果该ThreadLocal对象本身能够被垃圾收集器回收掉,那么这个Entry就变成了key为null的Entry。这种情况下,除非手动清理这些Entry,否则它们将永远无法被释放。
- 在某些情况下,如线程池里的线程重复使用时,如果前一个任务遗留下来的ThreadLocal数据没有被清理干净,那么这可能导致内存泄露随着时间推移而积累起来。
- 提出解决方案:
-
- 显式调用
remove()
方法:在每次使用完ThreadLocal之后立即调用其remove()
方法,以确保及时从当前线程的ThreadLocalMap中移除不再使用的条目。 - 使用弱引用包装ThreadLocal:通过自定义WeakReference类来包裹ThreadLocal实例,这样当外部没有强引用指向ThreadLocal对象时,就可以让垃圾收集器自动回收相应的资源。
- 定期清理策略:对于长期运行的应用程序,可以考虑定期检查并清理不再活跃的ThreadLocal实例及其相关联的数据。
- 设计模式改进:采用更合适的设计模式或架构方式减少对ThreadLocal的依赖,从根本上避免潜在的问题。
- 显式调用