volatile与synchronized实现原理

目录

一、CPU原子操作

1. 总线锁

2. 缓存锁

3. 总结

二、volatile实现原理

1. 三级缓存

2. volatile实现

3. volatile非线程安全

4. 指令重排序

5. volatile适用场景

三、synchronized实现原理

1. Java对象头

2.  synchronized实现

        a. 加锁的形式

        b. 加锁实现

3. 锁升级

        a. 偏向锁

        b. 轻量级锁

四、参考资料


一、CPU原子操作

        原子(atomic)本意是“不能被分割的最小粒子”,而CPU原子操作(atomic operation)为“不可被中断的一个或一系列指令操作”。

        CPU会自动保证基本内存操作的原子性,如系统内存读或写一个字节时是原子的,即:其他处理器不能访问该字节的内存地址。但是,复杂的内存操作(如:多核CPU对同一缓存行的写、跨总线宽度、跨多个缓存行、跨页表的访问)都不是原子操作,所以CPU提供总线锁定和缓存行锁定两种机制来保证CPU的原子操作。

1. 总线锁

        CPU总线负责CPU与外界所有部件的通信(包括高速缓存、内存、北桥)。

  • 控制总线:向各个部件发送控制信号
  • 地址总线:发送地址信号指定其要访问的部件
  • 数据总线:双向传输数据

        总线锁是处理器在总线上输出LOCK#信号,其他处理器的请求被阻塞,那么该处理器可以独占共享内存。总线锁把CPU和内存之间的通信锁住,导致其他处理器不能访问内存,所以总线锁的开销大

2. 缓存锁

        缓存锁是处理器有Lock前缀指令,当写入缓存行时,根据缓存一致性机制该缓存回写到内存,同时其他处理器有该数据的缓存行变成无效。这种机制阻止了有两个以上的处理器同时修改内存区域的数据。

        CPU使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存的一致性,能够嗅探其他处理器访问系统内存和它们的内部缓存。内部缓存被修改后,处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,若过期该缓存行设置为无效,从而保证了当前内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。

  • M _ 被修改的(Modified):只在本CPU中有缓存数据,而其他CPU中没有。同时相对于系统内存的值来说,是已经被修改的,且没有更新到内存中;
  • E _ 独占的(Exclusive):只在本CPU中有缓存,且其数据没有修改,即与内存中一致;
  • S _ 共享的(Share):多个CPU中都有缓存,且与内存一致;
  • I _ 无效的(Invalid):本CPU中的缓存已经无效。

        Intel处理器提供了很多Lock前缀的指令来实现。例如,位测试和修改指令:BTS、BTR、BTC;交换指令XADD、CMPXCHG,以及其他一些操作数和逻辑指令(如ADD、OR)等。其中Volatile底层实现就是通过指令CMPXCHG完成

        不会使用缓存锁、使用总线锁的情况:

  • 数据不能缓存到处理器内部缓存或跨多个缓存
  • 处理器不支持缓存锁

3. 总结

类型说明
总线锁

1. 总线上输出LOCK#信号,其他处理器无法与内存通信;

2. 开销大;

缓存锁

1. Lock前缀指令;

2. 回写系统内存;其他处理器缓存无效;

3. 指令CMPXCHG完成Volatile底层实现;

不使用缓存锁的情况:

                                a. 数据不能缓存到处理器内部缓存或跨多个缓存

                                b. 处理器不支持缓存锁

二、volatile实现原理

1. 三级缓存

        如上图所示,该电脑有L1、L2、L3的三级缓存。当CPU要读取一个数据时,首先从L1中查找,如果没有再从L2中查找,如果还是没有再从L3中或内存中查找。

        一般来说每级缓存的命中率大概都有80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取,如下图所示。

2. volatile实现

        Java中有volatile变量修饰变量,有两个作用。使用Lock前缀的CMPXCHG指令。

  • 禁止指令重排序,编译时指令重排序和CPU乱序执行
  • 多线程共享变量的内存可见性        

        如下图所示,若有volatile变量修饰的共享变量i。多线程使用i时,都会在自己的线程中创建变量i的副本,其中A线程处理完成后,去更新缓存(Lock前缀的CMPXCHG指令)。

  • step1:缓存是否有效;
  • step2:缓存有效,则回写到主存,且其他线程中缓存失效;
  • step3:缓存无效,等待其他线程释放缓存锁(回写到主存完成),再从主存读取并再处理。

        Lock前缀,有两个目的(见第一章节:CPU原子操作中的缓存锁):

  • 回写到主内存
  • 其他线程该数据的缓存行变成无效

        注意,线程共享变量的可见性,一个线程修改的状态对另一个线程时可见的,即:一个线程修改的结果,另一个线程马上就能看到(嗅探总线的数据)。 volatile只能保证变量的可见性,无法保证对变量操作的原子性

3. volatile非线程安全

        volatile修饰的i变量,进行i++操作:

                step1:从内存中读取i当前的值

                step2:局部变量区变量i加1

                step3:把修改后的值刷新到内存中

        这三个步骤不是原子性操作,volatile只能保证step1和step3的改变立即可见,但是无法决定step2,当多线程同时执行的时候,出现的交叉修改,所以无法保证线程安全性。

4. 指令重排序

        指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度,指令重排包括编译时重排序和运行时重排序

        例如,虽然代码语句的定义顺序为1-2-3,但是计算顺序1-2-3与2-1-3对结果并没有影响,所以编译时和运行时可以根据需要对1,2语句进行重排序。

double r = 2.1; //(1)

double pi= 3.14; //(2)

double area = p * r * r; //(3)

5. volatile适用场景

(1)volatile是轻量级同步机制:访问volatile变量时不会执行加锁操作,也就不会使执行线程阻

          塞,是一种比synchronized关键字更轻量级的同步机制。

(2)volatile不能修饰写入操作依赖当前值的变量:volatile关键字不起作用,如:i++

(3)当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile;

(4)volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低

三、synchronized实现原理

1. Java对象头

        Java对象一般在内存中的布局通常由对象头、实例数据、对齐填充三部分组成,如下所示。如果对象是数组类型,则对象头里增加了数组长度。

在这里插入图片描述

        如果对象是数组类型,对象头则JVM用3个字宽(Word)存储;如果对象是非数组类型,则用2字宽存储。32位JVM中,1字宽等于4字节,即32bit;64位JVM中,1字宽等于8字节,即64bit。Mark Word默认存储对象的哈希值(HashCode )、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息。

        运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,如下图所示。 

2.  synchronized实现

        a. 加锁的形式

        普通同步方法:锁当前实例对象。
        静态同步方法:锁当前类的Class对象。
        同步方法块:锁Synchonized括号里配置的对象

        b. 加锁实现

        Java中每个对象都有一个Monitor对象(对象的锁对象)。JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,所以要保证每个monitorenter必须有对应的monitorexit与之配对

        线程执行到monitorenter指令时,将会尝试获取对象所对应的Monitor的所有权,即尝试获得对象的锁。当线程持有Monitor对象时,那么该线程就拥有该对象的锁,其他线程被阻塞放入队列中等待释放锁。拥有锁的线程执行monitorexit,才会释放锁。

3. 锁升级

        锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这4个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率

        a. 偏向锁

        锁不仅存在多线程竞争,而且由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

        如果线程ID一致,表示线程已经获得了锁;如果不一致,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。如下流程图所示。

        偏向锁在Java6和Java7里是默认启用,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。可关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

        b. 轻量级锁

        随着锁的竞争加剧,偏向锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

        自旋来获取锁失败后,则锁膨胀为重量级锁,如下图所示。

四、参考资料

百度

Juc18_Java内存模型、对象头Mark Word、实例数据、对齐填充、谈谈new Object( )占多大内存_所得皆惊喜-CSDN博客

Volatile使用原理及作用_huangwei18351的博客-CSDN博客_volatile的作用和原理

详解synchronized与Lock的区别与使用_brickworkers的博客-CSDN博客_synchronized和lock的区别AtomicInteger原理_爱我所爱0505-CSDN博客

Java锁--Lock实现原理(底层实现)_技术与生活齐头并进-CSDN博客

让你彻底理解Synchronized - 简书

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值