大厂学苑之JUC并发编程与源码分析(上)
JUC并发编程与源码分析
1. 本课程前置要求说明
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开发工程师的要求明细
- 技术栈
- 阿里手册规范
大厂面试题
1、闲聊,自我介绍,负责业务,技术栈
【JUC】
2、synchronized作用、底层实现
3、说下什么是偏向锁,什么时候触发锁升级
4、jmm内存模型,说一下理解、这种内存模型会导致什么问题
5、把你知道的所有可以实现主内存到工作内存可见性的方法说出来
6、如何理解指令重排序
7、juc中lock的底层实现及公平锁和非公平锁,非公平锁是如何竞争的(aqs原理)
8、new一个对象,存在哪里,说在堆里,面试官说不全对,问了解过内存逃逸吗?
9、jvm的类加载,为什么有双亲委派机制,比如我本地写一个自己定义的String和java中后在main方法new String的时候
是new的哪个对象,说的是java自己的,问为什么?
【redis】
10、redis 的缓存淘汰策略
11、redis的多路io复用如何理解,为什么单线程还可以抗那么高的qps
【网络】
12、https底层是如何实现安全的?
13、说一下你知道的黑客攻击手段,知道SYN攻击吗,说下原理?
14、tcp连接为什么是可靠的,说下原因?
【mysql】
见我网络部分答得不太好,不再问了
15、mysql使用索引有哪些注意点,说下你知道的所有不适合建索引的情况
16、举个例子,我有个update语句,说下你理解,mvsal执行update的时候,是如何做的
2. 线程基础知识复习
2.1 先拜拜大神

2.2 JUC四大口诀
高内聚低耦合前提下,封装思想,线程操作资源类
判断、干活、通知
防止虚假唤醒,wait方法要注意使用while判断
注意标志位flag,可能是volatile的
2.3 为什么多线程极其重要???
硬件方面
- 摩尔定律失效
摩尔定律:
它是由英特尔创始人之一Gordon Moore(戈登·摩尔)提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。
可是从2003年开始CPU主频已经不再翻倍(可以理解为 CPU上的晶体管已经满了 插不进去了),而是采用多核而不是更快的主频。、
摩尔定律失效。 在主频不再提高且核数在不断增加的情况下,要想让程序更快就要用到并行或并发编程。
软件方面
- 高并发系统,异步+回调等生产需求
2.4 从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

2.5 Java多线程相关概念
进程
是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源
线程
在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程
⼀个进程会有1个或多个线程的
面试题:何为进程和线程?
管程
- Monitor(监视器),也就是我们平时所说的锁
- Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
- JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,
1 | Object o = new Object(); |
Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。
JVM第3版

2.6 用户线程和守护线程
- Java线程分为用户线程和守护线程,线程的daemon属性为true表示是守护线程,false表示是用户线程
守护线程
- 是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
用户线程
- 是系统的工作线程,它会完成这个程序需要完成的业务操作
code
1 | package com.atguigu.itdachang; |
重点
- 当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出
- 如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出
- 设置守护线程,需要在start()方法之前进行
3. CompletableFuture
3.1 Future和Callable接口
Future接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
Callable接口中定义了需要有返回的任务需要实现的方法。
- 比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会才去获取子任务的执行结果。
3.2 从之前的FutureTask说开去
本源的Future接口相关架构

案例1 get()阻塞
- 一旦调用get()方法,不管是否计算完成都会导致阻塞,o(╥﹏╥)o
1 | package com.zzyy.study.test; |
案例2 isDone()轮询
- 轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果.
- 如果想要异步获取结果,通常都会以轮询的方式去获取结果尽量不要阻塞
1 | package com.zzyy.study.test; |
小总结
- 不见不散
- 过时不候
- 轮询
想完成一些复杂的任务
- 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
- 将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果。
- 当Future集合中某个任务最快结束时,返回结果。
- 等待Future结合中的所有任务都完成。
3.3 对Future的改进
CompletableFuture和CompletionStage源码分别介绍
类架构说明

public class CompletableFuture
implements Future , CompletionStage {
- 新技术的出现一定能够替代老技术,新技术不但能干老技术能干的所有活,还能干老技术不能干的活
- 实现了Future 说明能取代FutureTask的功能,额外实现了 CompletionStage,说明拓展了额外功能
接口CompletionStage
- 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。
- 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。比如: stage.thenApply(x -> square(x).thenAccept(x-> System.out.print(x)).thenRun(() ->system.out.printIn())
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
类CompletableFuture
- 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。
- 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage ),它支持在计算完成以后触发一些函数或执行某些动作。
- 它实现了Future和CompletionStaqe接口
核心的四个静态方法,来创建一个异步操作
runAsync 无 返回值
1 | public static CompletableFuture<Void> runAsync(Runnable runnable) |
supplyAsync 有 返回值
1 | public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) |
上述Executor executor参数说明
没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool() 作为它的线程池执行异步代码。
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
Code
- 无 返回值
1 | package com.zzyy.study.test; |
- 有 返回值
1 | package com.zzyy.study.test; |
Code之通用演示,减少阻塞和轮询
- 从Java8开始引入了CompletableFuture,它是Future的功能增强版,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
1 | package com.atguigu.juc.senior.inner.completablefuture; |
解释下为什么默认线程池关闭,自定义线程池记得关闭
CompletableFuture的优点
- 异步任务结束时,会自动回调某个对象的方法;
- 异步任务出错时,会自动回调某个对象的方法;
- 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
3.4 案例精讲-从电商网站的比价需求说开去
大厂面试题看看
1,你怎么理解java多线程的?怎么处理并发?线程池有那几个核心参数?
2,Java加锁有哪几种锁?我先说了syn,syn刚讲到偏向锁,他就不让我讲了,太自信了
3,简单说说lock ?
4,hashmap的实现原理? hash冲突怎么解决?为什么使用红黑树?
5,spring里面都使用了那些设计模式?循环依赖怎么解决?
6,项目中那个地方用了countdownlanch,怎么使用的?
7,JVM项目了解过吗?说说都有什么﹖栈里面都放什么东西?
8,都用redis来做什么? aof和rdb都什么做持久化缓存的?
9,myaql的锁机制? mysql的索引是怎么实现的?
10,spring实现事务的几种方式?
11,zookeeper怎么实现分布式锁?
12,java8函数式编程用过吗?
13,算法:求链表倒数第K个元素?
先说说join和get对比
二者用法一样 只是JOIN不会抛出异常,更优雅一点
1 | public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException |
Code
1 | package com.zzyy.study.day628; |
3.5 CompletableFuture常用方法
获得结果和触发计算
获取结果
public T get() 不见不散
public T get(long timeout, TimeUnit unit) 过时不候
public T getNow(T valueIfAbsent) 没有计算完成的情况下,给我一个替代结果
立即获取结果不阻塞
计算完,返回计算完成后的结果
没算完,返回设定的valueIfAbsent值
1 |
|
- public T join()
1 |
|
主动触发计算
- public boolean complete(T value)
- 是否打断get方法立即返回括号值
1 |
|
对计算结果进行处理
thenApply
- 计算结果存在依赖关系,这两个线程串行化
- 由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
1 | package com.zzyy.study.test; |
handle
- 有异常也可以往下一步走,根据带的异常参数可以进一步处理
1 | package com.zzyy.study.test; |
总结

whenComplete和whenCompleteAsync的区别:
- whenComplete:是执行当前任务的线程执行继续执行whenComplete的任务。
- whenCompleteAsync:是执行把 whenCompleteAsync这个任务继续提交给线程池来进行执行。
对计算结果进行消费
接收任务的处理结果,并消费处理,无返回结果
thenAccept
1 | public static void main(String[] args) throws ExecutionException, InterruptedException |
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 有返回值
1 | System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join()); |
对计算速度进行选用
- 谁快听谁的,最后返回快的那一个结果
applyToEither
1 | package com.zzyy.study.test; |
对计算结果进行合并
- 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine 来处理
- 先完成的先等着,等待其它分支任务
thenCombine
- code标准版,好理解先拆分
1 | package com.zzyy.study.test; |
- code表达式
1 | package com.zzyy.study.test; |
4. 说说Java“锁”事
4.1 大厂面试题复盘
Synchronized相关问题
- Synchronized用过吗,其原理是什么?
- 你刚才提到获取对象的锁。这个“锁”到底是什么?如何确定对象的锁?
- 什么是可重入性.为什么说Synchronized是可重入锁?
- JMM对Java的原生锁做了哪些优化?
- 为什么说Synchronized是非公平锁?
- 什么是锁消除和锁粗化?
- 为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS.
- 乐观锁一定就是好的吗?
可重入锁ReentrantLock及其他显式锁相关问题
- 跟Synchronized相比,可重入锁ReentrantLock其实现原理有什么不同?
- 那么请谈谈AQS框架是怎么回事儿?
- 请尽可能详尽地对比下Synchronized和ReentrantLock的异同。
- ReentrantL ock是如何实现可重入性的?
4.2 从轻松的乐观锁和悲观锁开讲
悲观锁
- 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
- synchronized关键字和Lock的实现类都是悲观锁
- 适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 显式的锁定之后再操作同步资源
伪代码说明
1 |
|
乐观锁
- 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
- 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
- 乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢
- 乐观锁一般有两种实现方式:
- 采用版本号机制
- CAS(Compare-and-Swap,即比较并替换)算法实现
4.3 通过8种情况演示锁运行案例,看看我们到底锁的是什么
锁相关的8种案例演示
1 | package com.atguigu.juc.locks; |
synchronized有三种应用方式
- 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
- 作用于代码块,对括号里配置的对象加锁。
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
JDK源码(notify方法)说明举例

从字节码角度分析synchronized实现
- javap -c ***.class文件反编译
-c 对代码进行反汇编
-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
synchronized同步代码块
- javap -c ***.class文件反编译
1 | public class Lock_SyncDemo { |
- 反编译之后

- 实现使用的是monitorenter和monitorexit指令
一定是一个enter两个exit吗?
m1方法里面自己添加一个异常试试

synchronized普通同步方法
- 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor

synchronized静态同步方法
- ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

反编译synchronized锁的是什么
[Java集合类]
- 1、从集合开始吧,介绍一下常用的集合类,哪些是有序的,哪些是无序的
- 2、hashmap是如何寻址的,哈希碰撞后是如何存储数据的,1.8后什么时候变成红黑树、说下红黑树的原
理,红黑树有什么好处 - 3、 concurrrenthashmap 怎么实现线程安全,一一个里面会有几个段segment, jdk1. 8后有优化
concurrenthashmap吗? 分段锁有什么坏处.
[多线程JUC]
- 1、 reentrantlock 实现原理,简单说下aqs
- 2、 synchronized实现原理,monitor对象什么时候生成的?知道monitor的monitorenter和monitorexit
这两个是怎么保证同步的吗,或者说,这两个操作计算机底层是如何执行的. - 3、 刚刚你提到了synchronized的优化过程,详细说一下吧。 偏向锁和轻量级锁有什么区别?
- 4、线程池几个参数说下,你们项目中如何根据实际场景设置参数的,为什么cpu密集设置的线程数比io密
集型少
管程
- 管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
- 这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
- 在HotSpot虚拟机中,monitor采用ObjectMonitor实现
上述C++源码解读
- ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp
- objectMonitor.hpp
ObjectMonitor中有几个关键属性
| 属性名 | 意义 |
|---|---|
| _owner | 指向持有ObjectMonitor对象的线程 |
| _WaitSet | 存放处于wait状态的线程队列 |
| _EntryList | 存放处于等待锁block状态的线程队列 |
| _recursions | 锁的重入次数 |
| _count | 用来记录该线程获取锁的次数 |
每个对象天生都带着一个对象监视器
对于synchronized关键字,我们在《Synchronized与锁升级》章节还会再深度讲解
synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位,后续讲解锁升级时候我们再加深,目前为了承前启后的学习,对下图先混个眼熟即可,O(∩_∩)O

4.4 公平锁和非公平锁
ReentrantLock卖票示例
- 从ReentrantLock卖票编码演示公平和非公平现象
1 | package com.atguigu.juc.senior.test; |
何为公平锁/非公平锁?
生活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为不公平
- 按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获取锁;
- 先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以

面试题
为什么会有公平锁/非公平锁的设计为什么默认非公平?
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
使用公平锁会有什么问题
- 公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”
什么时候用公平?什么时候用非公平?
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
4.5 可重入锁(又名递归锁)
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”这四个字分开来解释:
- 可:可以。
- 重:再次。
- 入:进入。
- 锁:同步锁。
进入什么
- 进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话
- 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
- 自己可以获取自己的内部锁
可重入锁种类
1. 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
同步块
1 | package com.atguigu.juc.senior.prepare; |
同步方法
1 | package com.atguigu.juc.senior.prepare; |
2. Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
显式锁(即Lock)也有ReentrantLock这样的可重入锁。
1 | package com.atguigu.juc.senior.prepare; |
4.6 死锁及排查
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

产生死锁主要原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
请写一个死锁代码case
1 | package com.atguigu.juc.senior.prepare; |
如何排查死锁
1 | jps -l |
4.7 写锁(独占锁)/读锁(共享锁)
- 源码深度分析见后续第14章
4.8 自旋锁SpinLock
- 源码深度分析见后续第8章
4.9 无锁→独占锁→读写锁→邮戳锁
- 有没有比读写锁更快的锁?
StampedLock
源码深度分析见后续第14章
4.10 无锁→偏向锁→轻量锁→重量锁
- 源码深度分析见后续第12章
4.11 其它细节
- 不可以String同一把锁,严禁这么做(String 是不可变 字符串池中只有一份,同一把锁)
1 | String lockA = "aa"; |
5. LockSupport与线程中断
5.1 线程中断机制
从阿里蚂蚁金服面试题讲起
- 如何停止、中断一个运行中的线程??

什么是中断?
首先
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
其次
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的机制——中断。
中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
中断的相关API方法
| API | 描述 |
|---|---|
| public void interrupt() 实例方法 | 实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程 |
| public static boolean interrupted() | 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态 这个方法做了两件事: 1. 返回当前线程的中断状态 2. 将当前线程的中断状态设为false 这个方法有点不好理解,因为连续调用两次的结果可能不一样。 |
| public boolean isInterrupted() 实例方法, | 判断当前线程是否被中断(通过检查中断标志位) |
面试题:如何使用中断标识停止线程?
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑。
- 修改状态
- 停止程序的运行
方法
1. 通过一个volatile变量实现
1 | package com.atguigu.juc.senior.test; |
2. 通过AtomicBoolean
1 | package com.zzyy.study.test; |
3. 通过Thread类自带的中断api方法实现
- 实例方法interrupt(),没有返回值
1 | public void interrupt() 实例方法, |
- 如果线程处于wait join sleep 状态 执行中断直接抛出 InterruptedException异常


- 实例方法isInterrupted,返回布尔值

1 | // public boolean isInterrupted() 实例方法, |

4. Code
1 | package com.atguigu.itdachang; |
当前线程的中断标识为true,是不是就立刻停止?
具体来说,当对一个线程,调用 interrupt() 时:
① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。
被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
- Code02
1 | package com.atguigu.juc.senior.test; |
- code02后手案例(重要,面试就是它,操蛋)

sleep方法抛出InterruptedException后, 中断标识也被清空置为false,我们在catch没有通过调用th.interrupt() 方法再次将中断标识置为true,这就导致无限循环了
- 小总结
中断只是一种协同机制,修改中断标识位仅此而已,不是立刻stop打断
总结
线程中断相关的方法:
- interrupt()方法是一个实例方法
它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了。
- isInterrupted()方法也是一个实例方法
它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
- Thread类的静态方法interrupted()
返回当前线程的中断状态(boolean类型)且将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false
静态方法Thread.interrupted()
1 | package com.atguigu.juc.senior.test; |

public static boolean interrupted() 静态方法,Thread.interrupted();
判断线程是否被中断,并清除当前中断状态,类似i++
这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false这个方法有点不好理解,因为连续调用两次的结果可能不一样。
- 都会返回中断状态,两者对比


方法的注释也清晰的表达了“中断状态将会根据传入的ClearInterrupted参数值确定是否重置”。
所以,静态方法interrupted将会清除中断状态(传入的参数ClearInterrupted为true),实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)。
大厂面试加餐
字节跳动面试
1、数据结构
2、梳理业务及落地(仔细)
3、简历中写到的技术点,要回顾
一面:考察基础知识;介绍做的业务,考察业务中用到的技术深度;一道算法题,看代码风格,能做出来算加分项(刷一周的算法可以使思维活跃)
二面:二面面试官会跟一面面试官协商,考察一面表现薄弱的部分(如果基础好算法弱,二面会考算法;如果算法好基础弱,二面着重考基础)给道设计题
三面:业务负责人直接面,考察综合能力,通过率较高
上海抖音
1.Jvm堆和栈的区别
2.Jvm年轻代垃圾回收算法
3.Mysql聚簇索引和非聚簇索引的区别
4.Redis key过期的实现
5.Mq如何保证消费有且只有一次
6.网站打不开了怎么排查问题
7.算法题是链表操作
上海教育中台
1.为啥去年没接offer
2.项目中数据库选型,mysql聚集和非聚集索引
3.如何看有没有用上索引,如果where里写了条件但还是没用上索引可能是什么场景?
4.介绍下NIO,操作系统底层是什么,详解select、poll、epoll区别
5.java线程池参数每个的作用
6.介绍下你做的几个项目,最有技术含量的详细说下(其实都没有技术含量)
7.系统设计题:10亿视频、平均50弹幕(有热点)、20亿用户,做到:视频弹幕加载、视频任意位置拖动后加载、用户发的所有弹幕浏览。读多写少
【杭州IES互娱】
1.hashmap的原理,
2.resize的系列,0.75这个值怎么来的。
3.redis的数据结构,zset的实现,为什么用的跳表不是红黑树。
4.http请求的整个链路
算法题:链表重排序,基数位元素升序,偶数位降序。
【北京IES互娱】
1.如何实现并发调用多个RPC接口?
2.IO多路复用? select/poll/epoll的区别?
3.netty的NIO实现机制
4.redis
5.jvm垃圾回收算法
算法题:二叉树按层遍历,偶数层倒序
【北京商业化广告】
1.mysql主从复制,原理
2.binglog 日志格式
3.redis事务,分布式锁,线程模型,持久化
4.hashmap优化,concurrenthashmap.
5.volatile
6.单例模式
7.mutex和 piplinee
8.二分查找
【上海教育中台】
1.正则:写一个手机号的正则表达式,13开头,11位数
2.算法题:单链表,每N个一组进行翻转e
3.系统设计:微博点赞场景,能查用户点赞记录、不能重复点赞、能查当天热榜Top10
4.MQ的高吞吐量咋做到的
5.TCP几次握手、几次挥手、TIME_WAIT在什么场景出现
6.讲下DNS劫持
7.未来职业规划
【上海广告创意中心】
1.算法:大数减法
2.分布式事务
3.AOP的实现原理,怎样实现动态代理
4.最有挑战性的事是什么
5.有什么问题要问?
5.2 LockSupport是什么

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
下面这句话,后面详细说
LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程(类比 wait notify)

5.3 线程等待唤醒机制
3种让线程等待和唤醒的方法
- 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
- 方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
- 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
代码
- 正常
1 | package com.atguigu.juc.prepare; |
- 异常1
1 | package com.atguigu.juc.prepare; |

- 异常2
将notify放在wait方法前面
程序无法执行,无法唤醒
1 |
|
小总结
- wait和notify方法必须要在同步块或者方法里面,且成对出现使用
- 先wait后notify才OK
Condition接口中的await后signal方法实现线程的等待和唤醒
代码
- 正常
1 | package com.atguigu.juc.prepare; |
- 异常1
去掉lock/unlock
1 | package com.atguigu.juc.prepare; |

- 异常2
先signal后await
1 | package com.atguigu.juc.prepare; |

小总结
- Condtion中的线程等待和唤醒方法之前,需要先获取锁
- 一定要先await后signal,不要反了
Object和Condition使用的限制条件
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
官网解释

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),
permit只有两个值1和零,默认是零。
可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
主要方法

- 阻塞
调用LockSupport.park()时
park() /park(Object blocker)
1 | public static void park() { |
permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为零并返回。
- 唤醒
LockSupport.unpark(thread);
1 | public static void unpark(Thread thread) { |
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
代码
- 正常+无锁块要求
1 | package com.atguigu.juc.prepare; |
- 之前错误的先唤醒后等待,LockSupport照样支持
1 | package com.zzyy.study.test; |
解释

6. Java内存模型之JMM
6.1 先从大厂面试题开始
- 你知道什么是Java内存模型JMM吗?
- JMM与volatile它们两个之间的关系?(下一章详细讲解)
- JMM有哪些特性or它的三大特性是什么?
- 为什么要有JMM,它为什么出现?作用和功能是什么?
- happens-before先行发生原则你有了解过吗?
6.2 计算机硬件存储体系
计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。
一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算

问题?和推导出我们需要知道JMM
因为有这么多级的缓存(cpu和物理主内存的速度不一致的),CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题

Java虚拟机规范中试图定义一种Java内存模型(java Memory Model,简称JMM) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。推导出我们需要知道JMM
6.3 Java内存模型Java Memory Model
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
原则:
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛?
- 通过JMM来实现线程和主内存之间的抽象关系。
- 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM规范下,三大特性
可见性
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。

Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现”脏读”,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

线程脏读:如果没有可见性保证
- 主内存中有变量 x,初始值为 0
- 线程 A 要将 x 加 1,先将 x=0 拷贝到自己的私有内存中,然后更新 x 的值
- 线程 A 将更新后的 x 值回刷到主内存的时间是不固定的
- 刚好在线程 A 没有回刷 x 到主内存时,线程 B 同样从主内存中读取 x,此时为 0,和线程 A 一样的操作,最后期盼的 x=2 就会变成 x=1

原子性
指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰
有序性
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。
指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生”脏读”,简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
- 简单案例先过个眼熟
1 |
|
JMM规范下,多线程对变量的读写过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

JMM定义了线程和主内存之间的抽象关系
- 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
- 每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
小总结
- 我们定义的所有共享变量都储存在物理主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
JMM规范下,多线程先行发生原则之happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者 代码重排序,那么这两个操作之间必须存在happens-before关系。
x、y案例说明
| x = 5 | 线程A执行 |
|---|---|
| y = x | 线程B执行 |
| 上述称之为:写后读 |
问题?
y是否等于5呢?
如果线程A的操作(x= 5)happens-before(先行发生)线程B的操作(y = x),那么可以确定线程B执行后y = 5 一定成立;
如果他们不存在happens-before原则,那么y = 5 不一定成立。
这就是happens-before原则的威力。——————-》包含可见性和有序性的约束
先行发生原则说明
如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,
但是我们在编写Java并发代码的时候并没有察觉到这一点。
我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下
有一个“先行发生”(Happens-Before)的原则限制和规矩
这个原则非常重要:
它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。
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操作;
1 |
|
- 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()方法的
案例说明
1 | private int value = 0; |
假设存在线程A和B,线程A先(时间上的先后)调用了setValue(1),然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是什么?
假设存在线程A和B,线程A先(时间上的先后)调用了setValue(1),然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是什么?
我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8 可以忽略,因为他们和这段代码毫无关系):
- 1 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足程序次序规则;
- 2 两个方法都没有使用锁,所以不满足锁定规则;
- 3 变量不是用volatile修饰的,所以volatile变量规则不满足;
- 4 传递规则肯定不满足;
所以我们无法通过happens-before原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,
但就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?
- 把getter/setter方法都定义为synchronized方法
- 把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景
7. volatile与Java内存模型
7.1 被volatile修改的变量有2大特点
可见性
有序性
volatile的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
7.2 内存屏障(面试重点必须拿下)
内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。
内存屏障之前的所有写操作都要回写到主内存
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。
一句话:对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。
volatile凭什么可以保证可见性和有序性???
- 内存屏障 (Memory Barriers / Fences)
JVM中提供了四类内存屏障指令
上一章讲解过happens-before先行发生原则,类似接口规范,落地?
落地靠什么?
C++源码分析
IDEA工具里面找Unsafe.java
- Unsafe.java

- Unsafe.cpp

- OrderAccess.hpp

- orderAccess_linux_x86.inline.hpp

四大屏障分别是什么意思
| 屏障类型 | 屏障类型 | 说明 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
| StoreStore | Store1; StoreStore; Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存 |
| LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束 |
| StoreLoad | Store1; StoreLoad; Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |

happens-before 之 volatile 变量规则
| 第一个操作 | 第二个操作:普通读写 | 第二个操作: volatile读 | 第二个操作: volatile写 |
|---|---|---|---|
| 普通读写 | 可以重排 | 可以重排 | 不可以重排 |
| volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
| volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
- 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
- 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
- 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
JMM 就将内存屏障插⼊策略分为 4 种
写
- 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障
- 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障

读
在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

7.3 volatile特性
保证可见性
- 保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见
- 不加volatile,没有可见性,程序无法停止
- 加了volatile,保证可见性,程序可以停止
1 | package com.zzyy.study.juc; |
线程t1中为何看不到被主线程main修改为false的flag的值?
问题可能:
- 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
- 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。
我们的诉求:
1.线程中修改了工作内存中的副本之后,立即将其刷新到主内存;
2.工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
解决:
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
volatile变量的读写过程
Java内存模型中定义的8种工作内存与主内存之间的原子操作
read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

- read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
- load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
- use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
- assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
- store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
- write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
- lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
- unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
没有原子性
- volatile变量的复合操作(如i++)不具有原子性
1 | package com.zzyy.study.juc; |
从i++的字节码角度说明

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
1 | public void add() |
如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,执行相同值的加1操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全.

多线程环境下,”数据计算”和”数据赋值”操作可能多次出现,即操作非原子。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致
对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。
由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步
读取赋值一个普通变量的情况
当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套操作

既然一修改就是可见,为什么还不能保证原子性?
- volatile主要是对其中部分指令做了处理
要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。
写操作是把assign和store做了关联(在assign(赋值)后必需store(存储))。store(存储)后write(写入)。也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。
就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。注意蓝色框框的间隙。。。。。。o(╥﹏╥)o

结论
- 读取赋值一个volatile变量的情况

read-load-use 和 assign-store-write 成为了两个不可分割的原子操作,但是在use和assign之间依然有极小的一段真空期,有可能变量会被其他线程读取,导致写丢失一次…o(╥﹏╥)o
但是无论在哪一个时间点主内存的变量和任一工作内存的变量的值都是相等的。这个特性就导致了volatile变量不适合参与到依赖当前值的运算,如i = i + 1; i++;之类的那么依靠可见性的特点volatile可以用在哪些地方呢? 通常volatile用做保存某个状态的boolean值or int值。
《深入理解Java虚拟机》提到:

面试回答
- JVM的字节码,i++分成三步,间隙期不同步非原子操作(i++)

指令禁重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
- 不存在数据依赖关系,可以重排序;
- 存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
重排序的分类和执行流程

- 编译器优化的重排序: 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
- 指令级并行的重排序: 处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
- 数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。
案例 :
不存在数据依赖关系,可以重排序===> 重排序OK
| 重排前 | 重排后 |
|---|---|
| int a = 1; //1 int b = 20; //2 int c = a + b; //3 |
int b = 20; //1 int a = 1; //2 nt c = a + b; //3 |
| 结论:编译器调整了语句的顺序,但是不影响程序的最终结果。 | 重排序OK |
- 存在数据依赖关系,禁止重排序===> 重排序发生,会导致程序运行结果不同。
编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行,但不同处理器和不同线程之间的数据性不会被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。
| 名称 | 代码示例 | 说明 |
|---|---|---|
| 写后读 | a= 1; b=a; |
写一个变量之后,再读这个位置 |
| 写后写 | a=1; a = 2; |
写一个变量之后,再写这个变量 |
| 读后写 | a = b; b= 1; |
读一个变量之后,再写这个变量 |
volatile的底层实现是通过内存屏障,2次复习
volatile有关的禁止指令重排的行为
| 第一个操作 | 第二个操作:普通读写 | 第二个操作: volatile读 | 第二个操作: volatile写 |
|---|---|---|---|
| 普通读写 | 可以重排 | 可以重排 | 不可以重排 |
| volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
| volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
- 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
- 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
- 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
四大屏障的插入情况
- 在每一个volatile写操作前面插入一个StoreStore屏障
- StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
- 在每一个volatile写操作后面插入一个StoreLoad屏障
- StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
- 在每一个volatile读操作后面插入一个LoadLoad屏障
- LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
- 在每一个volatile读操作后面插入一个LoadStore屏障
- LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
1 | //模拟一个单线程,什么顺序读?什么顺序写? |

7.4 如何正确使用volatile
单一赋值可以,but含复合运算赋值不可以(i++之类)
- volatile int a = 10
- volatile boolean flag = false
状态标志,判断业务是否结束
```java
package com.atguigu.juc.prepare;import java.util.concurrent.TimeUnit;
/**
@auther zzyy
@create 2020-04-14 18:11
使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
例子:判断业务是否结束
*/
public class UseVolatileDemo
{
private volatile static boolean flag = true;public static void main(String[] args)
{
new Thread(() -> {
while(flag) {
//do something……
}
},”t1”).start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
flag = false;
},”t2”).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 开销较低的读,写锁策略
- ```java
public class UseVolatileDemo
{
/**
* 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
* 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
*/
public class Counter
{
private volatile int value;
public int getValue()
{
return value; //利用volatile保证读取操作的可见性
}
public synchronized int increment()
{
return value++; //利用synchronized保证复合操作的原子性
}
}
}
DCL双端锁的发布
DCL双端锁的发布
问题代码
1 | package com.atguigu.itdachang; |
- 单线(或者说正常情况下),在”问题代码处”,会执行如下操作,保证能获取到已完成初始化的实例程环境下

解决 -加volatile修饰
1 | package com.atguigu.itdachang; |
- 面试题,反周志明老师的案例,你还有不加volatile的方法吗
解决02 采用静态内部类的方式实现
1 | //现在比较好的做法就是采用静态内部内的方式实现 |
7.5 最后的小总结
内存屏障是什么
内存屏障:是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也叫内存栅栏或栅栏指令
内存屏障能干嘛
- 阻止屏障两边的指令重排序
- 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
- 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
内存屏障四大指令
- 在每一个volatile写操作前面插入一个StoreStore屏障
- 在每一个volatile写操作后面插入一个StoreLoad屏障
- 在每一个volatile读操作后面插入一个LoadLoad屏障
- 在每一个volatile读操作后面插入一个LoadStore屏障
volatile关键字和内存屏障系怎么勾搭上的
- 凭什么我们java写了一个volatile关键字系统底层加入内存屏障?两者关系怎么勾搭上的?
字节码层面

关键字
- 它影响的是Class内的Field 的flags :添加了一个ACC_VOLATILE
- JVM在把字节码生成为机器码的时候,发现操作是 volatile 的变量的话,就会根据JMM要求,在相应的位置去插入内存屏障指令
volatile可见性
volatile关键字保证可见性,意味着∶
- 1:对一个volatile修饰的变量进行读操作的话,总是能够读到这个变量的最新的值,也就是这个变量最后被修改的值
- 2:一个线程修改了volatile 修饰的变量的值的时候,那么这个变量的新的值,会立即刷新回到主内存中
- 3:一个线程去读取volatile 修饰的变量的值的时候,该变量在工作内存中的数据无效,需要重新到主内存去读取最新的数据
volatile禁重排
写指令

读指令

对比java.util.concurrent.locks.Lock来理解
cpu执行机器码指令的时候,是使用lock前缀指令来实现volatile的功能的。
Lock指令,相当于内存屏障,功能也类似内存屏障的功能︰
1)首先对总线/缓存加锁,然后去执行后面的指令,最后,释放锁,同时把高速缓存的数据刷新回到主内存
2)在lock锁住总线/缓存的时候,其它cpu的读写请求就会被阻塞,直到锁释放。Lock过后的写操作,会让其它cpu的高速缓存中相应的数据失效,这样后续这些cpu在读取数据的时候,就会从主内存去加载最新的数据
加了Lock指令过后的具体表现,就跟JMM添加内存屏障后一样。
一句话总结
- volatile 写之前的操作,都禁止重排序到volatile之后
- volatile 读之后的操作,都禁止重排序到volatile之前
- volatile 写之后volatile读,禁止重排序的







