最后
很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。
我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。
不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执⾏.
例子:
1 public class Demo29 {
2 int a=0;
3 boolean flag=false;
4 public void writer(){
5 a=1; //1
6 flag=true; //2
7 }
8 public void reader(){
9 if(flag){ //3
10 int i=a * a; //4
11 }
12 }
13 }
假如线程B在进行操作4时,能否看到线程A在操作1对共享变量a的写入呢? 不一定
| 时刻 | 线程A | 线程B |
| T1 | flag=true | |
| T2 | | if(flag) |
| T3 | | int i=a*a |
| T4 | a=1 | |
当线程A在执行writer方法时,因为指令重排序,会先执行flag=true,再执行a=1.而线程B在执行操作4时就会读不到线程A对共享变量a的写入,导致运行结果超出预期.
解决方案1:
通过加锁的方式来解决
1 public class Demo29 {
2 int a=0;
3 boolean flag=false;
4 public synchronized void writer(){
5 a=1; //1
6 flag=true; //2
7 }
8 public synchronized void reader(){
9 if(flag){ //3
10 int i=a * a; //4
11 }
12 }
13 }
锁的内存语义:
-
线程A释放⼀个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A 对共享变量所做修改的)消息。
-
线程B获取⼀个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共 享变量所做修改的)消息。
-
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发 送消息。
-
volatile原理:被volatile关键字修饰的变量,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
-
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
-
volatile在Java并发编程中常用于保持内存可见性和防止指令重排序。内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态;防止指令重排:在基于偏序关系的Happens-Before内存模型中,指令重排技术大大提高了程序执行效率,但同时也引入了一些问题。
-
可见性:volatile保持内存可见性的特殊规则:read、load、use动作必须连续出现;assign、store、write动作必须连续出现;每次读取前必须先从主内存刷新最新的值;每次写入后必须立即同步回主内存当中。也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。在线程1中对变量v的最新修改,对线程2是可见的。
-
内存屏障:volatile防止指令重排的策略:在每个volatile写操作的前面插入一个StoreStore屏障;在每个volatile写操作的后面插入一个StoreLoad屏障;在每个volatile读操作的后面插入一个LoadLoad屏障;在每个volatile读操作的后面插入一个LoadStore屏障。
-
volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
volatile内存语义
-
线程A写⼀个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程 发出了(其对共享变量所做修改的)消息。
-
线程B读⼀个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。
-
线程A写⼀个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过 主内存向线程B发送消息。
volatile内存语义的实现
| 是否能重排序 | 第二个操作 |
| — | — |
| 第一个操作 | 普通读/写 |
| 普通读/写 | Y |
| volatile读 | N |
| volatile写 | Y |
-
当第⼆个操作是volatile写时,不管第⼀个操作是什么,都不能重排序。
-
当第⼀个操作是volatile读时,不管第⼆个操作是什么,都不能重排序。
-
当第⼀个操作是volatile写,第⼆个操作是volatile读时,不能重排序。
内存屏障
| 屏障类型 | 指令示例 | 说明 |
| — | — | — |
| LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据的装载先于Load2及所有后续装载指令的装载 |
| StoreStore Barriers | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新达到内存)先于Store2及所有后续存储指令的存储 |
| LoadStore Barriers | Load1;LoadStrore;Store2 | 确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存 |
| StoreLoad Barriers | Store;StoreLoad;Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载.StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令 |
-
在每个volatile写操作的前⾯插⼊⼀个StoreStore屏障
-
在每个volatile写操作的后⾯插⼊⼀个StoreLoad屏障
-
在每个volatile读操作的后⾯插⼊⼀个LoadLoad屏障
-
在每个volatile读操作的后⾯插⼊⼀个LoadStore屏障
写final域的重排序规则
-
JMM禁止编译器把final域的写重排序到构造函数之外.
-
编译器会在final域的写之后,构造函数return之前插入一个StoreStore屏障
读final域的重排序规则
-
在⼀个线程中,初次读对象引⽤与初次读该对象包含的final域,JMM禁⽌处理器重排序这两个操作
-
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
写final域的重排序规则
- 在构造函数内对⼀个final引⽤的对象的成员域 的写⼊,与随后在构造函数外把这个被构造对象的引⽤赋值给⼀个引⽤变量,这两个操作之 间不能重排序。
双重检查锁定
1 public class DoubleCheckedLocking {
2 private static DoubleCheckedLocking doubleCheckedLocking;
3
4 private DoubleCheckedLocking() {
5
6 }
7
8 public static DoubleCheckedLocking getInstance() {
9 if (doubleCheckedLocking == null) {
10 synchronized (DoubleCheckedLocking.class) {
11 if (doubleCheckedLocking == null) {
12 doubleCheckedLocking = new DoubleCheckedLocking();//问题出现在这里
13 }
14 }
15 }
16 return doubleCheckedLocking;
17 }
18 }
我们来看看这段双重检查锁定的单例模式有什么问题?
线程A设置指向刚分配的内存地址后,线程B就判断doubleCheckedLocking
对象是否为空,然后直接返回未初始化的doubleCheckedLocking对象,这样会引发出很严重的问题.
解决方案1:
使用volatile,禁止2和3重排序
1 public class DoubleCheckedLocking {
2 private volatile static DoubleCheckedLocking doubleCheckedLocking; 3
4 private DoubleCheckedLocking() {
5
6 }
7
8 public static DoubleCheckedLocking getInstance() {
9 if (doubleCheckedLocking == null) {
10 synchronized (DoubleCheckedLocking.class) {
11 if (doubleCheckedLocking == null) {
12 doubleCheckedLocking = new DoubleCheckedLocking();//问题出现在这里
13 }
14 }
15 }
16 return doubleCheckedLocking;
17 }
18 }
最后
面试是跳槽涨薪最直接有效的方式,马上金九银十来了,各位做好面试造飞机,工作拧螺丝的准备了吗?
掌握了这些知识点,面试时在候选人中又可以夺目不少,暴击9999点。机会都是留给有准备的人,只有充足的准备,才可能让自己可以在候选人中脱颖而出。
薪最直接有效的方式,马上金九银十来了,各位做好面试造飞机,工作拧螺丝的准备了吗?
掌握了这些知识点,面试时在候选人中又可以夺目不少,暴击9999点。机会都是留给有准备的人,只有充足的准备,才可能让自己可以在候选人中脱颖而出。
[外链图片转存中…(img-wltj9sx0-1715237090573)]
[外链图片转存中…(img-8YhnEPQn-1715237090573)]