JAVA并发(四)JMM、CAS、原子性、可见性、有序性、synchronized对象锁、JVM锁优化

线程安全的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者线程如何交替执行,主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。

对于线程安全的理解,更简便的一种方式是:能否保证原子性、有序性、可见性。原子性简单理解就是:多线程下的对变量的访问操作也是原子的,不会中途被其他线程篡改。有序性的简单理解是:多线程下代码的执行顺序是正确的,不会因为指令重排序的存在,导致多次的执行结果不同。可见性的简单理解是:多线程下某个线程对变量的修改,其他线程能够看到这种修改。

本文将从缓存与内存之间数据安全传递的问题入手,介绍现代处理器的缓存一致性协议,然后介绍JMM工作内存、主内存、以及二者之间数据的安全传递是如何保证的,进而详细介绍原子性、可见性、有序性;在介绍原子性时,顺便介绍CAS,最后单独一章介绍JAVA对象锁-----synchronized关键字的原理、使用、以及JVM的锁优化。

JAVA内存模型

本章参考书籍《深入理解JVM》

缓存与内存

cpu处理速度远大于从内存读写数据的速度,因此,现在的计算机都会在cpu与内存之间加一层高速缓存区,将运算需要的数据从内存复制到缓冲中,让运算快速进行,运算结束后,再从缓存同步回内存,处理器无需等待内存读写。

缓存带来了缓存一致性的问题,在多处理器中,每个处理器都有自己的高速缓存区,而这些处理器又共享同一块内存,当多个处理器的运算任务涉及到同一块内存时,将可能导致缓存数据不一致,同步回内存的数据究竟以哪个缓存为准?
在这里插入图片描述

缓存一致性协议

缓存一致性协议不是JMM规定的,而是处理器厂商共同制定的,作用在处理器上。

在多处理器下,为了保证各个处理器的缓存是一致的,就会在各个处理器实现缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,如果处理器发现自己缓存行对应的内存地址被修改,就会将该缓存行设置成无效状态,下次要使用就会重新从内存中读取。

比较容易理解的方式是:若某个CPU将缓存回写至内存,其他CPU能够立刻获取这种修改。

内存模型JMM

java内存模型是根据缓存一致性协议,解决缓存与内存读写同步问题的过程抽象。

JMM定义了程序中变量的访问规则。这些规则保证了:变量在工作内存与主内存间安全的传递

与JVM堆栈的对应关系

主内存与工作内存与JVM的堆栈不是一个划分方式,如果一定要勉强对应,如下:

  • 主内存:堆,为JAVA虚拟机分配的物理内存

  • 工作内存:栈(线程使用到的变量的主内存副本),对应寄存器、高速缓存。
    在这里插入图片描述
    虚拟机规定:

  • 线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。

  • 不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

工作内存与主内存交互操作

主内存与工作内存间变量的操作基于8种最小粒度的原子操作分类:

①lock:作用于主内存变量,把变量标识为某一个线程独占的状态,其他线程无法访问

②unlock:作用于主内存变量,释放被lock的变量

③read:作用于主内存变量,把变量从主内存传输到工作内存

④load: 作用于工作内存变量,把read传输过来的变量加载至工作内存的变量副本中。

⑤use:作用于工作内存变量,cpu发出使用变量值的字节码指令时,jvm将该变量从工作内存传递给cpu

⑥assign:作用于工作内存变量,cpu发出赋值该变量的字节码指令时,jvm将从cpu获取的新值赋给工作内存的该变量。

⑦store: 作用于工作内存变量,把工作内存中一个变量的值传输到主内存

⑧write: 作用于主内存变量,把store传输过来的变量,加载至主内存变量中

如下图,是其中6种原子操作,JMM规定了read与load、store与write必需成对出现(避免工作内存或主内存不接收变量),所以图中直接将它们画在一起,表示工作内存与主内存之间的交互,

use、assign表示CPU与工作内存之间的交互。在执行assign后,必需执行store&write(给其他线程获取变量的新值);在执行use后,不得执行store&write(变量值未改动,避免浪费资源执行store&write);

lock与unlock比较特殊,用在主内存中控制变量的访问权限。正因为lock、unlock只作用于主内存中,JMM规定了:变量只能在主内存中产生,在执行use或assign前,必需先执行read&load。这样才真正的控制了CPU对变量的使用权限。lock、unlock对应的字节指令为monitorenter、monitorexit,更上层的是synchronized关键字。进入synchronized修饰的代码块,会添加monitorenter指令,退出synchronized修饰的代码块,会添加monitorexit。

关于lock:

  • lock&unlock可以保证原子性、有序性:一个变量同一时刻只允许一个线程(线程A)对其lock,其他线程想使用变量,必须等线程A执行unlock,A线程可以在lock状态下多次lock,多次lock后,只有线程A再执行相同次unlock,其他线程才能使用该变量;
  • lock&unlock可以保证可见性:①一旦线程执行了lock操作获取了变量的访问权限,该线程的CPU使用变量时,必需从主内存read&load变量;②一旦线程需要执行unlock,必需先执行store&write;③因为unlock会伴随着store&write,如果没有获取lock的线程可能会执行unlock,回写变量值至主内存,因此JMM又规定了:没有执行lock的线程不能执行unlock;上述三点保证了获取锁的线程永远能够看到变量的最新值。

在这里插入图片描述
将上述规则简单总结如下:

  1. lock&unlock对应synchronized修饰的代码块:目的是保证线程安全,保证原子性、有序性、可见性
  2. 变量只能在主内存产生:目的是控制变量的访问权限
  3. read和load、store和write操作必须成对出现:目的是保证变量在主内存与工作内存之间的传递
  4. 执行assign后必须执行store&write:目的是给其他线程获取变量的最新值的机会,缓存一致性协议规定了执行了store&write(回写操作)后,其他处理器能嗅探到变量的更新,会将自身工作内存中的变量设置为无效态。值得注意的是,JMM没有规定执行assign后必须立刻执行store&write,所以不能保证其他线程立刻能够获取到变量的修改。
  5. 执行use后不得执行store&write,目的是节省资源

可见性

可见性指的是内存可见性,当一个线程修改了对象状态后,其他线程能够立刻看到发生的状态变化。

在一个处理器里修改的变量值,JMM只规定了执行assign后必须执行store&write,但没有规定立刻执行store&write,所以变量的修改不一定能及时回写缓存,这种变量修改对其他处理器变得“不可见”了。所以,如果要保证可见性,要么保证变量一旦创建就不再变化(final修饰的变量),要么保证执行assign后必须立刻执行store&write。对应以下三种:

  • final修饰符,final修饰的变量在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。
  • volatile修饰符,执行assign后必须立刻执行store&write,**“立刻”**二字保证可见性。
  • 加锁,在上一章中介绍过,JMM规定了执行unlock前必须执行store&write,因此,lock与unlock也能保证可见性。

可见性与原子性对比如下:

  • 原子性:一个线程使用对象期间,对象不被其他线程修改。
  • 可见性:一个线程A使用对象期间,对象可以被其他线程修改,但是线程A能够看到发生的变化。

加锁的含义不仅仅局限于原子性,还包括内存可见性,但在不要求互斥、只要求内存可见性的情况下,再使用锁就显得有些重了,此时可以使用volatile修饰符保证可见性。

volatile原理

Java代码

private volatile TestInstance instance = new TestInstance();

上述代码的汇编代码:

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值