JUC并发编程与源码分析学习笔记
2023-07-13 11:07:44 3 举报
AI智能生成
JUC并发编程与源码分析学习笔记,笔记内容来之尚硅谷周阳老师视频
作者其他创作
大纲/内容
CompletableFuture演化历史
Futrue接口理论知识
Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。(异步:可以被叫停,可以被取消)
一句话:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。
Futrue接口常用实现类
Futrue接口能干什么
Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。
如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。
主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。
主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
Futrue接口相关架构
Futrue优缺点
Future优点
future+线程池异步多线程任务配合,能显著提高程序的执行效率。
Future缺点
1.get方法阻塞 一旦调用get()方法,不管是否计算完成,都会导致阻塞(所以一般get方法放到最后)
2.isDone()轮询 利用if(futureTask.isDone())的方式使得他在结束之后才get(),但是也会消耗cpu
2.isDone()轮询 利用if(futureTask.isDone())的方式使得他在结束之后才get(),但是也会消耗cpu
Futrue现状
对于简单的业务场景使用Future完全OK
回调通知
创建异步任务
多个任务前后依赖可以组合处理(水煮鱼)
对计算速度选最快完成的(并返回结果)
CompletableFuture对Future的改进
CompletableFuture为什么会出现
通过上面的Futrue我们知道 futrue阻塞的方式和异步编程的设计理念相违背,而轮询的方式会消耗无畏的CPU资源。因此,JDK8设计出CompletableFuture
CompletableFuture和CompletionStage
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
CompletionStage
Completion Stage代表异步计算过程中的某一个阶段, 一个阶段完成以后可能会触发另外一个阶段
一个阶段的计算执行可以是一个Function, Consumer或者Runnable。
比如:stage.then Apply(x->square(x) ) .then Accept(x->System.out.print(x) ) .then Run() ->System.out.print In() )
,一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发。
比如:stage.then Apply(x->square(x) ) .then Accept(x->System.out.print(x) ) .then Run() ->System.out.print In() )
,一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发。
核心四个静态方法
利用核心的四个静态方法创建一个异步操作 | 不建议用new
关键就是 |有没有返回值|是否用了线程池|
runAsync无返回值
runAsync+线程池
supplyAsync有返回值
supplyAsync+线程池
CompletableFuture优点
异步任务结束时,会自动回调某个对象的方法;
主线程设置好毁掉后,不再关心异步任务的执行,异步任务之间可以顺序执行
异步任务出错时,会自动回调某个对象的方法
CompletableFuture常用方法
1.获得结果和触发计算
获取结果
public T get() 不见不散,容易阻塞
public T get(long timeout,TimeUnit unit) 过时不候,超过时间会爆异常
public T join() 类似于get(),区别在于是否需要抛出异常
public T getNow(T valueIfAbsent) 没有计算完成的情况下,给一个替代结果
主动触发计算
public boolean complete(T value) 是否立即打断get()方法返回括号值
2.对计算结果进行处理
thenApply 计算结果存在在依赖关系,使得线程串行化。因为依赖关系,所以一旦有异常,直接叫停
handle 类似于thenApply,但是有异常的话仍然可以往下走一步
3.对计算结果进行消费
thenAccept 接收任务的处理结果,并消费处理,无返回结果|消费型函数式接口
任务之间的顺序执行
thenRun 任务A执行完执行B,并且B不需要A的结果
thenAccept 任务A执行完执行B,B需要A的结果,但是任务B无返回值
thenApply 任务A执行完执行B,B需要A的结果,同时任务B有返回值
CompleteFuture和线程池说明(非常重要)
1.没有传入自定义线程池,都用默认线程池ForkJoinPool
2.传入了一个自定义线程池如果你执行第一个任务的时候,传入了一个自定义线程池
2.传入了一个自定义线程池如果你执行第一个任务的时候,传入了一个自定义线程池
4.对计算速度进行选用
applyToEither方法,快的那个掌权
5.对计算结果进行合并
thenCombine 合并
说说JAVA的锁事
简要看看大厂面试题
一、Synchronized相关问题
1.Synchronized用过吗, 其原理是什么?
2.你刚才提到获取对象的锁,这个锁到底是什么?如何确定对象的锁?
3.什么是可重入性,为什么说Synchronized是可重入锁?
4.JVM对Java的原生锁做了哪些优化?
5.为什么说Synchronized是非公平
锁?
6.什么是锁消除和锁粗化?
7.为什么说Synchronized是个悲观锁?乐观锁的实现原理又是什么?什么是CAS, 它有
8.乐观锁一定就是好的吗?
二、可重入锁Reentrant Lock及其他显式锁相关问题
1.跟Synchronized相比,可重入锁Reentrant Lock其实现原理有什么不同?
2.那么请谈谈AQS框架是怎么回事儿?
3.请尽可能详尽地对比下Synchronized和Reentrant Lock的异同。
4.Reentrant Lock是如何实现可重入性的?
1.Synchronized用过吗, 其原理是什么?
2.你刚才提到获取对象的锁,这个锁到底是什么?如何确定对象的锁?
3.什么是可重入性,为什么说Synchronized是可重入锁?
4.JVM对Java的原生锁做了哪些优化?
5.为什么说Synchronized是非公平
锁?
6.什么是锁消除和锁粗化?
7.为什么说Synchronized是个悲观锁?乐观锁的实现原理又是什么?什么是CAS, 它有
8.乐观锁一定就是好的吗?
二、可重入锁Reentrant Lock及其他显式锁相关问题
1.跟Synchronized相比,可重入锁Reentrant Lock其实现原理有什么不同?
2.那么请谈谈AQS框架是怎么回事儿?
3.请尽可能详尽地对比下Synchronized和Reentrant Lock的异同。
4.Reentrant Lock是如何实现可重入性的?
乐观锁和悲观锁
乐观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
悲观锁的实现方式
1.synchronized关键字
2.Lock的实现类都是悲观锁
1.synchronized关键字
2.Lock的实现类都是悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源
悲观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
乐观锁的实现方式
1.版本号机制Version (ABA问题)
2.最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
1.版本号机制Version (ABA问题)
2.最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
适合读操作多的场景,不加锁的性能特点能够使其操作的性能大幅提升
synchronized三个体现
作用域实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁
作用于代码块,对括号里配置的对象加锁
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
synchronized实现原理
synchronized同步代码块
1.synchronized实现使用的是moniterenter和moniterexit指令(moniterexit可能有两个)
2.那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)
2.那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)
synchronized普通同步方法
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitore然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
synchronized静态同步方法
ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
synchronized锁的是什么
“锁”本身是个对象,synchronized这个关键字不是“锁”。硬要说的话,加synchronized仅仅是相当于“加锁”这个操作。
公平锁和非公平锁
公平锁
ReentrantLock lock = new ReentrantLock(true);
是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
非公平锁
默认是非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)
面试题
为什么会有公平锁/非公平锁的设计?为什么默认是非公平?
1.恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
2.使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
2.使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平?什么时候用非公平?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
否则那就用公平锁,大家公平使用。
可重入锁(递归锁)
概念
在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
种类
隐式锁Synchronized
在同步块中
在同步方法中
显式锁Lock
显式锁(即Lock)也有ReentrantLock这样的可重入锁
注意:lock unlock要成对
死锁以及排查
概念
是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生的原因
系统资源不足
进程运行推进的顺序不合适
资源分配不当
手写一个死锁案例
demo
如何排查死锁
纯命令
jps -l 查看当前进程运行状况
jstack 进程编号 查看该进程信息
图形化
jconsole
写锁 / 读锁
看 ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
自旋锁
看 CAS
LockSupport与线程中断
线程中断机制
从阿里面试题说起
三个方法了解过吗?用在哪?
如何停止一个运行中的线程?
如何中断一个运行中的线程?
如何停止一个运行中的线程?
如何中断一个运行中的线程?
什么是中断机制
概念
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制——中断。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
因此,Java提供了一种用于停止线程的协商机制——中断。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
中断的相关API方法说明
三大方法
public void interrupt() 实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程;
public static boolean interrupted() 静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样)
public boolean isInterrupted() 实例方法,判断当前线程是否被中断(通过检查中断标志位)
public static boolean interrupted() 静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样)
public boolean isInterrupted() 实例方法,判断当前线程是否被中断(通过检查中断标志位)
相关面试题
如何停止中断运行中的线程?
通过一个volatile变量实现
通过AtomicBoolean(原子布尔型)
通过Thread类自带的中断api方法实现
当前线程的中断标识为true,是不是线程就立刻停止?
否
中断只是一种协同机制,修改中断标识位仅此而已,而不是立刻stop打断
中断只是一种协同机制,修改中断标识位仅此而已,而不是立刻stop打断
静态方法Thread.interrupted(),谈谈你的理解
判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)
他们在底层都调用了native方法isInterrupted。
只不过传入参数ClearInterrupted一个传参传了true,一个传了false。
静态方法interrupted() 中true表示清空当前中断状态。
isInterrupted(false) 则不清空
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)
他们在底层都调用了native方法isInterrupted。
只不过传入参数ClearInterrupted一个传参传了true,一个传了false。
静态方法interrupted() 中true表示清空当前中断状态。
isInterrupted(false) 则不清空
什么是LockSupport
用于创建锁和其他同步类的基本线程阻塞原语
核心就是park()和unpark()方法
线程等待唤醒机制
3种让线程等待和唤醒的方法
使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
1.wait和notify方法必须要在同步块或者方法里面,且成对出现使用
2.先wait后notify才OK,顺序
2.先wait后notify才OK,顺序
使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
await和signal类似于上面wait和notify
1.Condition中的线程等待和唤醒方法,需要先获取锁
2.一定要先await后signal,不能反了
1.Condition中的线程等待和唤醒方法,需要先获取锁
2.一定要先await后signal,不能反了
LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object和Condition使用的限制条件
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
概念
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
LockSupport类使用了一种名为Permit(许可) 的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit)
主要方法
park()/park(Object blocker)
unpark(Thread thread)
重点说明
Lock Support是用来创建锁和其他同步类的基本线程阻塞原语。
Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。
归根结底, Lock Support调用的Unsafe中的native代码。
Lock Support提供park() 和unpark() 方法实现阻塞线程和解除线程阻塞的过程
Lock Support和每个使用它的线程都有一个许可(permit) 关联。
每个线程都有一个相关的permit, permit最多只有一个, 重复调用unpark也不会积累凭证。
Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。
归根结底, Lock Support调用的Unsafe中的native代码。
Lock Support提供park() 和unpark() 方法实现阻塞线程和解除线程阻塞的过程
Lock Support和每个使用它的线程都有一个许可(permit) 关联。
每个线程都有一个相关的permit, permit最多只有一个, 重复调用unpark也不会积累凭证。
面试题
为什么可以突破wait/notify的原有调用顺序?
因为unpark获得了一个凭证, 之后再调用park方法, 就可以名正言顺的凭证消费, 故不会阻塞。
先发放了凭证后续可以畅通无阻。
先发放了凭证后续可以畅通无阻。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1, 连续调用两次un park和调用一次un park效果一样, 只会增加一个凭证;
而调用两次park却需要消费两个凭证, 证不够, 不能放行。
而调用两次park却需要消费两个凭证, 证不够, 不能放行。
JAVA内存模型之JMM
简要看看大厂面试题
你知道什么是Java内存模型JMM吗?
JMM与volatile它们两个之间的关系?(下一章详细讲解)
JMM有哪些特性or它的三大特性是什么?
为什么要有JMM,它为什么出现?作用和功能是什么?
happens-before先行发生原则你有了解过吗?
JAVA内存模型Java Memory Model
概念
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的
原则
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛
1 通过JMM来实现线程和主内存之间的抽象关系。
2 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果
2 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果
JMM三大特性
可见性
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中
原子性
指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰
有序性
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
优缺点:
JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的机器对指令进行重排序,使得机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
但是,指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生“脏读”),简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化
JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的机器对指令进行重排序,使得机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
但是,指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生“脏读”),简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化
指令重排的三种表现
1.编译器优化的重排
2.指令并行的重排
3.内存系统的重排
1.编译器优化的重排
2.指令并行的重排
3.内存系统的重排
小结
1.单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性
2.多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
2.多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
JMM规范下,多线程对变量的读写过程
读取过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程
JMM定义了线程和主内存之间的抽象关系
1.线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
2.每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
2.每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
小总结
1.我们定义的所有共享变量都存储在物理主内存中
2.每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
3.线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
4.不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
2.每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
3.线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
4.不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
JMM规范下,多线程先行发生原则之happens-before
先行发生原则
在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重新排序,那么这两个操作之间必须存在happens-before(先行发生)原则。
逻辑上的先后关系
逻辑上的先后关系
happens-before总原则
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
两个操作之间存在happens-before关系,并不一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
happens-before8条
1.次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作
2.锁定规则
锁的获取的先后顺序
3.volatile变量规则
对一个volatile变量的读写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的后面同样是指时间上的先后
4.传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
5.线程启动规则(Thread Start Rule)
Thread对象的start()方法先行发生于此线程的每一个动作
6.线程中断规则(Thread Interruption Rule)
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断。
7.线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行
8.对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
volatile与JMM
被volatile修改的变量有2大特点
特点
可见性
有序性
volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量立即刷新回到主内存中。
当读一个volatile变量时,JMM会把该线程对应的工作内存设置为无效,直接从主内存中读取共享变量。
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
内存屏障
是什么
内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令 ,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性 。
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
内存屏障分类
粗分
写屏障
在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
读屏障
在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
细分
loadload
storestore
loadstore
storeload
valatile的特性
保证可见性
概念:保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见
volatile变量的读写过程:read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)
指令禁排序
如果存在数据依赖关系 ,禁止重排序;但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑
重排序的分类和执行流程
如何正确使用valatile
单一赋值可以,但是含有符合运算赋值不可以(比如i++)
状态标志,判断业务是否结束
开销较低的读,写锁策略
DCL双锁案例
小小的总结
volatile写之前的的操作,都禁止重排到volatile之后
volatile读之后的操作,都禁止重排到volatile之前
volatile写之后volatile读,禁止重排序
CAS
原子类
java.util.concurrent.atomic包下的所有相关类和API
是什么
compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。
执行CAS操作的时候,将内存位置的值与预期原值比较:
如果相匹配,那么处理器会自动将该位置值更新为新值,
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功
执行CAS操作的时候,将内存位置的值与预期原值比较:
如果相匹配,那么处理器会自动将该位置值更新为新值,
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功
CAS底层原理?谈谈对Unsafe类的理解
原理
CAS (CompareAndSwap)
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
当它重来重试的这种行为叫 自旋
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
当它重来重试的这种行为叫 自旋
源码分析
demo
上面三个方法都是类似的,主要对4个参数做一下说明。
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值
Unsafe
CAS这个理念 ,落地就是Unsafe类
它是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门 ,基于该类可以直接操作特定内存\ 的数据 。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是 \ 修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
注意Unsafe类中的所有方法都是 \ 修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
小结
CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。
核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来
实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。
核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来
原子引用
AtomicReference<V>
demo
CAS与自旋锁
是什么
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,
当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。
这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
若在OpenJDK源码中查看Unsafe.java这里while体现了自旋的思想假如是ture,取反false退出循环;假如是false,取反true要继续循环
当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。
这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
若在OpenJDK源码中查看Unsafe.java这里while体现了自旋的思想假如是ture,取反false退出循环;假如是false,取反true要继续循环
自己实现一个自旋锁SpinLockDemo
demo
CAS优缺点
缺点
循环时间长开销很大
引出来ABA问题
ABA问题解决方案
利用 AtomicStampedReference(比较+版本号)
原子操作类の18罗汉增强
是什么
atomic
分类
基本类型原子类
有几个
AtomicInteger
AtomicBoolean
AtomicLong
AtomicBoolean
AtomicLong
常用API
public final int get()
public final int getAndSet(int new Value)
public final int getAndIncrement()
public final int getAndDecrement()
public final int getAndAdd(int delta)
public comapreAndSet(int expect,int update)
public final int getAndSet(int new Value)
public final int getAndIncrement()
public final int getAndDecrement()
public final int getAndAdd(int delta)
public comapreAndSet(int expect,int update)
数组类型原子类
有几个
AtomicIntegerArray
AtomicLongArray
AtomicRreferenceArray
AtomicLongArray
AtomicRreferenceArray
引用类型原子类
有几个
AtomicReference
AtomicStampedReference
AtomicMarkableReference
AtomicStampedReference
AtomicMarkableReference
AtomicStampedReference 带版本号以防CAS中的ABA问题
AtomicMarkableReference 原子更新带有标记位的引用类型对象
解决是否修改过,它的定义就是将 状态戳 简化为 true|false,类似一次性筷子
解决是否修改过,它的定义就是将 状态戳 简化为 true|false,类似一次性筷子
对象的属性修改原子类
有几个
AtomicIntegerFieldUpdater //原子更新对象中int类型字段的值
AtomicLongFieldUpdater //原子更新对象中Long类型字段的值
AtomicReferenceFieldUpdater //原子更新引用类型字段的值
AtomicLongFieldUpdater //原子更新对象中Long类型字段的值
AtomicReferenceFieldUpdater //原子更新引用类型字段的值
AtomicReferenceFieIdUpdater demo
原子操作增强类原理解析
有几个
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
DoubleAdder
LongAccumulator
LongAdder
阿里开发手册规范
使用场景
1.热点商品点赞计算器,点赞数加加统计,不要求实时精确
2.一个很大的List,里面都是int类型,如何实现加加,说说思路
2.一个很大的List,里面都是int类型,如何实现加加,说说思路
LongAdder
LongAdder只能用来计算加法 。且从零开始计算
LongAccumulator提供了自定义的函数操作
LongAccumulator提供了自定义的函数操作
模拟下点赞计数器,看看性能
demo
源码分析
LongAdder架构
LongAdder是Striped64的子类
原理
ongAdder的基本思路就是分散热点 ,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。
源码解读
LongAdder.increment()源码解析
add(1L)
一开始竞争小的时候CAS能成功,也就是casBase能成功,然后cells也是空的,所以不会进到循环
竞争大的时候,他会Cell[] rs = new Cell[2]; 新建两个cell, 此时≠ null ,条件满足了,进入循环。
然后这里还有一层循环,这里是多个if并排
然后这里还有一层循环,这里是多个if并排
longAccumulate()
上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:
CASE1:Cell[]数组已经初始化
CASE2:Cell[]数组未初始化(首次新建)
CASE3:Cell[]数组正在初始化中
CASE1:Cell[]数组已经初始化
CASE2:Cell[]数组未初始化(首次新建)
CASE3:Cell[]数组正在初始化中
sum()
demo
sum()会将所有Cell数组中的value和base累加作为返回值。
核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。
核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。
小总结
AtomicLong
原理:CAS+自旋
使用场景:低并发下的全局计算
AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
缺陷:高并发后性能急剧下降
LongAdder
原理:CAS+Base+Cell数组分散
空间换时间并分散了热点数据
空间换时间并分散了热点数据
场景:高并发的全局计算
缺陷:sum求和后还有计算线程修改结果的话,最后结果不够准确
ThreadLocal
ThreadLocal简介
面试题
1.ThreadLocal中ThreadLocalMap的数据结构和关系?
2.ThreadLocal的key是弱引用,这是为什么?
3.ThreadLocal内存泄漏问题你知道吗?
4.ThreadLocal中最后为什么要加remove方法?
2.ThreadLocal的key是弱引用,这是为什么?
3.ThreadLocal内存泄漏问题你知道吗?
4.ThreadLocal中最后为什么要加remove方法?
作用
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),
主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
概念
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。 ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
API
常用的五个方法:两个初始化 一个get 一个set 一个remove
小结
每个Thread内有自己的实例副本并且该副本只由当前线程自己使用
其他Thread不可访问,那就不存在多线程间共享的问题
统一设置初始值,但是每个线程对这个值的修改都是各自线程相互独立的
其他Thread不可访问,那就不存在多线程间共享的问题
统一设置初始值,但是每个线程对这个值的修改都是各自线程相互独立的
一句话:
假如synchronized或者Lock控制资源的访问顺序
利用ThreadLocal人手一份,大家各自安好,没必要抢夺
假如synchronized或者Lock控制资源的访问顺序
利用ThreadLocal人手一份,大家各自安好,没必要抢夺
ThreadLocal源码分析
Thread,ThreadLocal,ThreadLocalMap关系
根据官方API,Thread是程序中执行的线程;ThreadLocal类提供线程局部变量
打开Thread.java类,发现每个Thread类里面有一个ThreadLocal类
而ThreadLocalMap是ThreadLocal的一个静态内部类
打开Thread.java类,发现每个Thread类里面有一个ThreadLocal类
而ThreadLocalMap是ThreadLocal的一个静态内部类
ThreadLocal内存泄漏问题
什么是内存泄漏
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露
四大引用
强引用 Reference
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了
软引用 SoftReference
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,当系统内存充足时它 不会 被回收,当系统内存不足时它 会 被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收
弱引用 WeakReference
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存
虚引用 PhantomReference
虚引用必须和引用队列 (ReferenceQueue)联合使用虚引用需要java.lang.ret.PhantomReterence类来实现,顾名思义, 就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用
PhantomReference的get方法总是返回null
处理监控通知使用
ThreadLocal为什么源代码用弱引用
当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用,就大概率会减少内存泄漏的问题(还有一个key为null的雷,后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用,就大概率会减少内存泄漏的问题(还有一个key为null的雷,后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
使用弱引用就万事大吉了吗?
我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(这个tl就不会被干掉),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏
所以在不使用某个ThreadLocal对象后,手动调用remove方法来删除它。
所以在不使用某个ThreadLocal对象后,手动调用remove方法来删除它。
最佳实践
一定要进行初始化避免空指针问题ThreadLocal.withInitial(()- > 初始化值)
建议把ThreadLocal修饰为static
用完记得手动remove
ThreadLocal remove()源码分析
set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry
Java对象内存布局和对象头
大厂题目
Object object = new Object() 谈谈你对这句话的理解?一般而言JDK8按照默认情况下,new一个对象占多少内存空间
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据( Instance Data)和对齐填充(Padding) 。
对象在堆内存中的布局
存储布局
对象头
对象标记(markOpp)
默认存储对象的HashCode、分代年龄和锁标志位等信息。
类元信息 (klassOop)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
结构图
对象头有多大
在64位系统中,MarkWord占了8个字节,类型指针占了8个字节,一共是16个字节
实例数据
存放类的属性(Field)数据信息,包括父类的属性信息
对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。
有个案例,对象头16+实例数据5+对齐填充3=24字节
官网理论
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
细说对象头markword
64位的markword对象头
对象布局、GC回收和后面的锁升级就是对象标记MarkWord里面标志位的变化
Synchronized与锁升级
大厂题目
谈谈你对Synchronized的理解
请你聊聊Synchronized的锁升级
入门
synchronized锁:由对象头重的Mark Word根据锁标志位的不同而被复用及锁升级策略
Synchronized的性能变化
发展历程
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(系统互斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
为什么每一个对象都可以成为一个锁
我们说在java中每个对象都可以成为一把锁,因为在JVM中每个对象都一个monitor(监视器锁)。对应到C底层叫做Object Monitor,并用c定义了很多信息。再往下到操作系统中是基于Mutex Lock互斥锁实现,涉及到了用户态和内核态的切换,所以非常耗费资源
synchronized和对象头图
Synchronized锁种类及升级步骤
多线程访问情况
只有一个线程来访问,有且只有一个的情况
有多个线程(2个线程A、B来交替访问)
竞争激烈,更多个线程来访问
升级流程
synchronized用的锁是存在Java对象头里的Mark Word中,锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
64位标记图再看看
锁指向
偏向锁:MarkWord存储的是偏向的线程ID;
轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针;
重量锁:MarkWord存储的是指向堆中的monitor对象的指针;
轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针;
重量锁:MarkWord存储的是指向堆中的monitor对象的指针;
无锁
简单的理解就是new了一个对象,程序没有锁的竞争
偏向锁
概念
单线程竞争
当线程A第一次竞争到锁时,通过修改Mark Word中的偏向ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
当线程A第一次竞争到锁时,通过修改Mark Word中的偏向ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
作用
当一段同步代码一直被同一个线程多次访问,由于只有一个线程访问那么该线程在后续访问时便会自动获得锁。
偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能。
偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能。
轻量级锁
概念
多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞
作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短
本质就是自选锁CAS
本质就是自选锁CAS
重量级锁
概念
有大量的线程参与锁的竞争,冲突性很高
原理
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor
JIT编译器对锁的优化
概念
Just In Time Compiler,一般翻译为即时编译器
锁消除
从JIT角度看相当于无视它,synchronized(o)不存在了,
这个锁对象并没有被共用扩散到其它线程使用,
极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
这个锁对象并没有被共用扩散到其它线程使用,
极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
锁粗化
假如方法中首位相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
加粗加大范围,一次申请使用即可,避免次次都申请和释放锁,提升了性能
加粗加大范围,一次申请使用即可,避免次次都申请和释放锁,提升了性能
小结
锁升级后hashcode去哪里了?
在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法己经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。
对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法己经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。
各种锁优缺点、synchronized锁升级和实现原理
AbstractQueuedSynchronizer之AQS
AQS入门级别理论知识
是什么
字面意思
抽象的队列同步器
技术解释
是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
AQS为什么是JUC内容中最重要的基石
和AQS有关的
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
...
进一步理解锁和同步器的关系
锁,面向锁的使用者 -- 定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可
同步器,面向锁的实现者 -- 同步器,面向锁的实现者
比如Java并发大神DougLee,提出统一规范并简化了锁的实现,
屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等
比如Java并发大神DougLee,提出统一规范并简化了锁的实现,
屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等
能干吗
加锁会导致阻塞
有阻塞就需要排队,实现排队必然需要队列
解释说明
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种 排队等候机制 。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去 候客区排队等候 ),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了 排队等候机制 ,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用, 就需要一定的阻塞等待唤醒机制来保证锁分配 。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点( Node ),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果
既然说到了 排队等候机制 ,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用, 就需要一定的阻塞等待唤醒机制来保证锁分配 。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点( Node ),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果
源码说明
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
小总结
AQS基本结构
CLH:Craig、Landin and Hagersten 队列,是个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)
AQS源码分析
体系结构
AQS自身
AQS的int变量
AQS的CLH队列
小小的总结
有阻塞就需要排队,实现排队必然需要队列
state变量+CLH双端队列
内部类Node
内部结构
属性说明
AQS源码深度解析
AQS相关图
从我们的ReentrantLock开始解读AQS
ReentrantLock的原理
从最简单的lock方法开始看看公平和非公平
可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
hasQueuedPredecessors()
hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
非公平锁走起,方法lock()
对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于 非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()
hasQueuedPredecessors() 中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
公平锁 :公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
非公平锁 :不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
hasQueuedPredecessors() 中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
公平锁 :公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
非公平锁 :不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
源码解读
lock()
acquire() 源码和3大流程走向
tryAcquire(arg) 本次走非公平锁
addWaiter(Node.EXCLUSIVE)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
假如再抢抢失败就会进入
shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法中
ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
路线总纲
无锁→独占锁→读写锁→邮戳锁
锁的大厂题目
你知道Java里面有哪些锁?
你说你用过读写锁,锁饥饿问题是什么?
有没有比读写锁更快的锁?
StampedLock知道吗?(邮戳锁/票据锁)
ReentrantReadWriteLock有锁降级机制策略你知道吗?
你说你用过读写锁,锁饥饿问题是什么?
有没有比读写锁更快的锁?
StampedLock知道吗?(邮戳锁/票据锁)
ReentrantReadWriteLock有锁降级机制策略你知道吗?
请你简单聊聊ReentrantReadWriteLock
是什么
读写锁定义为
一个资源能够被 多个读线程 访问,或者被 一个写线程 访问,但是不能同时存在读写线程。
一个资源能够被 多个读线程 访问,或者被 一个写线程 访问,但是不能同时存在读写线程。
特点和意义
『 读写锁 ReentrantReadWriteLock 』 并不是 真正意义上的读写分离, 它只允许读读共存,而读写和写写依然是互斥的,
大多实际场景是 “读/读”线程间并不存在互斥关系 ,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁( 切菜还是拍蒜选一个 )。
也即 一个资源可以被多个读操作访问或一个写操作访问 ,但两者不能同时进行。
只有在读多写少情境之下,读写锁才具有较高的性能体现
大多实际场景是 “读/读”线程间并不存在互斥关系 ,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁( 切菜还是拍蒜选一个 )。
也即 一个资源可以被多个读操作访问或一个写操作访问 ,但两者不能同时进行。
只有在读多写少情境之下,读写锁才具有较高的性能体现
从写锁→读锁,ReentrantReadWriteLock可以降级
读写锁之读写规矩,为什么要锁降级?
锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)
读写锁降级演示
锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
重入还允许通过获取写入锁定,然后读取锁然后释放写锁从写锁到读取锁,
但是,从读锁定升级到写锁是不可能的。
锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
但是,从读锁定升级到写锁是不可能的。
锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
源码解析
ReentrantWriteReadLock支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序, 写锁能够降级成为读锁 ,不支持锁升级。
1 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。
2 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性
2 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性
主要特性
1.公平性:支持公平性和非公平性。
2.重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
3.锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
4.读写锁除了读读不互斥,读写、写读、写写都是互斥的
2.重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
3.锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
4.读写锁除了读读不互斥,读写、写读、写写都是互斥的
有没有比读写锁更快的锁? ---- 有那就是:邮戳锁StampedLock
邮戳锁StampedLock
是什么
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。
邮戳锁也叫票据锁
stamp(戳记,long类型)代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
邮戳锁也叫票据锁
stamp(戳记,long类型)代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
它是由锁饥饿问题引出
锁饥饿问题
ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,
假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁, 根本没机会写
假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁, 根本没机会写
如何缓解锁饥饿问题?
使用“公平”策略可以一定程度上缓解这个问题 new ReentrantReadWriteLock(true);
但是“公平”策略是以牺牲系统吞吐量为代价的
但是“公平”策略是以牺牲系统吞吐量为代价的
StampedLock类的乐观读锁闪亮登场
ReentrantReadWriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态, 读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多, 原因就是在于 ReentrantReadWriteLock 支持 读并发
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态, 读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多, 原因就是在于 ReentrantReadWriteLock 支持 读并发
StampedLock横空出世
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时 不会被阻塞 ,这其实是对读锁的优化,
所以, 在获取乐观读锁后,还需要对结果进行校验
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时 不会被阻塞 ,这其实是对读锁的优化,
所以, 在获取乐观读锁后,还需要对结果进行校验
StampedLock的特点
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
StampedLock的缺点
StampedLock 不支持重入,没有Re开头
StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法
如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly()
0 条评论
下一页