Synchronized 关键字提供了一种锁机制,可以实现一个简单的策略来防止线程的干扰和内存一致性错误。即 Synchronized 能够确保共享变量之间的互斥访问,从而防止数据不一致的问题出现。
Synchronized 关键字包括 moniter enter 和 moniter exit 两个 JVM 命令,他能够保证在任何线程执行到moniter enter 成功之前都必须从主存中获取数据,而不是从缓存中。在 moniter exit 运行成功之后,共享变量被更新后的值必须被刷入主存中。
public class Test {
public synchronized void functionA() {
//...
}
public synchronized static void functionB() {
//...
}
}
其中 functionA 是对 this 对象加锁,而 functionB 是对 Test.class 对象加锁
public static Object MUTEX;
public void functionA() {
synchronized(MUTEX) {
}
}
用于放在同步代码块的对象一定是 Object 类型的对象
Volatile 能够用于解决 32 位电脑中 64 位数据写入的原子性,内存可见性和禁止重排序的功能
在 32 位系统中写入 64 位数据,64 位的变量会被拆成两个 32 位进行进行写入,那么在写入一半时有其他线程来读取数据,那么会造成数据读取的错误
在线程写入一个数据时,会先向缓存中写入数据,稍后在写入到本地的主存中去。这就造成了一个线程写完了,另一个线程立刻去读取写入的数据,却读取到原先的值,虽然过一段时间后,可以读到这个数据,但是却是最终一致性,而不是强一致性。使用 volatile 后,则会立刻写入到主存中去,对其他线程可见。
对于开发者而言,是希望程序按照顺序执行下去,但是对于计算机而言,则需要将没有依赖关系的指令进行重排序,以期获得更大的效率。
例如,instance=new Instance(); 中有三个基础操作
传统的 x86 的布局如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JYmsx3sO-1661482925988)(en-resource://database/805:1)]
在 CPU 的缓存一致性协议下,多个CPU 之间的缓存不会出现不同步的问题。但是这却严重损耗了性能。
为了降低性能损耗,又在计算单元和 L1 之间增加了 Store Buffer、Load Buffer。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pywFsGYZ-1661482925990)(en-resource://database/806:1)]
从操作系统的角度来看这就相当于每一个逻辑 CPU 都有自己的缓存。
由于每个逻辑 CPU 都有自己的缓存,这些缓存和主存之间是不完全同步的,因此也就会存在内存可见性问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsWUiZYR-1661482925991)(en-resource://database/808:1)]
抽象到 Java 就是 JVM 的内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AwsUxKeO-1661482925991)(en-resource://database/807:1)]
只要操作之间没有数据依赖,编译器和CPU 可以进行任意重排序,执行的最终结果不会发生改变。这就是 as-if-serial 语义。
但是在多线程情况下便不能保证 as-if-serial 语义。
由于线程之间数据的依赖和相互影响,我们需要告知编译器和 CPU 在什么场景下可以进行重排序,什么时候不可以进行重排序。
为此 JMM 引入了 happen before 语义。如果说 A happen before B 则就意味着, 操作 A 的结果对操作 B 必须可见。
happen before 规定,对于 volatile 变量的写入操作必须对后续 volatile 变量的读取可见。
同时 happen before 具有传递性,如果 A happen before B,B happen before C,那么 A happen before C。
volatile 只用于对变量的声明。
public static volatile int c;
内存屏障分为四种
PS 上述 A 和 B 都是操作的代号,不是数据的代号。