不保证原子性测试
public class demo2 {
private volatile static int num=0;
public static void add(){
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2)
{
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"---"+num);
}
}
不加lock和synchronized如何保证原子性
使用原子类
代码示例
public class demo2 {
private static AtomicInteger num=new AtomicInteger();
public static void add(){
num.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2)
{
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"---"+num);
}
}
这些类的底层都和操作系统挂钩!在内存中修改值!unsafe类是一个很特殊的存在!
可见性(指令重排)
什么是指令重排
指令重排:你写的程序,并不是按照你写的那样去执行的
处理器在进行指令重排的时候,考虑:数据之间的依赖性
源代码到代码执行的过程
源代码->编译器优化重排->指令也可能重排->内部系统也会重排
指令重排原理
内存屏障,Cpu指令、作用:
- 保证特定的操作的执行顺序!
- 可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)
可见性
作用
下面会涉及几个CPU的术语
下图是内存屏障的四种类型
- 内存屏障:用于实现用户操作排列顺序的CPU指令
- 缓冲行:缓存的最小单位
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事。
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
其实很简单,各个线程会有一个共享的主内存,读取数据都要从主内存读取,每个线程都有自己的内存,线程读取数据的时候就是把主内存的数据读取到线程私有的内存里面,当线程修改自己内存中的变量之后,会将修改的值更新到主内存中,此时其他线程私有内存中所保留的值全部失效,必须重新从主内存读取该变量
volatile中使用的是地址的引用,而非值的复制,因为如果是值的复制的话,当volatile中所修饰的值变得非常大之后,复制也是非常耗时的,可能就无法保证及时将修改后的数据及时协会到内存中
线程
JMM是个虚拟概念,在实际中其实就是放在cache中
那其他线程是如何知道自己缓存的数据有变化了那?
这是通过硬件(处理器的嗅探机制)来实现的,不难猜出,所谓的嗅探机制就是当共享内存的变量被修改的时候,所有缓存该变量的线程中的值都将失效,必须要重新从共享内存读取才行
volatile优化
缓存行数据为64字节,但是如果结点的数据不足64字节的话,自动填充到64字节可以提升效率
不能使用volatile变量是都应该追加到64字节吗?两种情况不允许
- 缓存行非64字节宽的处理器
- 共享变量不会被频繁的写
内存
现代CPU 架构的形成
由于CPU 运算器的运算速度远比内存读写速度快,以至于CPU 大部分时间都在等数据从内存读取,运算完数据写回内存,所以CPU会有自己的高速缓存,修改数据的时候,都是先将内存中的数据拷贝到高速缓存中,对高速缓存中的数据进行修改,然后将数据刷新到内存中。
一般来说高速缓存分为三级,l1,l2,和l3
缓存一致性
最初是使用对系统总线加锁来控制缓存的一致性的,但是由于总线对CPU的性能损耗非常大,把多核CPU 并行的优势直接给干没了!所以后来一直到现在都是采用的缓存一致性协议。
议的类型很多(MSI、MESI、MOSI、Synapse、Firefly),最常见的就是Intel 的MESI 协议。缓存一致性协议主要规范了CPU 读写主存、管理缓存数据的一系列规范.
MESI 协议的核心思想:
-
定义了缓存中的数据状态只有四种,MESI 是四种状态的首字母。
-
当CPU写数据时,如果写的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态;
-
当CPU读取共享变量时,发现自己缓存的该变量的缓存行是无效的,那么它就会从内存中重新读取。
MESI 协议和volatile实现的内存可见性时什么关系?
volatile 和MESI 中间差了好几层抽象,中间会经历java编译器,java虚拟机和JIT,操作系统,CPU核心。
volatile 是Java 中标识变量可见性的关键字,说直接点:使用volatile 修饰的变量是有内存可见性的,这是Java 语法定的,Java 不关心你底层操作系统、硬件CPU 是如何实现内存可见的,我的语法规定就是volatile 修饰的变量必须是具有可见性的。
Java 内存模型(JMM)
JMM 是内存模型,是抽象的协议。首先真实的内存架构是没有区分堆和栈的,这个Java 的JVM 来做的划分,另外线程私有的本地内存线程栈可能包括CPU 寄存器、缓存和主存。堆亦是如此!
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作(单一操作都是原子的)来完成:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量解除锁定,解除锁定后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(有的指令是save/存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
如果要把一个变量从主内存中复制到工作内存,需要顺序执行read 和load 操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store 和write 操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行,也就是操作不是原子的,一组操作可以中断。
不允许read和load、store和write操作之一单独出现,必须成对出现。
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
Java 通过 Java 内存模型(JMM )实现 volatile 平台无关
参考文章: