JUC并发编程与高性能内存队列disruptor实战-下 (3)

定义:程序的执行是存在一定顺序的。在Java内存模型中,为了提高性能和程序的执行效率,编译器和处理器会对程序指令做重排序。在单线程中,重排序不会影响程序的正确性;as-if-serial原则是指不管编译器和CPU如何重排序,必须保证单线程情况下程序的结果是正确的;但在并发编程中,却有可能得出错误的结果。

image-20220114122346225

在java当中使用volatile关键字、synchronized关键字或Lock接口来保证有序性。

可见性

定义:指在共享变量被某一个线程修改之后,另一个线程访问的时候能够立刻得到修改以后的值。如果多个线程同时读取一个变量的时候,会在每个高速缓存中都拷贝一份数据到工作内存,内存彼此之间是不可见的。缓存不能及时刷新导致了可见性问题。

JSR-133 内存模型使用happens-before原则来阐释线程之间的可见性。如果一个操作对另一个操作存在可见性,那么他们之间必定符合happens-before原则:

程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。

监视器锁规则:一个unLock操作先行发生于后面对同一个锁的lock操作。

volatile域规则:对一个变量的写操作先行发生于后面对这个变量的读操作。

传递性规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

在java当中普通的、未加修饰的共享变量是不能保证可见性的。我们照样可以通过synchronized关键字和Lock接口来保证可见性,同样也能利用volatile实现。

volatile和synchronized 两者区别

volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。

volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。

volatile用于禁止指令重排序:例如可以解决单例双重检查对象初始化代码执行乱序问题;volatile是通过内存屏障去完成的禁止指令重排序。

volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

synchronized

在Java中任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,这个对象处于锁定状态;尝试获得锁就是尝试获取对象所对应的monitor的所有权。

synchronized主要原理和思路通过monitor里面设计一个计数器,synchronized关键字在底层编译后的jvm指令中会有monitorenter(加锁)和monitorexit(释放锁)两个指令来实现锁的使用,每个对象都有一个关联的monitor,比如一个对象实例就有一个monitor,一个类的Class对象也有一个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁;计数器从0开始;如果一个线程要获取monitor的锁,就看看他的计数器是不是0,如果是0的话,那么说明没人获取锁,他就可以获取锁了,然后对计数器加1加锁成功。

而对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头其中一个重要部分Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,锁总共有四个状态,分别为无锁状态、偏向锁、轻量级锁、重量级锁,锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。java对象主要组成如下:

image-20220115141738992

而对象头Mark Word组成如下:

image-20220115142308002

volatile

volatile原理有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。

将当前处理器缓存行数据刷写到系统主内存。

这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。

这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作的时候会重新从主内存中把数据读取到缓存里。例如在Jdk7的并发包里新增了一个队列集合类LinkedTransferQueue,它在使用volatile变量的时候,会采用一种将字节追加到64字节的方法来提高性能。那为什么追加到64字节能够优化性能呢?这是因为在很多处理器中它们的L1、L2、L3缓存的高速缓存行都是64字节宽,不支持填充缓存行,例如,现在有两个不足64字节的变量AB,那么在AB变量写入缓存行时会将AB变量的部分数据一起写入一个缓存行中,那么在CPU1和CPU2想同时访问AB变量时是无法实现的,也就是想同时访问一个缓存行的时候会引起冲突,如果可以填充到64字节,AB两个变量会分别写入到两个缓存行中,这样就可以并发,同时进行变量访问,从而提高效率。

Disruptor实战 概述

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzjdzy.html