CPU
cache模型
CPU与内存的访问的速度相差很大,可达千倍,由于速度验证不对等 为了解决CPU直接访问内存效率地下的问题,就设计了cache模型
程序运行过程中,会将所需数据从主存中复制到CPU的cache中,操作完毕后再将数据刷新到主存中。 CPU通过直接访问cache极大提高了CPU的吞吐能力
缓存一致性
由于数据需要从主存中复制到cache,然后再操作,最后刷新到主存中 单线程不会出现缓存不一致问题,但是多线程会出现
解决方法
总线加锁
一种悲观锁的实现,某个时刻只能有一个CPU能够访问到这个变量,这种方式低下, 所以有了第二种方式
缓存一致性协议
比较出名的就是Intel的MESI协议 基本思想
- 读取操作:不做任何处理,只是将cache中的数据读取到寄存器
- 写入操作:发出信号通知其他CPU将共享变量的Cache line 置为无效,所以CPU必须重新到主存中读取
JMM(Java内存模型)
JMM指定了JVM如何与主存(RAM)进行工作, 定义了线程与主存的抽象关系,具体如下
- 共享变量存放与主存中,每个线程都能访问
- 每个线程都有自己私有的工作内存或者称之为本地内存
- 工作内存只存储该线程对共享变量的副本
- 线程不能直接操作主存,只能先操作工作内存后,才能写入到主存中
具体如下图
JMM关于同步的规定
-
线程解锁前,必须把内存变量的值刷新回主存中
-
线程加锁前,必须读取主存的最新值到自己的工作内存中
-
加锁解锁是同一把锁
并发编程基本特性
原子性
跟数据库事务的原子性类似 指操作要么全部完成,要么全部不完成
可见性
可见性指当一个线程对共享变量进行了修改,其他线程可以立即看到修改后的最新值 代码实例
class MyData{
//如果不加volatile修饰,那么会一致在while中遍历
public volatile int number = 0;
public void add060(){
number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
MyData myData = new MyData();
executorService.submit(()->{
System.out.println( Thread.currentThread().getName() + " come in");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.add060();
System.out.println( Thread.currentThread().getName() + " number changed");
});
// main线程中的判断, 如果主存中的number刷新,但是main线程的工作内存没有刷新, 那么会一致死循环下去
while (myData.number == 0){
}
System.out.println( Thread.currentThread().getName() + " mission completed");
}
}
有序性
有序性是指代码在执行过程中的先后顺序, 由于Java编辑器的优化,导致代码可能不严格按照编写时的顺序执行,但是它会保证最终的运算结果跟期望一致
如:
int x = 10;
int y = 0;
x++;
y = 20;
执行过程中 y = 20 可能在 x++前执行 单线程中没有问题,但在多线程中,线程交替执行,由于编译器优化重拍的存在,两个线程中使用的变量无法保证一致性,所以结果无法预测
JMM如何保证三大特性
1. 原子性
JMM只保证基本读取和赋值的原子操作,其余的均不保证,需要借助外部工具 如:
- synchronized
- JUC中的lock
- 原子类
2. 可见性
Java使用以下方法来保证可见性
- 关键字volatile
- 关键字synchronized
- JUC中的显式锁Lock
3. 有序性
Java使用以下方法来保证有序性
- 关键字volatile
- 关键字synchronized
- JUC中的显式锁Lock
volatile解析
JVM提供的轻量级锁 volatile只能保证两个:
- 可见性
- 有序性
volatile
1. 保证可见性
由于JMM的特点,线程的工作内存与主存取值 所以需要使用volatile及时更新变量值,保证其他线程拿到的是最新值
2. 不能保证原子性
可以使用原子类解决, 或者加锁(下策)
3.有序性
多线程中,线程交替执行,由于编译器优化重拍的存在,两个线程中使用的变量无法保证一致性,所以结果无法预测
使用volatile后,会禁止指令重排 单例模式的懒汉模式双重检测锁就用到了volatile
// 使用volatile ,避免指令重排
private static volatile DoubleCheck singleton;
// 双重检测锁
public static DoubleCheck getInstance(){
if(singleton == null){
synchronized (DoubleCheck.class){
if(singleton == null){
singleton = new DoubleCheck();
}
}
}
return singleton;
}
volatile 与 synchronized
使用区别
-
volatile 只能用于修饰实例变量或者类变量 不能修饰方法,方法参数,局部变量,常量等
-
synchronized可以修饰方法或代码,不能修饰变量。
效果区别
-
原子性 volatile不能保证原子性 synchronized可以保证原子性
-
可见性 都可以保证可见性
-
有序性 都可以保证有序性
-
阻塞 volatile不会导致阻塞 synchronized会导致线程进入阻塞状态