揭秘JMM、Synchronized、Volatile之间的关系

2、线程在加锁前,必须读取主存中最新的值到工作内存(线程有自己的工作内存)中!

3、加锁和解锁是同一把锁

1.2 JMM的内存操作

JMM呢我们逻辑上可以把它分为主内存和工作内存。而两个内存之间也是有进行交互的,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存回到主内存。关于这些操作我们主要是有八种:

  • lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线程独占这个变量

  • unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定

  • read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用

  • load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)

  • use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作

  • assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作

  • store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用

  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

在使用上的流程就是如下顺序,我们可以画一个图就更加清晰明了:

我的主内存有一个flag = false;通过线程A来修改为true

但是需要注意的是,在使用这些指令的时候也是需要满足一些规则的:

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回
    主内存
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存
    中。
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或
    assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执
    行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。(也就是可重入锁的概念)
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量
    前,需要重新执行load或assign操作以初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

这8种内存访问操作以及上述规则限定明确地描述了在我们Java程序中哪些内存访问操作在并发下是安全的。但是这样操作却是极其繁琐的,所以被简化成了read,write,lock,unlock四种操作,但也只是语言上的简化,实际模型的基础设计并未简化。

1.3 JMM的特性

我们在开头提到了volatile的三大特性,然后要介绍就要先普及JMM的基础,其实并不是没有道理的。关于JMM我们也有三大特性(JMM保证),总结起来就是:

  • 原子性
  • 可见性
  • 有序性

可以发现,这与volatile是很类似的。下面一张图可以很好的理清之间的关系:

在上面基本的数据类型读或写,我们看到了long,double除外,这涉及到了针对long和double型变量的特殊规则。

Java内存模型要求lock、unlock、read、load、assign、use、store、write这八种操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非原子性协定”(Non-Atomic Treatment of double and long Variables)。

关于三大特性,我们这里简单介绍一下

1.3.1 原子性

原子性呢是指操作是一体的,要么成功,要么失败,没有第三种情况。上图我们也说到Java中的基本数据类型的访问,读或写都是具备原子性的。这里我们举一个例子

int i = 5;

这里我们的赋值操作就是原子性,而一个比较经典的就是

int i = 0;
i++;

这里i++就不是原子性的,我们可以看作它是先获取了i的值,然后进行写入值i = 1的操作。我们常见的数据类型的操作都是原子性的,但是如果应用场景需要一个更大范围的原子性保证的话,Java内存模型提供了lock和unlock操作来满足这种需求。也提供了更方便快捷的synchronized关键字,同样具备原子性。

1.3.2 可见性

可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。比如当我们没有任何操作来处理两个线程的时候:

int i = 0; //主线程
i++; //线程1
j = i; //线程2

我们可以发现线程1修改了i的值,但是没有刷回主内存,线程2读取了i的值,赋值给了j,我们期望j就是1,但是因为虽然线程1修改了,没有来得及复制到主内存中,线程2读取后,j还是0。这就是内存不可见性,同理我们就可以理解了可见性。

常见的我们可以用volatile关键字来修饰变量,达到了内存可见性。

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除了volatile之类,还有synchronized和final也可以保证可见性。syschronized是因为JMM的规则限定对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作),而final关键字的可见性是指:

被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程中就能看见final字段的值。

1.3.3 有序性

一句话总结的话就是

如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。

问题来了,为什么在一个线程观察另一个线程的时候,操作都是无序的呢?

这就涉及到了指令重排:你写的程序,计算机并不是按照你写的那样去执行的,我们可以举个例子来说明

int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4

我们期望的程序执行顺序是1->2->3->4,我们发现如果程序是2->1->3->4执行结果也是一样的,或者1->3->2->4也行,但是如果按照1—>2->4->3之类的呢?就得不到我们的期望结果,这就是指令重排导致的。

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

但是如果所有的有序性都靠这两个关键字来完成的话,那么很多操作就会变得特别啰嗦,所以就有了一个Happens-Before原则。

1.4 Happens-Before原则

  • 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
    ·volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止
    执行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

2 关于Synchronized关键字

上面我们介绍了JMM的特性的时候,也了解到了Synchronized是一个比较全能的同步块,可以保证很多的特性。

synchronized块是Java提供的一种原子内置锁,Java中的每个对象都可以把它当作一个同步块来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫做监视器锁。程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异后或者在同步块内调用了该内置锁资源的wai t系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。

2.1 synchronized的内存语义

使用synchronized可以解决共享变量内存可见性的问题。

synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。

除可以解决共享变量内存可见性问题外,synchronized经常被用来实现原子性操作。另外请注意,synchronized关键字会引起线程上下文切换并带来线程调度开销。

3 关于Volatile关键字

其实关于volatile的特性,我们在介绍JMM的时候都已经了解很大一部分了。但是我们也说过volatile是不保证原子性的,这里我们还是需要用代码来展示的。

我们开十个线程,每个线程都对被volatile修饰的共享变量进行1000次自增操作。

public class Demo01 {

private volatile int num = 0;

private void increase(){
num++;
}

public static void main(String[] args) throws InterruptedException {
final Demo01 demo01 = new Demo01();
for(int i = 0; i < 10; i++){
new Thread(()->{
for(int j = 0; j < 1000; j++){
demo01.increase();
}
}).start();
}
//保证在主线程结束之前,其他线程执行完毕
TimeUnit.SECONDS.sleep(2);
System.out.println(demo01.num);
}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

MySQL全家桶笔记

还有更多面试复习笔记分享如下

Java架构专题面试复习

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
转存中…(img-3Dpy8xNi-1713706008185)]

还有更多面试复习笔记分享如下

[外链图片转存中…(img-PgYWQh5B-1713706008185)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 10
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值