尚硅谷JUC并发编程
2024-11-10 22:26:02 0 举报
AI智能生成
个人根据B站尚硅谷JUC课程学习并整理的
作者其他创作
大纲/内容
5、LockSupport与线程中断
内容简介
线程中断机制
面试题
面试题
什么是中断机制
子主题
1、一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。因此Thread.stop,Thread.suspend,Thread.resume都已经废弃了
2、在java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制
中断只是一种协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
中断的相关API方法之三大方法说明
API方法
interrupt()
中断这个线程。仅仅只是标记状态
interrupted()
public static boolean interrupted()
测试当前线程是否中断。 该方法可以清除线程的中断状态 。 换句话说,如果这个方法被连续调用两次,那么第二个调用将返回false(除非当前线程再次中断,在第一个调用已经清除其中断状态之后,在第二个调用之前已经检查过)。
忽略线程中断,因为线程在中断时不存在将被该方法返回false所反映。
忽略线程中断,因为线程在中断时不存在将被该方法返回false所反映。
结果
true如果当前线程已被中断; false否则。
true如果当前线程已被中断; false否则。
isInterrupted()
public boolean isInterrupted()
测试这个线程是否被中断。 线程的中断状态不受此方法的影响。
忽略线程中断,因为线程在中断时不存在将被该方法返回false所反映。
忽略线程中断,因为线程在中断时不存在将被该方法返回false所反映。
结果
true如果这个线程已被中断; false否则。
true如果这个线程已被中断; false否则。
大厂面试题中断机制考点
如何中断运行中的线程
通过一个volatile变量实现
通过AtomicBoolean
通过Thread类自己带的中断api实例方法实现
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程
API
子主题
子主题
具体来说,当对一个线程,调用interrupt()时:
① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而己。被设置中断标志的线程将继续正常运行,不受影响。
所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。
②) 如果线程处于被阻塞状态(例如处于sleep,wait,join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而己。被设置中断标志的线程将继续正常运行,不受影响。
所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。
②) 如果线程处于被阻塞状态(例如处于sleep,wait,join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
当前线程中断标识是true,是不是线程就立刻停止
sleep方法抛出InterruptedException后,中断标识也被清空置为法拉瑟,我们在catch没有通过调用th.interrupt()方法再次将中断标识置为true,这就导致无限循环了。
中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断
静态方法Thread.interrupted(),谈谈你的理解
静态方法Thread.interrupted()
说明
静态方法,Thread.interrupted();
判断线程是否被中断并清除当前中断状态。T这个方法做了两件事:
1 返回当前线程的中断状态,测试当前线程是否已被中断
2 将当前线程的中断状态清零并重新设为false,清除线程的中断状态
此方法有点不好理解,如果连续两次调用此方法,则第二次调用将返回false,因为连续调用两次的结果可能不一样
判断线程是否被中断并清除当前中断状态。T这个方法做了两件事:
1 返回当前线程的中断状态,测试当前线程是否已被中断
2 将当前线程的中断状态清零并重新设为false,清除线程的中断状态
此方法有点不好理解,如果连续两次调用此方法,则第二次调用将返回false,因为连续调用两次的结果可能不一样
子主题
子主题
code
都会返回中断状态,两者比对
实际都是调用了同一个方法,只是传参不同,
状态
isInterrupted()不会清除中断状态
interrupted()会清除中断状态
方法
isInterrupted()是实例方法
interrupted()是静态方法
总结
LockSupport是什么
是什么?
子主题
park
除非许可证可用,否则禁用当前线程以进行线程调度
unpark
如果给定线程尚不可用,则为其提供许可
线程等待唤醒机制
三种让线程等待和唤醒的方法
方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
方式2:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
代码
正常
子主题
异常1
wait方法和notify方法,两个都去掉同步代码块
异常情况
异常2
将notify放到wait方法前
程序无法执行,无法唤醒
小总结
wait和notify方法必须要在同步块或者方法里面,且成对出现使用
先wait后notify才OK
Condition接口中的await后signal方法实现线程的等待和唤醒
代码
正常
异常1
去掉lock和unlock块
子主题
异常2
先signal后await
小总结
Condition中的线程等待和唤醒必须要在lock中
必须要先等待后唤醒,线程才能够被唤醒
上述两个对象Object和Condition使用的限制条件
LockSupport类中的park等待和unpark唤醒
是什么?
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),但与Semaphore不同的是,许可的累加上限是1。
https://doc.qzxdp.cn/jdk/17/zh/api/java.base/java/util/concurrent/locks/LockSupport.html
主要方法
API
阻塞
park()/park(Object blocker)
阻塞当前线程/阻塞传入的具体线程
唤醒
Unpark(Thread thread)
调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回
唤醒处于阻塞状态的指定线程
代码
正常+无锁块要求
之前错误的先唤醒后等待,LockSupport照样支持
子主题
成双成对要牢记
重点说明(重要)
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
Locksupport是一个线程阻寒工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。
每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。
形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用 park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出:
*如果无凭证,就必须阻塞等待凭证可用:
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
*如果有凭证,则会直接消耗掉这个凭证然后正常退出:
*如果无凭证,就必须阻塞等待凭证可用:
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
面试题
为什么可以突破wait/notify的原有调用顺序
因为unpark获得了一个凭据,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞
先发放凭证后续可以畅通无阻
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
6、Java内存模型之JMM
大厂面试题
你知道什么是Java内存模型JMM吗?
JMM与volatile他们两个之间的关系?
JMM有哪些特性or它的三大特性是什么?
为什么要有JMM?它为啥出现?作用和功能是什么?
计算机硬件存储体系
子主题
重点:屏蔽掉各种硬件和操作系统内存的访问差异
Java内存模型Java Memory Model
子主题
Java内存模型Java Memory Model
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
原则:
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛?
1 通过JMM来实现线程和主内存之间的抽象关系
2 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
原则:
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛?
1 通过JMM来实现线程和主内存之间的抽象关系
2 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM规范下,三大特性
可见性
子主题
子主题
线程脏读
原子性
同一个操作不可打断,即多线程环境下,操作不能被其他线程干扰
有序性
简单案例
是什么?
是什么
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不致,此过程叫指令的重排序。
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不致,此过程叫指令的重排序。
优缺点
子主题
子主题
JMM规范下,多线程对变量的读写过程
子主题
读取过程
由于JM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
由于JM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
JMM定义了线程和主内存之间的抽象关系
1、线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
2、每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器L1、L2、L3缓存等)
小总结
我们定义的所有共享变量都存储在物理内存中
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能互相访问)
JMM规范下,多线程先行发生原则之Happens-before·
在JMM中
如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。逻辑上的先后关系
如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。逻辑上的先后关系
x、y案例说明
子主题
问题
先行发生原则说明
子主题
happens-before总原则
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,
而且第一个操作的执行顺序排在第二个操作之前
而且第一个操作的执行顺序排在第二个操作之前
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行
如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
值日的调换班
1+2+3=3+2+1
happen-before之8条
1 次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作
加深说明
前一个操作的结果可以被后续的操作获取。讲白点就是前面
一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1。
一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1。
2 锁定规则
一个unLock操作先行发生于后面(这里的“后面”是指时间上的先后)对同一个锁的lock操作
3 volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后
4 传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
5 线程启动规则(Thread start rule)
线程对象的start()方法先行于发生于此线程的每个动作
6 线程中断规则(Thread Interruption rule)
对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生
可以通过Thread.interrupted检测到是否发生中断
也就是说你要先调用interrupt方法设置过中断标志位,我才能检测到中断发送
7 线程终止规则(Thread Termination rule)
线程中的所有操作都发生于此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行
8 对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束),先行于发生于它的finalize()方法开始
直白点
finalize方法是回收之前执行的方法
happens-before-小总结
在java语言里面,Happens-before的语义本质上也是一种可见性
A Happens-before B 意味着A发生过的事情对B来说是可见的,无论A事件和B事件是否发生在同一个线程里
JMM的设计
一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了
另一部分是针对JMM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解happens-before规则即可,其它繁杂的内容有JMM规范结合操作系统给我们搞定,我们只写好代码就行
案例说明
子主题
修复方式
修复01(并发量小可以用这个)
把getter/setter方法都定义为synchronized方法
修复02
把Value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景
子主题
7、volatile与JMM
被volatile修饰的变量有2个大特点
特点
可见性
有序性
volatile语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读到一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
volatile如何可以保证可见性和有序性???
内存屏障Memory
内存屏障(面试重点)
先说生活case
没有管控,顺序难保
设定规则禁止乱序
再说volatile两个大特性
可见
写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对面所有的线程可见
有序(禁止重排)
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序不存在数据依赖关系,可以重排序;
存在数据依赖关系,禁止重排序,但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
是什么?
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。
内存屏障其实就是一种JVM指令,java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatie实现了java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
写屏障(Store Memory Barier):告诉处理器在写屏障之前将所有存储在缓存(store buferes)中的数据同步到主内存。也就是
说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。
说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。
读屏障(Load Memory Barier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
线程、主内存、工作内存三者的交互关系
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。
一句话:对一个volatie变量的写,先行发生于任意后续对这个
volatile变量的读,也叫写后读。
volatile变量的读,也叫写后读。
内存屏障分类
一句话
happens-brfore先行发生原则,类似接口规范的落地
粗分2种
读屏障
在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
写屏障
在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
细分4种
C++代码
java中的调用接口
unsafe。cpp
子主题
屏障类型
LoadLoad
Load1; LoadLoad; Load2
保证load1的读取操作在load2及后续读取操作之前执行
StoreStore
Store1; StoreStore; Store2
在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore
Load1; LoadStore; Store2
在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad
Store1; StoreLoad; Load2
保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行
细致重述
什么叫指令重排
禁止重排
通过内存屏障来禁止重排序
1重排序有可能影响程序的执行和实现,因此,我们有时候希望告诉JVM你别“自作聪明”给我重排序,我这里不需要排序,听主人的。
2 对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序。
3 对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。
2 对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序。
3 对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。
子主题
JMM就将内存屏障插入策略分为4种规则
读屏障
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
写屏障
在每个volatile写操作的后面插入一个storestore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障
volatile特性
保证可见性
说明
保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见
Code
上述代码原理解释
volatile变量的读写过程
read(读収)--load(加载)-use(便用)→assign(赋值)→store(存储)-write(写入)-lock(锁定)--unlock(解锁)
read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当VM遇到需要该变量的字节码指令时会执行该操作
assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令;
lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
没有原子性
volatile变量的复合操作不具有原子性,比如number++
code
关于读的过程
对于volatle变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不-致。由此可见volatle解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。
结论
volatile不适合参与到依赖当前值的运算
面试回答
JVM的字节码,i++分成三步,间隙期不同步非原子操作(i++)
对于volatle变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也只是数据加载时是最新的。
如果第二个线程在第一个线程读取旧值和写回新值期间读取的域值,也就造成了线程安全问题。
如果第二个线程在第一个线程读取旧值和写回新值期间读取的域值,也就造成了线程安全问题。
指令禁止重排序
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序;
存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
重排序的分类和执行流程
子主题
编译器优化的重排序:编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
指令级并行的重排序;处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
数据依赖性
若两个操作访问同一个变量,且这两个操作中有一个为写操作,此时两操作间都存在数据依赖性
案例
子主题
子主题
读之后,写之前
在每一个volatile写操作前面插入一个storestore屏障
storestore可以保证在volatile写之前,其他的所有普通写操作都已经刷新到主内存中
在每一个volatile写操作后面插入一个storeLoad屏障
storeLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
在每个volatile的读操作后面插入一个loadload屏障
loadload屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
在每个volatile读操作后面插入一个loadstore屏障
loadstore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序
子主题
子主题
写的时候内存屏障是在前后
子主题
读的时候内存屏障都放到后面
如何正确使用volatile
单一赋值可以,但含复合运算复制不可以(i++之类)
状态标志,判断业务是否结束
开销较低的读、写锁策略
DCL双端锁的发布
子主题
本章最后的小总结
volatile可见性
volatile没有原子性
volatile禁重排
写操作
读操作
凭什么我们java写一个volatile系统底层加入内存屏障?两者关系怎么勾搭上的
只要有了volatile,jvm编译的时候就会加入一个关键字ACC_VOLATILE,再把字节码编译成机器码的时候就会加入内存屏障
内存屏障是什么?
内存屏障 : 是一种 屏障指令,它使得 CPU 或编译器 对 屏障指令的前 和 后所发出的内存操作 执行一个排序的约束。也叫内存栅栏 或 栅栏指令
内存屏障能干嘛?
阻止屏障两边的指令重排序
写数据的时候加入屏障,强制线程私有工作内存的数据刷到主物理内存
读操作的时候加入屏障,线程私有工作内存的数据失效,重新到主内存中获取最新数据
内存屏障四大指令
3句话总结
volatile写之前的操作,都禁止重排序到volatile之后
Volatile读之后的操作,都禁止重排序到volatile之前
volatile写之后volatile读,禁止重排序
9、原子操作类之十八罗汉增强
是什么?
atomic
AtomicBoolean
可以自动更新的 boolean 值。
AtomicInteger
可以自动更新的 int 值。
AtomicIntegerArray
一个 int 数组,其中的元素可以自动更新。
AtomicIntegerFieldUpdater <T>
一种基于反射的实用程序,可以对指定类的指定 volatile int 字段进行原子更新。
AtomicLong
可以自动更新的 long 值。
AtomicLongArray
一个 long 数组,其中的元素可以自动更新。
AtomicLongFieldUpdater <T>
一种基于反射的实用程序,可以对指定类的指定 volatile long 字段进行原子更新。
AtomicMarkableReference<V>
AtomicMarkableReference 维护一个对象引用和一个标记位,可以自动更新。
AtomicReference<V>
可以自动更新的对象引用。
AtomicReferenceFieldUpdater <T,V>
一种基于反射的实用程序,可以对指定类的指定 volatile 引用字段进行原子更新。
AtomicStampedReference <V>
AtomicStampedReference 维护一个对象引用以及一个可以自动更新的整数“戳记”。
DoubleAccumulator
一个或多个变量共同维护使用提供的函数更新的运行 double 值。
DoubleAdder
一个或多个变量一起保持最初为零的 double 总和。
LongAccumulator
一个或多个变量共同维护使用提供的函数更新的运行 long 值。
LongAdder
一个或多个变量一起保持初始为零的 long 总和
再分类
基本类型原子类
AtomicBoolean
AtomicInteger
AtomicIntegerArray
常见API
public final int get()//获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement()//获取当前的值,并自减
public final int getAndAdd(int delta)//获取当前的值,并加上预期的值
boolean compareAndSet(int expect,int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
Case
用CountDownLatch代替睡眠的方式
子主题
数组类型原子类
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
case
代码使用
引用类型原子类
AtomicReference
自旋锁
子主题
AtomicStampedReference
携带版本号的引用类型原子类,可以解决ABA问题
解决修改过几次
状态戳原子引用
ABADemo
AtomicMarkableReference
原子更新带有标记位的引用类型对象
解决是否修改过
它的定义是将状态戳简化为true/false
类似一次性筷子
状态戳(true/false)原子引用
case
子主题
对象的属性修改原子类
AtomicIntegerFieldUpdater <T>
原子更新对象中int类型字段的值
AtomicLongFieldUpdater <T>
原子更新对象中Long类型字段的值
AtomicReferenceFieldUpdater <T,V>
原子更新引用类型字段的值
使用目的
以一种线程安全的方式操作非线程安全对象内的某些字段
使用要求
更新的对象属性必须使用public volatile修饰符。
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
面试官问你:你在哪里用了volatile
AtomicReferenceFieldUpdater <T,V>
case
原子操作增强类原理深度解析
点赞计数器,看看性能
常用API
入门讲解
LongAdder高性能对比code演示
查看代码
源码原理分析
架构
十八罗汉剩下的两个重要的罗汉是Striped64、Number
原理(LongAdder为什么这么快)
官网说明和阿里要求
阿里java开发手册
LongAdder是Striped64的子类
子主题
Striped64
子主题
子主题
Cell
LongAdder为什么这么快
简单描述
LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
子主题
数学表达
内部有一个base变量,一个cell[]数组
base变量:低并发,直接累加到该变量上
cell[]数组:高并发,累加进各个线程自己的槽cell[i]中
公式
小总结
LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cels,将一个value拆分进这个数组cels。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cels的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cels的所有值和base都加起来作为最终结果。
源码解读深度分析
add(1L)
条件递增,逐步解析
最初无竞争时只更新base
如果更新base失败后,首次创建一个Cell[]数组
当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容
上述小总结
代码梳理
精简版
longAccumulate
longAccumlate入参说明
子主题
Striped64中一些变量或方法的定义
变量或者方法的定义
方法的入参
步骤
线程hash值:probe
获取线程的hash值
总纲
上述代码首先给当前线程分配一个hash值,然后进入
一个for(;;)自旋,这个自旋分为三个分支:
CASE1:CeI]数组已经初始化
CASE2:CeI数组未初始化(首次新建)
CASE3:CelI]数组正在初始化中
一个for(;;)自旋,这个自旋分为三个分支:
CASE1:CeI]数组已经初始化
CASE2:CeI数组未初始化(首次新建)
CASE3:CelI]数组正在初始化中
计算
刚刚要初始化Cell[]数组(首次新建)
未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组
子主题
兜底
多个线程尝试CAS修改失败的线程会走到这个分支
子主题
Cell数组不再为空且可能存在Cell数组扩容
多个线程同时命中一个cell的竞争
总体代码
1、
97节重点看
上面代码判断当前线程hash后指向的数据位置元素是否为空
如果为空则将Cell数据放入数组中,跳出循环。
如果不空则继续循环
如果为空则将Cell数据放入数组中,跳出循环。
如果不空则继续循环
2、
2
子主题
3、
说明当前线程对应的数组中有了数据,也重置过hash值,
这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环
这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环
4、
如果n大于CPU最大数量,不可扩容
并通过下面的h=advanceProbe(h)方法修改线程的probe再重新尝试
并通过下面的h=advanceProbe(h)方法修改线程的probe再重新尝试
5、
如果扩容意向collide是false则修改它为true,然后重新计算当前线程的hash值继续循环,
如果当前数组的长度已经大于了CPU的核数,就会再次设置扩容意向collide=false(见上一步)
如果当前数组的长度已经大于了CPU的核数,就会再次设置扩容意向collide=false(见上一步)
6、
子主题
以上6个步骤总结
子主题
sum
为啥在并发情况下sum的值不精确
sum()会将所有Cell数组中的value和base累加作为返回值。
核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
sum执行时,并没有限制对base和cels的更新(一句要命的话)。所以LongAdder不是强一致性的,它是最终一致性的。
首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。
其次,这里对cel的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。
首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。
其次,这里对cel的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。
使用总结
AtomicLong
线程安全
可允许一些性能损耗,要求高精度时可使用
保证精度,性能代价
AtomicLong是多个线程针对单个热点值value进行原子操作
LongAdder
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
保证性能,精度代价
LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS
小总结
AtomicLong
原理
CAS+自旋
incrementAndGet
场景
低并发下的全局计算
AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
缺陷
高并发后性能急剧下降
why?
AtomicLong的自旋会成为瓶颈
N个线程CAS操作修改线程的值,每次只有一个成功过,其它N-1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。
LongAdder
原理
CAS+Base+Cell数组分散
空间换时间并分散了热点数据
场景
高并发下的全局计算
缺陷
sum求和后还有计算线程修改结果的话,最后结果不准确
10、ThreadLocal(线程局部变量)
ThreadLocal简介
恶心的大厂面试题
ThreadLocal中ThreadLocalMap的数据结构
ThreadLocal的key是弱引用,这是为什么?
ThreadLocal内存泄露问题你知道吗?
ThreadLocal中最后为什么要加remove方法?
其他
是什么
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e. g., a user ID or Transaction ID).
此类提供线程局部变量。这些变量与它们的普通对应项不同,因为访问一个变量(通过其 get 或 set 方法)的每个线程都有其自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,希望将状态与线程(例如,用户 ID 或事务 ID)相关联。
能干嘛
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题,比如我们之前讲解的8锁案例,资源类是使用同一部手机,多个线程抢夺同一部手机使用,假如人手一份是不是天下太平??
子主题
举例每个玩家的经济,血槽,装备都不同
api介绍
子主题
protected T initialValue()
返回此线程局部变量的当前线程的“初始值”。此方法将在线程第一次使用 get() 方法访问变量时调用,除非线程先前调用了 set(T) 方法,在这种情况下不会为该线程调用 initialValue 方法。通常,每个线程最多调用一次此方法,但在后续调用 remove() 后跟 get() 的情况下可能会再次调用它。
此实现仅返回 null ;如果程序员希望线程局部变量具有 null 以外的初始值,则必须对 ThreadLocal 进行子类化,并覆盖此方法。通常,将使用匿名内部类。
返回:
此线程本地的初始值
此实现仅返回 null ;如果程序员希望线程局部变量具有 null 以外的初始值,则必须对 ThreadLocal 进行子类化,并覆盖此方法。通常,将使用匿名内部类。
返回:
此线程本地的初始值
public static <S> ThreadLocal <S> withInitial(Supplier <? extends S> supplier)
创建线程局部变量。变量的初始值通过调用 Supplier 上的 get 方法来确定。
类型参数:
S - 线程本地值的类型
参数:
supplier - 用于确定初始值的供应商
返回:
一个新的线程局部变量
抛出:
NullPointerException - 如果指定的供应商为空
类型参数:
S - 线程本地值的类型
参数:
supplier - 用于确定初始值的供应商
返回:
一个新的线程局部变量
抛出:
NullPointerException - 如果指定的供应商为空
public T get()
返回此线程局部变量的当前线程副本中的值。如果该变量对于当前线程没有值,则首将其初始化为调用 initialValue() 方法返回的值。
返回:
此线程局部的当前线程值
返回:
此线程局部的当前线程值
public void set(T value)
将此线程局部变量的当前线程副本设置为指定值。大多数子类不需要重写此方法,仅依赖initialValue() 方法来设置线程局部变量的值。
参数:
value - 要存储在此线程本地的当前线程副本中的值。
参数:
value - 要存储在此线程本地的当前线程副本中的值。
public void remove()
删除此线程局部变量的当前线程值。如果此线程局部变量随后被当前线程 read 调用,则其值将通过调用其 initialValue() 方法重新初始化,除非其值在此期间被当前线程变为 set 。这可能会导致在当前线程中多次调用 initialValue 方法。
永远的hellword讲起
子主题
使用实例
6.【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理
自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally 块进行回收。
正例:
自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally 块进行回收。
正例:
子主题
通过上面代码总结
因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用
既然其他Thread不可访问,那就不存在多线程间共享的问题
统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
一句话
如何才能不争抢
加入synchronized或者Lock控制资源的访问顺序
人手一份,大家各自安好,没必要抢夺
ThreadLocal源码分析
源码解读
Thread、ThreadLocal,ThreadLocalMap关系
第一个知识点
Thread类中有一个变量ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是ThreadLocal的静态内部类
Thread和ThreadLocal
各自线程人手一份不和别人共用
子主题
ThreadLocal和ThreadLocalMap
ThreadLocalMap当中有一个静态类Entry
ThreadLocal的get()方法
获取map
设置初始值
子主题
All三者概括
三者关系
threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。
void createMap(Thread t,T firstValue){
t.threadLocals = new ThreadLocalMap( this, firstValue);
}
当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
void createMap(Thread t,T firstValue){
t.threadLocals = new ThreadLocalMap( this, firstValue);
}
当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
子主题
小总结
近似的可以理解为:
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
JVM内部维护了一个线程版的Map<ThreadLocal,Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
ThreadLocal内存泄漏问题
问题
为啥在ThreadLocalMap中的Entry要用一个弱引用?不用会怎样?
是如何导致内存泄漏的?
什么内存泄漏
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏
谁惹的祸
ThreadLocalMap与WeakReference
ThreadLocaMap从字面上就可以看出这是一个保存ThreadLocal对象的map(以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference<ThreadLocal<?>>将ThreadLocal对象变成一个弱引用的对象;
(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>:
关于引用
整体架构
finalize方法(11以后已经被弃用)
当垃圾收集确定没有对该对象的更多引用时,由对象上的垃圾收集器调用。
子类重写finalize方法以处置系统资源或执行其他清理
子类重写finalize方法以处置系统资源或执行其他清理
finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作
新建一个带finalize方法的对象MyObject
新建一个带finalize()方法的对象MyObject
强引用(默认支持模式)
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 nul,-般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
case
强引用
子主题
软引用
case
添加虚拟机参数最大10M(-Xms10m -Xmx10m),在程序中加载一个20M对象造成内存不够用
运行结果
解释
软引用是一种相对于强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
软引用的对象来说
当系统内存充足时,它不会被回收
当系统内存不足时,它会会被回收
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
弱引用
case
弱引用
运行结果
解释
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
软引用和弱引用的适用场景
假如有一个应用需要读取大量的本地图片
难点
如果每次读取图片都从硬盘读取则会严重影响性能
如果一次性全部加载到内存中又可能造成内存溢出
设计思路
此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SofReference<Bitmap>>();
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SofReference<Bitmap>>();
虚引用
case
子主题
运行结果
解释
虚引用必须和引用队列(ReferenceQueue)联合使用
虚引用需要java.lang.ref.PhantomReference类来实现,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虑引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
PhantomReference的get方法总是返回null
虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的通知机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
处理监控通知使用
换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作
API说明
PhantomReference类解释
构造方法
引用队列
GCRoots和四大引用小总结
四大引用
ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
1)调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
2)调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
2)调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为一个key来让线程从ThreadLocalMap获取value.
正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
为啥要用弱引用,强引用行不
line1新建了一个ThreadLocal对象,t1是强引用指向这个对象;
line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象
line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象
子主题
为什么源代码用弱引用?
当function01方法执行完毕后,栈帧销毁强引用t1也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷,第2个坑后面讲)。使用弱引用,
就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
弱引用就万事大吉了吗?
当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(t1=null,那么系统 GC 的时候,根据可达性分析,这个treadLoca实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。这样一来,ThreadLocalMap中就会出现key为null的Enty,就没有办法访问这些key为null的Entry的value.如果当前线程再迟迟不结束的话(线程池中的线程有的是一直不结束的),这些key为null的Enty的value就会一直存在一条强引用链: Thread Ref -> Thread -> ThreaLocalMap-> Enty ->value永远无法回收,造成内存泄漏。
当然,如果当前thread运行结束,threadLocal.threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
set、get方法会去检查所有键为null的Entry对象
expungeStaleEntry
set()
get()
remove()
寻找脏Entry,即key=null的Entry,然后进行删除
结论
从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。
最佳实践
ThreadLocal.withInitial(()->初始值)
建议把ThreadLocal修饰为static
阿里手册
ThreadLocal能实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap所以,ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。
用完记得手动remove
总结
ThreadLocal并不解决线程间共享数据的问题
ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
每个线程持有一个只属于自己的专属Map并维护了threadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry,对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法
群雄逐鹿起纷争,人各一份天下安
11、java对象内存布局和对象头
基础知识
从start一个线程说起
java多线程相关概念
一把锁
synchronized
2个并
并发
是在同一实体上的多个事件,
是在一台机器上“同时”处理多个任务
同一时刻,其实是只有一个事情再发生
并行
是在不同实体上的多个事件,
是在多台处理器上同时处理多个任务,
同一时刻,大家都在做事情,你做你的,我做我的,各干各的。
并发VS并行
子主题
3个程
进程
在系统中运行的一个应用程序,每个进程都有它自己的内存空间和系统资源
线程
也被称为轻量级进程,在同一个进程内会有1个或多个线程,是大多数操作系统进行时序调度的基本单元。
管程
Monitor(锁),也就是我们平时所说的锁。Monitor其实是一种同步机制,它的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码,JVM中同步是基于进入和退出监视器(Monitor管程对象)来实现的,每个对象实例都会有一个Monitor对象,Monitor对象和Java对象一同创建并销毁,底层由C++语言实现。
用户线程和守护线程
一般情况下不做特别说明,默认都是用户线程。
用户线程(User Thread)
是系统的工作线程,它会完成这个程序需要完成的业务操作。
守护线程(Daemon Thread)
是一种特殊的线程为其他线程服务的,在后台默默地完成一些系统性的任务,比如垃圾回收线程就是最典型的例子。
守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以假如当系统只剩下守护线程的时候,守护线程伴随着JVM一同结束工作。
线程的daemon属性
true表示是守护线程
false表示是用户线程
代码
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个legalThreadStateException异常!
你不能把正在运行的常规线程设置为守护线程。
你不能把正在运行的常规线程设置为守护线程。
3.CompletableFuture
Future接口理论知识复习
取消任务的执行、中断任务是否被Future接囗(FutureTask实现类)定义了操作异步任务执行些方法,如获取异步任务的执行结果取消、判断任务执行是否完毕等。
比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,过了一会才去获取子任务的执行结果或变更的任务状态。主线程就去做其他事情了,忙其它事情或者先执行完,
举例上课买水案例给大家说明补充。。。。。。
Future接口常用实现类FutureTask异步任务
Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
代码说话:
Runnable接口
Callable接口
Future接口和FutureTask实现类
目的:异步多线程任务执行且返回有结果,三个特点:多线程/有返回/异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)
代码说话:
Runnable接口
Callable接口
Future接口和FutureTask实现类
目的:异步多线程任务执行且返回有结果,三个特点:多线程/有返回/异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)
三个特点
多线程
有返回
异步任务
子主题
关于Future的使用
子主题
Future编码实战的优缺点分析
优点
future+线程池异步多线程任务配合,能显著提高程序的执行效率
代码展示,线程池结合Future
子主题
缺点
get()阻塞
代码
CompletableFuture对Future的改进
CompletableFuture的出现
源码分析
接口CompletionStage是什么
CompletableFuture类
CompletableFuture为啥出现
核心的四个静态方法,来创建一个异步任务
runAsync 无 返回值
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable,
Executor executor)
Executor executor)
返回一个新的 CompletableFuture,它在运行给定操作后由 ForkJoinPool.commonPool() 中运行的任务异步完成。
supplyAsync 有 返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
Executor executor)
Executor executor)
返回一个新的 CompletableFuture,它由 ForkJoinPool.commonPool() 中运行的任务异步完成,其值是通过调用给定的 Supplier 获得的。
上述Executor参数说明
没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool()作为它的线程池执行异步代码。
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
code
code通用演示,减少轮询和阻塞
案例精讲:从电商网站的比价需求说开去
CompletableFuture常用方法
获得结果和触发计算
获取结果
public T get()
public T get(long timeout, TimeUnit unit)
public T join()
public T getNow(T valueIfAbsent)
主动触发计算
public boolean complete(T value)
对计算结果进行处理
thenApply
计算结果存在依赖关系,这两个线程串行化
异常相关
由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
handle
计算结果存在依赖关系,这两个线程串行化
异常相关
有异常也可也往下走一步,根据带的异常参数可以进一步处理。
总结
子主题
对计算结果进行消费
接受任务的处理结果,并消费处理,无返回结果
thenAccept
对比补充
Code之任务之间的顺序执行
thenRun
在任务A执行完执行B,并且B不需要A的结果
thenAccept
任务A执行完,执行任务B,B需要A的结果,但是任务B无返回值
thenApply
任务A执行完执行B,B需要A的结果,同时任务B有返回值
子主题
CompletableFuture和线程池说明
以thenRun和thenRunAsync为例,有什么区别?
code
小总结
子主题
源码分析
对计算速度进行选用
谁快用谁
applyToEither
code
对结算结果进行合并
两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理先
完成的先等着,等待其它分支任务
thenCombine
标准版
4、java的锁
乐观锁和悲观锁
悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确
显式的锁定之后再操作同步资源
乐观锁
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁则直接操作同步资源,是一种无锁算法,得之我幸不得我命,再重来就行
一句话总结
佛系
乐观锁一般有两种实现方式:
采用version制
采用CAS
通过8种情况演示锁运行案例,看看我们到底锁的是什么?
锁的相关的8种案例演示code
阿里要求
【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
子主题
子主题
Synchronized有三种应用方式
静态同步方法
持有的锁是类
普通同步方法
持有的锁是对象
同步代码块
持有的锁是synchronized(obj)中的参数
从字节码角度分析Synchronized实现
javap -c ***.class文件反编译
-c 对代码进行反汇编
假如你需要更多的信息
javap -v ***.class 文件反编译
-v -verbose 输出附加信息(包括行号、本地变量表、反汇编等详细信息)
synchronized 同步代码块
实现使用的是monitorenter和monitorexit指令
一定是monitorenter和monitorexit,一个enter对两个exit的比例吗?
一般是
为了避免异常导致一直持有锁的情况
极端情况是方法中带有一个异常的抛出的情况
synchronized 普通同步方法
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
synchronized 静态同步方法
ACC STATIC,ACC SYNCHRONIZED访问标志区分该方法是否静态同步方法
反编译Synchronized锁的是什么
面试题:为什么任何一个对象都可以成为一个锁
在Hostpot虚拟机中,monistor采用ObjectMonitor实现
C++源码解读
ObjectMonitor.java->ObjectMonitor.cpp->objectMonitor.hpp
objectMonitor.hpp
子主题
子主题
每个对象天生都带一个对象监视器
每一个被锁住的对象都会和Monitor关联起来
什么管程monitor
管程 (英语:Monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
这些共享资源一般是硬件设各或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
这些共享资源一般是硬件设各或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
对于synchronized关键字
子主题
子主题
公平锁和非公平锁
从ReentrantLock卖票demo演示公平和非公平现象
何为公平锁和非公平锁?
关于公平锁和非公平锁的定义
面试题
为什么会有公平锁/非公平锁的设计?为什么默认非公平
什么时候用公平?什么时候用非公平?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了
否则就用公平锁,公平使用
预埋伏AQS
AQS,实现公平锁和非公平锁的原理
可重入锁(又名递归锁)
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次#入被自己阻塞了岂不是天大的笑话,出现了作茧自缚,所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”这四个字分开解释
可:可以
重:再次
入:进入
锁:同步锁
进入什么?
进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话总结
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
自己可以获取自己的内部锁
可重入锁的种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
同步块
同步方法
Synchronized的重入的实现机理
显式锁也有ReentrantLock这样的可重入锁。
死锁及排查
是什么?
子主题
请写一个死锁的代码case
如何排查死锁
java自带的原生命令
jps -l
jstack进程编号
图形化
jconsole
写锁(独占锁)/读锁(共享锁)
自旋锁spinLock
无锁->独占锁->读写锁->邮戳锁
无锁->偏向锁->轻量级锁->重量锁
小总结
指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码
ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor.hpp文件,C++实现的)
子主题
子主题
8、CAS
原子类
java.util.concurrent.atomic
没有CAS之前
多线程环境不使用原子类保证线程安全i++(基本数据类型)
使用CAS之后
多线程环境 使用原子类保证线程安全i++(基本数据类型)
类似于乐观锁
是什么
什么叫CAS
原理
原理说明
CASDemo代码
子主题
硬件级别保证
CAS是JDK提供的非阻寒原子性操作,它通过硬件保证了比较-更新的原子性,它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
源码分析CompareAndSet(int expect,int update)
CAS底层原理?如果知道,谈谈你对UnSafe的理解
unSafe
1 Unsafe
是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调川操作系统底层资源执行相应任务
是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调川操作系统底层资源执行相应任务
2 变量valueOfset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3 变量value用volatile修饰,保证了多线程之间的内存可见性。
存在的位置 lib/sun/misc文件夹下
我们知道I++线程不安全,那么atomicInteger.getAndIncrement()
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。Atomicnteger 类主要利用 CAS (compare and swap)+ volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销执行效率大为提升。
CAS并发原语体现在JAVA语言中就是sun.misc,Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若于条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
源码fenxi
OpenJDK源码
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):
1、Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有份值为3的value的副本分别到各自的工作内存。
2、线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起。
3 线程B也通过getlntVolatile(var1,var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndswaplnt方法,比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
4 这时线程A恢复,执行compareAndSwapint方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
5 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
底层汇编
native修饰的方法代表是底层方法
汇编代码
cmpxchg
子主题
在不同的操作系统下会调用不同的cmpxchg重载函数,win10的
子主题
总结
你只需要记住:CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。
核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。
核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。
原子引用
CAS与自旋锁,借鉴CAS思想
是什么?
子主题
自己实现一个自旋锁SpinLockDemo
CAS缺点
循环时间长CPU开销很大
子主题
引出来ABA问题???
CAS会导致“ABA问题”
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B.
然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B.
然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
版本号时间戳原子引用
子主题
ABADemo
第九章还有一个
一句话
比较+版本号
收藏
收藏
0 条评论
下一页