专业的JAVA编程教程与资源

网站首页 > java教程 正文

Java并发编程(19)synchronized如何保证原子性、可见性、有序性

temp10 2025-02-08 12:54:20 java教程 11 ℃ 0 评论

上一篇:Java并发编程(十八)指令乱序机制及预测执行

一、synchronized如何保证原子性

synchronized保证原子性其实底层是通过加锁来保证的,在进入加锁代码块的时候加一个monitorenter的指令,然后针对锁对象关联的monitor累加加锁计数器,同时标识自己这个线程加了锁;

Java并发编程(19)synchronized如何保证原子性、可见性、有序性

通过monitor里的加锁计数器可以实现可重入的加锁;

在出锁代码块的时候,加一个monitorexit的指令,然后递减锁计数器,如果锁计数为0,就会标志当前线程不持有锁,释放锁;

然后wait和notify关键字的实现也是依托于monitor实现的,在线程执行wait之后,自己会加入一个waitset中等待唤醒获取锁,notifyall操作会从monitor的waitset中唤醒所有的线程,让他们竞争获取锁;

  • Header(对象头)

1、自身运行时的数据(Mark Word)

(根据系统虚拟机大小不同其大小不同,32位虚拟机数据大小为32位,64位系统虚拟机数据大小为64位)

存储内容:

哈希值(hashCode()方法是native)

GC分代年龄(为分代收集算法所服务 分代好处:针对各个年龄代特点,选择适当的垃圾收集算法。)

锁状态标志,标识是否加锁,加锁了几次;

在Mark Word里就有一个指针,是指向了这个对象实例关联的monitor的地址,这个monitor是c++实现的,不是java实现的。这个monitor实际上是c++实现的一个ObjectMonitor对象,里面包含了一个_owner指针,指向了持有锁的线程。

ObjectMonitor里还有一个entrylist,想要加锁的线程全部先进入这个entrylist等待获取机会尝试加锁,实际有机会加锁的线程,就会设置_owner指针指向自己,然后对_count计数器累加1次;

偏向线程ID

偏向时间戳

2、类型指针(Class Metadata Address)

对象指向类的元数据的指针,虚拟机通过这个指针来确定对象是哪一个类的实例,并不是所有jvm实现都必须在对象数据上保留类型指针


3、作为普通对象的对象头结构只有以上两种,但如果对象是一个数组,则对象头中还包含记录数组长度的数据

synchronized如何保证可见性

Load屏障的作用是执行refresh处理器缓存的操作,说白了就是对别的处理器更新过的变量,从其他处理器的高速缓存(或者主内存)加载数据到自己的高速缓存来,确保自己看到的是最新的数据。

Store屏障的作用是执行flush处理器缓存的操作,说白了就是把自己当前处理器更新的变量的值,都刷新到高速缓存(或者主内存)里去

在monitorexit指令之后,会有一个Store屏障,让线程把自己在同步代码块里修改的变量的值都执行flush处理器缓存的操作,刷到高速缓存(或者主内存)里去;然后在monitorenter指令之后会加一个Load屏障,执行refresh处理器缓存的操作,把别的处理器修改过的最新值加载到自己高速缓存里来;

总结:通过Load屏障和Store屏障,就可以让synchronized保证可见性。

synchronized如何保证有序性

在monitorenter指令之后,Load屏障之后,会加一个Acquire屏障,这个屏障的作用是禁止读操作和读写操作之间发生指令重排序。在monitorexit指令之前,会加一个Release屏障,这个屏障的作用是禁止写操作和读写操作之间发生重排序。

所以说,通过 Acquire屏障和Release屏障,就可以让synchronzied保证有序性,只有synchronized内部的指令可以重排序,但是绝对不会跟外部的指令发生重排序。

总结

  1. 原子性:加锁和释放锁,ObjectMonitor;
  2. 可见性:加了Load屏障和Store屏障,释放锁flush数据,加锁会refresh数据;
  3. 有序性:Acquire屏障和Release屏障,保证同步代码块内部的指令可以重排,但是同步代码块内部的指令和外面的指令是不能重排的;

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表