JUC并发编程
2024-08-07 09:48:14 3 举报
AI智能生成
111
作者其他创作
大纲/内容
本课程前置要求说明
本课程学生对象
零基础就业班JUC知识串讲复习,快快过一下
零基础就业班,阳哥JUC四大口诀
高内聚低耦合前提下,封装思想
线程
操作
资源类
判断、干活、通知
防止虚假唤醒,wait方法要注意
注意标志位flag,可能是volatile的
JUC要求的知识内容,请回忆下
ReentrantLock
ReentrantReadWriteLock
Condition
工具类
CountDownLatch
CyclicBarrier
Semaphore
线程池与阻塞队列
ForkJoinPool与ForkJoinTask
Java8新特性函数式编程、方法引用、lambda Express
原子操作类Atomic
volatile
Callable和FutureTask
。。。。。。
本课程的难度对标
阿里P6---P7
易中难章节说明
阿里P6、P7对高级Java开发工程师的要求明细
技术栈
阿里手册规范
线程基础知识复习
先拜拜大神
Doug Lea(道格.利)
零基础就业班讲解时阳哥JUC四大口诀
高内聚低耦合前提下,封装思想
线程
操作
资源类
判断、干活、通知
防止虚假唤醒,wait方法要注意使用while判断
注意标志位flag,可能是volatile的
为什么多线程极其重要???
硬件方面
摩尔定律失效
软件方面
zhuanability
高并发系统,异步+回调等生产需求
从start一个线程说起
Java线程理解以及openjdk中的实现
private native void start0();
Java语言本身底层就是C++语言
OpenJDK源码网址
http://openjdk.java.net/
建议下载源码到本地观看
openjdk8\hotspot\src\share\vm\runtime
更加底层的C++源码解读
openjdk8\jdk\src\share\native\java\lang
thread.c
openjdk8\hotspot\src\share\vm\prims
jvm.cpp
openjdk8\hotspot\src\share\vm\runtime
thread.cpp
Java多线程相关概念
进程
是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源
线程
在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程
⼀个进程会有1个或多个线程的
面试题:何为进程和线程?
分支主题
分支主题
分支主题
分支主题
分支主题
分支主题
分支主题
分支主题
分支主题
分支主题
分支主题
管程
Monitor(监视器),也就是我们平时所说的锁
JVM第3版
用户线程和守护线程
Java线程分为用户线程和守护线程,
线程的daemon属性为true表示是守护线程,false表示是用户线程
守护线程
是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
用户线程
是系统的工作线程,它会完成这个程序需要完成的业务操作
code
默认为用户线程
重点
当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出
如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,
系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出
注意:设置守护线程,需要在start()方法之前进行
why
CompletableFuture
Future和Callable接口
从之前的FutureTask开始
本源的Future接口相关架构
Code
get()获取返回值会阻塞
一旦调用get()方法,不管是否计算完成都会等待结果返回,导致阻塞,o(╥﹏╥)o
get方法一旦调用会去取异步返回结果,如果此时异步处理还没有结束,需要等待,导致阻塞,后面代码无法执行
一般建议放在程序最后
Code2
isDone()轮询
轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果.
如果想要异步获取结果,通常都会以轮询的方式去获取结果尽量不要阻塞
小总结
不见不散
过时不候
轮询
想完成一些复杂的任务
应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果。
当Future集合中某个任务最快结束时,返回结果。
等待Future结合中的所有任务都完成。
。。。。。。
对Future的改进
CompletableFuture和CompletionStage源码分别介绍
类架构说明
接口CompletionStage
是什么
类CompletableFuture
是什么
核心的四个静态方法,来创建一个异步操作
runAsync 无 返回值
public static CompletableFuture runAsync(Runnable runnable)
public static CompletableFuture runAsync(Runnable runnable,Executor executor)
supplyAsync 有 返回值
public static CompletableFuture supplyAsync(Supplier supplier)
public static CompletableFuture supplyAsync(Supplier supplier,Executor executor)
上述Executor executor参数说明
没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool() 作为它的线程池执行异步代码。
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
Code
无 返回值
get获取的值是null
有 返回值
get方法获取返回值
函数式编程
whenComplete
接收上一步的结果和异常(v,e)-> {}
exceptionally最终处理方法,返回值或者抛出异常
异常处理(e -> {})
Code之通用演示,减少阻塞和轮询
从Java8开始引入了CompletableFuture,它是Future的功能增强版,
可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
code
解释下为什么默认线程池关闭,自定义线程池记得关闭
CompletableFuture的优点
异步任务结束时,会自动回调某个对象的方法;
异步任务出错时,会自动回调某个对象的方法;
主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
主线程执行不受异步线程影响,主线程无需等待get获取结果而阻塞
案例精讲-从电商网站的比价需求说开去
函数式编程已经主流
大厂面试题看看
Lambda +Stream+链式调用+Java8函数式编程带走
Runnable
内部消化,无输入输出
Function
输入输出(一个参数)
Consumer
BiConsumer
有输入无输出
Supplier
无输入,有输出
提供者/生产者
小总结
先说说join和get对比
说说你过去工作中的项目亮点?
大厂业务需求说明
切记,功能→性能
对内微服务多系统调用
对外网站比价
一波流Java8函数式编程带走
导入Lombok依赖
CompletableFuture常用方法
获得结果和触发计算
获取结果
public T get()
不见不散
一直死等到结果,容易阻塞
public T get(long timeout, TimeUnit unit)
过时不候
必须在指定时间拿到结果,超过指定时间报timeout错误,容易出现阻塞
public T join()
类似get
public T getNow(T valueIfAbsent)
当前线程需要数据时,如果没有计算线程没有完成计算的情况下,给我一个替代结果
立即获取结果不阻塞
计算完,返回计算完成后的结果
没算完,返回设定的valueIfAbsent值
Code
主动触发计算
public boolean complete(T value)
判断多线程是否已经计算出结果,如果没有计算结果,返回true,且根据这个结果做后续定制
是否打断get方法立即返回自定义括号值
打断方法返回true,并返回设定值,
正常执行则为false,返回线程计算值
code
对计算结果进行中间处理
thenApply
计算结果存在依赖关系,这两个线程串行化
code
code2
由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
串行化处理,中途出现异常就会报错终止
handle
code
有异常也可以往下一步走,根据带的异常参数可以进一步处理
上一步的异常可以在下一步中被捕获和处理,本层无法处理,不进行处理,同样会中断异步执行结果,并且异常出现的数据为null无法使用
总结
对计算结果进行消费
接收任务的处理结果,并消费处理,无返回结果
thenAccept
补充
Code之任务之间的顺序执行
thenRun
thenRun(Runnable runnable)
任务 A 执行完执行 B,并且 B 不需要 A 的结果
各忙各的
无输入无输出
thenAccept
消费型函数接口
有输入无输出
thenAccept(Consumer action)
任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 无返回值
thenApply
有输入有输出
thenApply(Function fn)
任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值
code
CompletableFuture和线程池的说明
以thenRun和thenRunAsync为例,有什么区别?
code
小总结
源码分析
对计算速度进行选用
谁快用谁
applyToEither
code
类似于比较equal来进行判断
对计算结果进行合并
两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine 来处理
先完成的先等着,等待其它分支任务
thenCombine
code标准版,好理解先拆分
code表达式
自己理解+动手
说说Java“锁”事
大厂面试题复盘
从轻松的乐观锁和悲观锁开讲
悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确。
显式的锁定之后再操作同步资源
一句话:狼性锁
乐观锁
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢
乐观锁一般有两种实现方式:
采用版本号机制
CAS(Compare-and-Swap,即比较并替换)算法实现
伪代码说明
通过8种情况演示锁运行案例,看看我们到底锁的是什么
承前启后的复习一下
锁相关的8种案例演示
看看JVM中对应的锁在哪里?
synchronized有三种应用方式
JDK源码(notify方法)说明举例
8种锁的案例实际体现在3个地方
作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
作用于代码块,对括号里配置的对象加锁。
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
从字节码角度分析synchronized实现
javap -c ***.class文件反编译
-c 对代码进行反汇编
假如你需要更多信息
javap -v ***.class文件反编译
-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
synchronized同步代码块
javap -c ***.class文件反编译
反编译
synchronized同步代码块
实现使用的是monitorenter和monitorexit指令
一定是一个enter两个exit吗?
m1方法里面自己添加一个异常试试
synchronized普通同步方法
javap -v ***.class文件反编译
反编译
synchronized普通同步方法
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。
如果设置了,执行线程会将先持有monitor然后再执行方法,
最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor
synchronized静态同步方法
javap -v ***.class文件反编译
反编译
synchronized静态同步方法
ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法
反编译synchronized锁的是什么
什么是管程monitor
大厂面试题讲解
为什么任何一个对象都可以成为一个锁
管程
在HotSpot虚拟机中,monitor采用ObjectMonitor实现
上述C++源码解读
ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp
objectMonitor.hpp
每个对象天生都带着一个对象监视器
对于synchronized关键字,
我们在《Synchronized与锁升级》
章节还会再深度讲解
提前剧透,混个眼熟
公平锁和非公平锁
从ReentrantLock卖票编码演示公平和非公平现象
何为公平锁/非公平锁?
源码解读
面试题
为什么会有公平锁/非公平锁的设计为什么默认非公平?
使⽤公平锁会有什么问题
什么时候用公平?什么时候用非公平?
预埋伏AQS
更进一步的源码深度分析见后续第13章
可重入锁(又名递归锁)
说明
“可重入锁”这四个字分开来解释:
可:可以。
重:再次。
入:进入。
锁:同步锁。
进入什么
进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
自己可以获取自己的内部锁
可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
同步块
同步方法
Synchronized的重入的实现机理
为什么任何一个对象都可以成为一个锁
显式锁(即Lock)也有ReentrantLock这样的可重入锁。
死锁及排查
是什么
产生死锁主要原因
系统资源不足
进程运行推进的顺序不合适
资源分配不当
请写一个死锁代码case
如何排查死锁
纯命令
jps -l
jstack 进程编号
图形化
jconsole
小总结
写锁(独占锁)/读锁(共享锁)
源码深度分析见后续第14章
自旋锁SpinLock
源码深度分析见后续第8章
无锁→独占锁→读写锁→邮戳锁
有没有比读写锁更快的锁?
StampedLock
源码深度分析见后续第14章
无锁→偏向锁→轻量锁→重量锁
源码深度分析见后续第12章
其它细节
不可以String同一把锁
严禁这么做
LockSupport与线程中断
线程中断机制
从阿里蚂蚁金服面试题讲起
什么是中断?
中断的三大方法相关API
面试题:如何使用中断标识停止线程?
在需要中断的线程中不断监听中断状态,
一旦发生中断,就执行相应的中断处理业务逻辑。
修改状态
停止程序的运行
。。。。。。
中断运行中的线程方法
通过一个volatile变量实现
通过AtomicBoolean
通过Thread类自带的中断api方法实现
API
实例方法interrupt(),没有返回值
源码分析
实例方法isInterrupted,返回布尔值
源码分析
code
说明
当前线程的中断标识为true,是不是就立刻停止?
说明
code02
code02后手案例(重要,面试就是它,操蛋)
结论
小总结
中断只是一种协同机制,修改中断标识位仅此而已,不是立刻stop打断
静态方法Thread.interrupted()
静态方法Thread.interrupted()
code
说明
都会返回中断状态,两者对比
分支主题
分支主题
总结
LockSupport是什么
是什么
线程等待唤醒机制
3种让线程等待和唤醒的方法
方式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
小总结
Condtion中的线程等待和唤醒方法之前,需要先获取锁
一定要先await后signal,不要反了
Object和Condition使用的限制条件
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
是什么
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
官网解释
主要方法
API
阻塞
park() /park(Object blocker)
阻塞当前线程/阻塞传入的具体线程
唤醒
unpark(Thread thread)
唤醒处于阻塞状态的指定线程
代码
正常+无锁块要求
之前错误的先唤醒后等待,LockSupport照样支持
解释
成双成对要牢记
总结
面试题
code
Java内存模型之JMM
先从大厂面试题开始
你知道什么是Java内存模型JMM吗?
JMM与volatile它们两个之间的关系?(下一章详细讲解)
JMM有哪些特性or它的三大特性是什么?
为什么要有JMM,它为什么出现?作用和功能是什么?
happens-before先行发生原则你有了解过吗?
计算机硬件存储体系
问题?和推导出我们需要知道JMM
Java内存模型Java Memory Model
JMM规范下,三大特性
可见性
原子性
指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰
有序性
简单案例先过个眼熟
JMM规范下,多线程对变量的读写过程
读取过程
小总结
我们定义的所有共享变量都储存在物理主内存中
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
JMM规范下,多线程先行发生原则之happens-before
在JMM中,
如果一个操作执行的结果需要对另一个操作可见性
或者 代码重排序,那么这两个操作之间必须存在happens-before关系。
x 、y案例说明
先行发生原则说明
happens-before总原则
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,
而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。
如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
值日
周一张三周二李四,假如有事情调换班可以的
1+2+3 = 3+2+1
happens-before之8条
次序规则:
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;
加深说明
前一个操作的结果可以被后续的操作获取。
讲白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1。
锁定规则:
一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;
volatile变量规则:
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,
前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。
传递规则:
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
线程启动规则(Thread Start Rule):
Thread对象的start()方法先行发生于此线程的每一个动作
线程中断规则(Thread Interruption Rule):
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
可以通过Thread.interrupted()检测到是否发生中断
线程终止规则(Thread Termination Rule):
线程中的所有操作都先行发生于对此线程的终止检
测,我们可以通过Thread::join()方法是否结束、
Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
对象终结规则(Finalizer Rule):
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
说人话
对象没有完成初始化之前,是不能调用finalized()方法的
happens-before小总结
案例说明
code
解释
修复
把getter/setter方法都定义为synchronized方法
串行化
把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景
volatile与Java内存模型
被volatile修改的变量有2大特点
特点
可见性
有序性
排序要求
volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
内存屏障(面试重点必须拿下)
先说生活case
没有管控,顺序难保
设定规则,禁止乱序
上海南京路步行街武警“人墙”当红灯
是什么
volatile凭什么可以保证可见性和有序性???
内存屏障 (Memory Barriers / Fences)
JVM中提供了四类内存屏障指令
一句话
上一章讲解过happens-before先行发生原则,类似接口规范,落地?
落地靠什么?
你凭什么可以保证?你管用吗?
粗分:2种
读屏障(Load Barrier)
在读指令之前插入读屏障,让工作内存或CPU高速缓存中的缓存数据失效,重新回到主存种获取最新数据
写屏障(Store Barrier)
在写指令之后插入写屏障,强制把写缓冲区中的数据刷入主内存中
细分:4种
C++源码分析
IDEA工具里面找Unsafe.class
Unsafe.java
Unsafe.cpp
OrderAccess.hpp
orderAccess_linux_x86.inline.hpp
四大屏障分别是什么意思
orderAccess_linux_x86.inline.hpp
如下内容困难,可能会导致学生懵逼,课堂要讲解细致
分2次讲解+复习
什么叫保证有序性?
禁止重排
通过内存屏障禁重排
上述说明
happens-before 之 volatile 变量规则
JMM 就将内存屏障插⼊策略分为 4 种
写
1. 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障
2. 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障
对比图
分支主题
读
3. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
4. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障
volatile特性
保证可见性
说明
保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见
Code
不加volatile,没有可见性,程序无法停止
加了volatile,保证可见性,程序可以停止
上述代码原理解释
volatile变量的读写过程
没有原子性
volatile变量的复合操作(如i++)不具有原子性
Code
从i++的字节码角度说明
不保证原子性
读取赋值一个普通变量的情况
既然一修改就是可见,为什么还不能保证原子性?
volatile主要是对其中部分指令做了处理
结论
读取赋值一个volatile变量的情况
面试回答
JVM的字节码,i++分成三步,间隙期不同步非原子操作(i++)
volatile变量不适合参与到依赖当前值的运算
指令禁重排
说明与案例
volatile的底层实现是通过内存屏障,2次复习
volatile有关的禁止指令重排的行为
四大屏障的插入情况
在每一个volatile写操作前面插入一个StoreStore屏障
StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
在每一个volatile写操作后面插入一个StoreLoad屏障
StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
在每一个volatile读操作后面插入一个LoadLoad屏障
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
在每一个volatile读操作后面插入一个LoadStore屏障
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
Code说明
如何正确使用volatile
单一赋值可以,but含复合运算赋值不可以(i++之类)
volatile int a = 10
volatile boolean flag = false
状态标志,判断业务是否结束
开销较低的读,写锁策略
DCL双端锁的发布
问题
单线程看问题代码
由于存在指令重排序......
多线程看问题代码
解决01
加volatile修饰
面试题,反周志明老师的案例,你还有不加volatile的方法吗
解决02
采用静态内部类的方式实现
最后的小总结(八股小作文)
内存屏障是什么
内存屏障能干嘛
阻止屏障两边的指令重排序
写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
内存屏障四大指令
在每一个volatile写操作前面插入一个StoreStore屏障
在每一个volatile写操作后面插入一个StoreLoad屏障
在每一个volatile读操作后面插入一个LoadLoad屏障
在每一个volatile读操作后面插入一个LoadStore屏障
凭什么我们java写了一个volatile关键字
系统底层加入内存屏障?两者关系怎么勾搭上的?
字节码层面
关键字
volatile可见性
volatile禁重排
写指令
读指令
对比java.util.concurrent.locks.Lock来理解
一句话总结
分支主题
CAS(compare and swap)
没有CAS之前
多线程环境不使用原子类保证线程安全(基本数据类型)
多线程环境 使用原子类保证线程安全(基本数据类型)
是什么
说明
原理
硬件级别保证
CASDemo代码
2019年回答到上面即可,但2020年受到疫情影响,o(╥﹏╥)o
源码分析compareAndSet(int expect,int update)
CAS底层原理?如果知道,谈谈你对UnSafe的理解
UnSafe
我们知道i++线程不安全的,那atomicInteger.getAndIncrement()
源码分析
new AtomicInteger().getAndIncrement();
底层汇编
native修饰的方法代表是底层方法
cmpxchg
在不同的操作系统下会调用不同的cmpxchg重载函数,
阳哥本次用的是win10系统
总结
原子引用
AtomicInteger原子整型,可否有其它原子类型?
AtomicBook
AtomicOrder
AtomicReferenceDemo
自旋锁,借鉴CAS思想
是什么
自己实现一个自旋锁SpinLockDemo
CAS缺点
循环时间长开销很大。
引出来ABA问题???
ABA问题怎么产生的
版本号时间戳原子引用
AtomicStampedReference
ABADemo
下一章介绍AtomicMarkableReference
一句话
比较+版本号一起
原子操作类之18罗汉增强
是什么
atomic
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater
AtomicLong
AtomicLongArray
AtomicLongFieldUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFieldUpdater
AtomicStampedReference
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
再分类
基本类型原子类
AtomicInteger
AtomicBoolean
AtomicLong
常用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
tsleep→countDownLatch
数组类型原子类
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
Case
基本使用
要求学生自学
引用类型原子类
AtomicReference
自旋锁SpinLockDemo
AtomicStampedReference
携带版本号的引用类型原子类,可以解决ABA问题
解决修改过几次
状态戳原子引用
ABADemo
AtomicMarkableReference
原子更新带有标记位的引用类型对象
解决是否修改过
它的定义就是将状态戳简化为true|false
类似一次性筷子
状态戳(true/false)原子引用
对象的属性修改原子类
AtomicIntegerFieldUpdater
原子更新对象中int类型字段的值
AtomicLongFieldUpdater
原子更新对象中Long类型字段的值
AtomicReferenceFieldUpdater
原子更新引用类型字段的值
使用目的
以一种线程安全的方式操作非线程安全对象内的某些字段
理念
使用要求
强制要求:更新的对象属性必须使用 public volatile 修饰符。
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须
使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
Case
AtomicIntegerFieldUpdaterDemo
AtomicReferenceFieldUpdater
面试官问你:你在哪里用了volatile
AtomicReferenceFieldUpdater
原子操作增强类原理深度解析
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
阿里要命题目
点赞计数器,看看性能
常用API
入门讲解
LongAdder只能用来计算加法,且从零开始计算
LongAccumulator提供了自定义的函数操作
LongAdderAPIDemo
LongAdder高性能对比Code演示
源码、原理分析
架构
原理(LongAdder为什么这么快)
官网说明和阿里要求
LongAdder是Striped64的子类
Striped64
Striped64有几个比较重要的成员函数
最重要2个
Striped64中一些变量或者方法的定义
Cell
是 java.util.concurrent.atomic 下 Striped64 的一个内部类
LongAdder为什么这么快
一句话
数学表达
内部有一个base变量,一个Cell[]数组。
base变量:非竞态条件下,直接累加到该变量上
Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中
分支主题
源码解读深度分析
小总结
longAdder.increment()
add(1L)
条件递增,逐步解析
1.最初无竞争时只更新base;
2.如果更新base失败后,首次新建一个Cell[]数组
3.当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容
longAccumulate
longAccumulate入参说明
Striped64中一些变量或者方法的定义
步骤
线程hash值:probe
总纲
计算
刚刚要初始化Cell[]数组(首次新建)
未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组
兜底
多个线程尝试CAS修改失败的线程会走到这个分支
Cell数组不再为空且可能存在Cell数组扩容
多个线程同时命中一个cell的竞争
总体代码
1
2
3
4
5
6
上6步骤总结
sum
为啥在并发情况下sum的值不精确
使用总结
AtomicLong
线程安全,可允许一些性能损耗,要求高精度时可使用
保证精度,性能代价
AtomicLong是多个线程针对单个热点值value进行原子操作
LongAdder
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
保证性能,精度代价
LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
小总结
AtomicLong
原理
CAS+自旋
incrementAndGet
场景
低并发下的全局计算
AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。
缺陷
高并发后性能急剧下降
why?
AtomicLong的自旋会成为瓶颈
LongAdder vs AtomicLong Performance
http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/
LongAdder
原理
CAS+Base+Cell数组分散
空间换时间并分散了热点数据
场景
高并发下的全局计算
缺陷
sum求和后还有计算线程修改结果的话,最后结果不够准确
聊聊ThreadLocal
ThreadLocal简介
恶心的大厂面试题
ThreadLocal中ThreadLocalMap的数据结构和关系?
ThreadLocal的key是弱引用,这是为什么?
ThreadLocal内存泄露问题你知道吗?
ThreadLocal中最后为什么要加remove方法?
......
是什么
能干嘛
api介绍
永远的helloworld讲起
按照总销售额统计,方便集团公司做计划统计
群雄逐鹿起纷争
Code
上述需求变化了...
不参加总和计算,希望各自分灶吃饭,
各凭销售本事提成,按照出单数各自统计
比如某找房软件,每个中介销售都有自己的销售额指标,自己专属自己的,不和别人掺和
上述需求该如何处理???
人手一份天下安
Code
通过上面代码总结
因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用
既然其它 Thread 不可访问,那就不存在多线程间共享的问题。
统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
一句话
如何才能不争抢
1 加入synchronized或者Lock控制资源的访问顺序
2 人手一份,大家各自安好,没必要抢夺
从阿里ThreadLocal规范开始
非线程安全的SimpleDateFormat
官网文档
Code
bugs
源码分析结论
解决1
将SimpleDateFormat定义成局部变量。
缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。
code
解决2
ThreadLocal,也叫做线程本地变量或者线程本地存储
code
其它
加锁
第3方时间库
DateUtils
学习自学给案例,家庭作业
ThreadLocal源码分析
源码解读
Thread,ThreadLocal,ThreadLocalMap 关系
Thread和ThreadLocal
再次体会,各自线程,人手一份
ThreadLocal和ThreadLocalMap
All三者总概括
小总结
ThreadLocal内存泄露问题
从阿里面试题开始讲起
什么是内存泄漏
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
谁惹的祸?
why
强引用、软引用、弱引用、虚引用分别是什么?
再回首ThreadLocalMap
整体架构
新建一个带finalize()方法的对象MyObject
强引用(默认支持模式)
case
软引用
case
弱引用
case
软引用和弱引用的适用场景
虚引用
构造方法
引用队列
case
GCRoots和四大引用小总结
关系
为什么要用弱引用?不用如何?
为什么源代码用弱引用?
弱引用就万事大吉了吗?
埋雷
key为null的entry,原理解析
set、get方法会去检查所有键为null的Entry对象
expungeStaleEntry
set()
get()
remove()
结论
结论
最佳实践
如何定义
定义
用完记得手动remove
小总结
ThreadLocal 并不解决线程间共享数据的问题
ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,
该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法
群雄逐鹿起纷争,人各一份天下安
Java对象内存布局和对象头
先从阿里及其它大厂面试题说起
同学反馈2020.6.27
Object object = new Object()谈谈你对这句话的理解?
一般而言JDK8按照默认情况下,new一个对象占多少内存空间
位置所在
JVM里堆→新生区→伊甸园区
构成布局
头体?想想我们的HTML报文
对象在堆内存中布局
权威定义
周志明老师JVM第3版
对象在堆内存中的存储布局
对象头
对象标记Mark Word
它保存什么
默认存储对象的HashCode、分代年龄和锁标志位等信息。
这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。
它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
类元信息(又叫类型指针)
参考尚硅谷宋红康老师原图
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
对象头多大
在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。
实例数据
存放类的属性(Field)数据信息,包括父类的属性信息,
如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍。
填充数据不是必须存在的,仅仅是为了字节对齐
这部分内存按8字节补充对齐。
官网理论
Hotspot术语表官网
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
底层源码理论证明
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/89fb452b3688/src/share/vm/oops/oop.hpp
再说对象头的MarkWord
32位(看一下即可,不用学了,以64位为准)
64位重要
oop.hpp
markOop.hpp
markword(64位)分布图,
对象布局、GC回收和后面的锁升级就是
对象标记MarkWord里面标志位的变化
聊聊Object obj = new Object()
JOL证明
JOL官网
http://openjdk.java.net/projects/code-tools/jol/
POM
小试一下
代码
结果呈现说明
GC年龄采用4位bit存储,最大为15,
例如MaxTenuringThreshold参数默认值就是15
try try
-XX:MaxTenuringThreshold=16
分支主题
尾巴参数说明
命令
java -XX:+PrintCommandLineFlags -version
默认开启压缩说明
-XX:+UseCompressedClassPointers
结果
上述表示开启了类型指针的压缩,以节约空间,假如不加压缩???
手动关闭压缩再看看
-XX:-UseCompressedClassPointers
结果
换成其他对象试试
结果
Synchronized与锁升级
先从阿里及其它大厂面试题说起
谈谈你对Synchronized的理解
Synchronized的锁升级你聊聊
Synchronized的性能是不是一定弱于Lock
同学反馈2020.6.17
本章路线总纲
说明
synchronized锁:由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略
Synchronized的性能变化
java5以前,只有Synchronized,这个是操作系统级别的重量级操作
重量级锁,假如锁的竞争比较激烈的话,性能下降
Java5之前,用户态和内核态之间的切换
为什么每一个对象都可以成为一个锁????
markOop.hpp
Monitor(监视器锁)
结合之前的synchoronized和对象头说明
java6开始,优化Synchronized
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
需要有个逐步升级的过程,别一开始就捅到重量级锁
synchronized锁种类及升级步骤
多线程访问情况,3种
只有一个线程来访问,有且唯一Only One
有2个线程A、B来交替访问
竞争激烈,多个线程来访问
升级流程
synchronized用的锁是存在Java对象头里的Mark Word中
锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
64位标记图再看
无锁
Code演示
程序不会有锁的竞争
偏锁
是什么
主要作用
当一段同步代码一直被同一个线程多次访问,
由于只有一个线程那么该线程在后续访问时便会自动获得锁
比如线程卖票中的a线程A线程因为获得最早获得锁,所以在后续竞争中更容易获取锁
同一个老顾客来访,直接老规矩行方便
看看多线程卖票,同一个线程获得体会一下
小结论
64位标记图再看
通过CAS方式修改markword中的线程ID
偏向锁的持有
说明
细化案例Account对象举例说明
偏向锁JVM命令
java -XX:+PrintFlagsInitial |grep BiasedLock*
重要参数说明
Code演示
一切默认
演示无效果
因为参数系统默认开启
关闭延时参数,启用该功能
-XX:BiasedLockingStartupDelay=0
好日子终会到头......o(╥﹏╥)o
开始有第2个线程来抢夺了
偏向锁的撤销
当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。
撤销
总体步骤流程图示
分支主题
Java15逐步废弃偏向锁
轻锁
多线程竞争,但是任意时刻只有一个线程竞争,即不存在竞争太过激烈的情况,也就没有线程阻塞
主要作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短
本质就是自旋锁
64位标记图再看
轻量级锁的获取
Code演示
如果关闭偏向锁,就可以直接进入轻量级锁
-XX:-UseBiasedLocking
步骤流程图示
分支主题
自旋达到一定次数和程度
java6之前
默认启用,默认情况下自旋的次数是 10 次
-XX:PreBlockSpin=10来修改
或者自旋线程数超过cpu核数一半
上述了解即可,别用了。
Java6之后
自适应
自适应意味着自旋的次数不是固定不变的
而是根据:
同一个锁上一次自旋的时间。
拥有锁线程的状态来决定。
轻量锁与偏向锁的区别和不同
争夺轻量级锁失败时,自旋尝试抢占锁
轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
重锁
有大量的线程参与锁的竞争,冲突性很高
锁标志位
Code演示
小总结
锁升级以后hashcode去哪里了
说明
各种锁优缺点、synchronized锁升级和实现原理
JIT编译器对锁的优化
JIT
Just In Time Compiler,一般翻译为即时编译器
锁消除
LockClearUPDemo
逃逸分析
锁粗化
LockBigDemo
AbstractQueuedSynchronizer之AQS
先从阿里及其它大厂面试题说起
同学反馈2020.6.27
前置知识
公平锁和非公平锁
可重入锁
自旋锁
LockSupport
数据结构之链表
设计模式之模板设计模式
是什么
字面意思
抽象的队列同步器
源代码
技术解释
官方解释
是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,
通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量
表示持有锁的状态
AQS为什么是JUC内容中最重要的基石
和AQS有关的
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
。。。。。。
进一步理解锁和同步器的关系
锁,面向锁的使用者
定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
同步器,面向锁的实现者
比如Java并发大神DougLee,提出统一规范并简化了锁的实现,
屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。
能干嘛
加锁会导致阻塞
有阻塞就需要排队,实现排队必然需要队列
解释说明
AQS初步
AQS初识
官网解释
有阻塞就需要排队,实现排队必然需要队列
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的
FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成
一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
AQS内部体系架构
AQS自身
AQS的int变量
AQS的同步状态State成员变量
分支主题
银行办理业务的受理窗口状态
零就是没人,自由状态可以办理
大于等于1,有人占用窗口,等着去
AQS的CLH队列
CLH队列(三个大牛的名字组成),为一个双向队列
银行候客区的等待顾客
小总结
有阻塞就需要排队,实现排队必然需要队列
state变量+CLH双端队列
内部类Node(Node类在AQS类内部)
Node的int变量
Node的等待状态waitState成员变量
volatile int waitStatus
说人话
等候区其它顾客(其它线程)的等待状态
队列中每个排队的个体就是一个 Node
Node此类的讲解
内部结构
属性说明
AQS同步队列的基本结构
从我们的ReentrantLock开始解读AQS
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
ReentrantLock的原理
从最简单的lock方法开始看看公平和非公平
非公平锁走起,方法lock()
本次讲解我们走非公平锁作为案例突破口
源码解读比较困难,别着急---阳哥的全系列脑图给大家做好笔记
源码解读走起
lock()
acquire()
源码和3大流程走向
tryAcquire(arg)
本次走非公平锁
nonfairTryAcquire(acquires)
return false;
继续推进条件,走下一个方法
return true;
结束
addWaiter(Node.EXCLUSIVE)
addWaiter(Node mode)
enq(node);
双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。
真正的第一个有数据的节点,是从第二个节点开始的。
假如3号ThreadC线程进来
prev
compareAndSetTail
next
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
acquireQueued
假如再抢抢失败就会进入
shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法中
shouldParkAfterFailedAcquire
如果前驱节点的 waitStatus 是 SIGNAL状态,即 shouldParkAfterFailedAcquire 方法会返回 true 程序会继续向下执行 parkAndCheckInterrupt 方法,用于将当前线程挂起
parkAndCheckInterrupt
unlock
sync.release(1);
tryRelease(arg)
unparkSuccessor
ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
本章路线总纲
无锁→独占锁→读写锁→邮戳锁
关于锁的大厂面试题
你知道Java里面有哪些锁?
你说你用过读写锁,锁饥饿问题是什么?
有没有比读写锁更快的锁?
StampedLock知道吗?(邮戳锁/票据锁)
ReentrantReadWriteLock有锁降级机制策略你知道吗?
。。。。。。
请你简单聊聊ReentrantReadWriteLock
是什么
读写锁说明
再说说演变
『读写锁』意义和特点
特点
可重入
读写分离
无锁无序→加锁→读写锁演变复习
code演示ReentrantReadWriteLockDemo
从写锁→读锁,ReentrantReadWriteLock可以降级
《Java 并发编程的艺术》中关于锁降级的说明:
说人话
锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)
读写锁降级演示
可以降级
锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
code演示LockDownGradingDemo
结论
如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略
不可锁升级
写锁和读锁是互斥的
读写锁之读写规矩,再说降级
Oracle公司ReentrantWriteReadLock源码总结
面试题:有没有比读写锁更快的锁?
邮戳锁StampedLock
无锁→独占锁→读写锁→邮戳锁
是什么
StampedLock是JDK1.8中新增的一个读写锁,
也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。
邮戳锁
也叫票据锁
stamp(戳记,long类型)
代表了锁的状态。当stamp返回零时,表示线程获取锁失败。
并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
它是由锁饥饿问题引出
锁饥饿问题
如何缓解锁饥饿问题?
使用“公平”策略可以一定程度上缓解这个问题
new ReentrantReadWriteLock(true);
但是“公平”策略是以牺牲系统吞吐量为代价的
StampedLock类的乐观读锁闪亮登场
Why闪亮
StampedLock的特点
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,
支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
乐观读模式code演示
读的过程中也允许获取写锁介入
StampedLock的缺点
StampedLock 不支持重入,没有Re开头
StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法
如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly()
课程总结与回顾
终章の回顾
CompletableFuture
“锁”事儿
悲观锁
乐观锁
自旋锁
可重入锁(递归锁)
写锁(独占锁)/读锁(共享锁)
公平锁/非公平锁
死锁
偏向锁
轻量锁
重量锁
邮戳(票据)锁
JMM
synchronized及升级优化
锁的到底是什么
无锁→偏向锁→轻量锁→重量锁
Java对象内存布局和对象头
64位图
CAS
CAS的底层原理
CAS问题
ABA问题
volatile
特性
内存屏障
LockSupport
是什么
LockSupport.park和Object.wait区别
AbstractQueuedSynchronizer
是什么
出队入队Node
ThreadLocal
原子增强类
0 条评论
下一页