都是缓存惹的祸害
缓存一致性协议: 当某个CPU核心写数据时,如果发现这个变量为共享变量,即在其他CPU缓存中也有副本,就会通知其他CPU将改变量的缓存置为无效, 其他CPU需要从内存重新读取。
MESI 的状态
案例:
数据有效,数据和内存中的数据一致,数据只存在于CPU核心1中的Cache中
数据有效,数据和内存中的数据一致,数据存在于CPU核心1和CPU核心2的Cache中。
当某一个CPU核心发生修改:CPU核心1中数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本CPU的Cache中。CPU核心中的Cache数据无效。
当修改完成后会将缓存更新,即回到S状态。
并发编程的概念: 无状态
无状态对象一定是线程安全的
并发编程的概念: 共享状态
由于count++ 不是一个原子操作,会导致线程安全问题。
并发编程的概念: 原子性
- 转账的例子
- A 向 B转账
- A = A-100
- B = B+100
- 要么全做,要么全不做, 不能出现做一半的情况
并发编程的概念: 可见性
- 多个线程访问一个共享变量时, 一个线程对变量的修改,其他线程能否立刻看到刚修改的值。
内存中共享变量i的值为0 ,在没有缓存一致性的情况下, 线程1对变量i的修改对线程2是不可见的。
并发编程的概念: 有序性
指令重排
- 为了提高性能,编译器和处理器常常会对指令做重排序
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
并发编程
在存在共享数据的情况下, 并发程序想要正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。这里的状态通常是对象的变量(静态变量和实例变量)
Java 内存模型
所有变量都在主存,必须读到工作内存线程才能操作
例子
- 线程t 读取 stop 的值(false) 并且放到自己的工作内存中
主线程也读取stop的值(false), 放到自己的工作内存中, 并且改为true
但是线程t 中工作内存中的stop 依然是false;一个线程对一个共享变量的修改对另外一个线程是不可见的!
用volatile实现可见性
- volatile关键字
- volatile boolean stop = false;
当读写一个volatile变量的时候, 每次都从主内存读写, 而不是工作内存
volatile实现了可见性
- 但是能实现原子性吗?
例子:
分析:
- 问题就出在 value ++ 上, 这不是一个原子操作
- 线程A从主内存读取最新的value值(依照volatile的原则)
- 线程A对value 值加1
- 线程A把value的值写回主内存
在线程A执行到第二步的时候, 线程B可能已经修改了主内存中value的值
对于volatile修饰的变量,直接读写是没问题的, 能保证可见性
但是对volatile变量进行操作, 原子性还是无法保证, 需要采用其他同步措施
- 例如:synchronized
有序性
//线程1:
context = loadContext(); //语句1
loaded = true; //语句2
//线程2:
while(!loaded ){
sleep()
}
doSomethingwithconfig(context);
/*由于编译器的指令重排, 语句2有可能放到语句1的前边,
这会导致线程2执行出错*/
例子:单例模式:看起来没什么问题
假设有两个线程,他们是怎么执行的?
例子:单例模式
例子:发生了指令重排
不允许对 一个volatile变量的赋值操作与其之前的任何读写操作 重新排序,
也不允许将 读取一个volatile变量的操作与其之后的任何读写操作 重新排序。