标签:解释 要求 方式 子类 初始化 增加 oid https 改进
CPU、内存、I/O设备的速度存在巨大差异,程序的整体性能取决于最慢的操作——读取I/O设备,为了合理利用CPU性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序做出了以下改进。
早期单核CPU时,CPU缓存的数据与内存的数据是保持一致的,
线程A,线程B在同一个核心上切换运行,A对缓存中的操作对于B是立即可见的,所以不存在可见性问题
到了多核CPU时代,每个核心都有自己的CPU缓存(L1 cache,L2 cache).
线程A,线程B在执行不同的核心上执行时,先将数据从内存读取到各自的CPU缓存,此时线程A对于数据的操作,对于线程B是不可见的。
写操作对于读操作 是立即可见的
高级程序语言中的一条语句往往对应这操作系统中的多条指令,
线程在CPU的执行又是时间片轮转的方式执行,和可能在线程A将内存中的变量值V = 0 读取到寄存器中时,切换到线程B执行将V的值进行了更新 V=V+1
此时V = 0 更新到内存,切换到A线程,对寄存器中 V=0 进行V++,在更新到内存 V=1。
就会出现两个线程进行 V = V+1操作,结果却还是2的情况。
一个或者多个操作在 CPU 执行的过程中不被中断的特性,称 为“原子性”
编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”
编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,
但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。
例如:双重校验单例模式
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
理想的执行过程:
假设线程A、线程B同时调用getInstance()方法,
经过编译优化后对象的创建过程
当执行2后发生线程切换,线程B得到的instance应用地址并未初始化对象就会产生空指针。
Java内存模型针对于
可见性问题 ——CPU缓存导致
有序性问题 ——编译优化导致
原子性问题 ——线程切换导致
提供的方案是按需禁用CPU缓存及编译优化
https://www.bilibili.com/video/av81008349
具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及Happens-Before 规则
volatile关键字:
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
虽然禁用了缓存,所有线程都要从内存中读取,但是不具备互斥性(即同一时间只有一个线程可以执行)
适用于一个线程写 另一个线程读 可以读到写入的最新值
synchronized关键字:
同步关键字,进行修饰的代码块或者方法,进行加锁,保证同时只有一个线程能够获取到锁。
Happens-Before 约束 了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 HappensBefore 规则。
核心原则:前面一个操作的结果对后续操作是可见的。
六大规则:
/**
* 可见性问题
* 1.主线程中启动子线程 获取a的值 判断进行循环
* 2.因为循环中是空方法,a一直会读取CPU缓存中的值 一直为true
* 3.所以执行结果是 打印false后 子线程还在死循环执行
*/
public class Test {
static boolean a = true;
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
while(a){
}
}).start();
Thread.sleep(1000);
a = false;
System.out.println(a);
}
}
/**
* 解决可见性问题
* volatile禁用CPU缓存
* 并且使得线程的写操作对 读操作可见
* 执行结果: 主线程打印false后 子线程读取到a的变更停止死循环
*/
public class Test {
static volatile boolean a = true;
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
while(a){
}
}).start();
Thread.sleep(1000);
a = false;
System.out.println(a);
}
}
/**
* 原子性问题
* a++操作对应
* 1.读取a的值
* 2.a+1
* 3.赋值
* 下面两个线程可能出现 线程1 读取a = 0
* 切换到 线程2 读取到 a = 0
* 线程2 执行 a++ 更新 a的值 a=1
* 再次回到线程1 寄存器中 a = 0,执行a+1,更新a=1
* 两个线程的操作 却没有 a = 2
*
* 执行结果 <=20000
**/
public class Test {
static int a = 0;//此时使用volatile关键字修饰a 并没有保证原子性
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<10000;i++){
new Thread(()->{
a++;
}).start();
new Thread(()->{
a++;
}).start();
}
Thread.sleep(2000);
System.out.println(a);
}
}
/**
* 原子性问题
* a++操作对应
* 1.读取a的值
* 2.a+1
* 3.赋值
*
* 解决原子性问题之使用原子类
* AtomicInteger 的 getAndAdd方法 在执行过程中 的读写是原子性的,不允许线程切换的
* AtomicInteger内部基于volatile关键字
*
* 执行结果:20000
**/
public class Test {
static AtomicInteger a = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<10000;i++){
new Thread(()->{
a.getAndAdd(1);
}).start();
new Thread(()->{
a.getAndAdd(1);
}).start();
}
Thread.sleep(2000);
System.out.println(a);
}
}
/**
* 原子性问题
* a++操作对应
* 1.读取a的值
* 2.a+1
* 3.赋值
*
* 解决原子性问题之使用synchronized关键字
* 线程1、线程2 同一个时间只有一个线程能执行同步代码块,通过互斥性来保证原子性
* 最为重量级的实现
*
* 执行结果:20000
**/
public class Test {
static int a = 0;
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<10000;i++){
new Thread(()->{
synchronized (Test.class){
a++;
}
}).start();
new Thread(()->{
synchronized (Test.class){
a++;
}
}).start();
}
Thread.sleep(2000);
System.out.println(a);
}
}
总结:
标签:解释 要求 方式 子类 初始化 增加 oid https 改进
原文地址:https://www.cnblogs.com/shinyrou/p/13264240.html