JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存或者叫工作内存,本地内存中存储了该线程以读/写共享变量的副本。
但本地内存是JMM的一个抽象概念,并不真实存在;它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
主内存和本地内存的交互
线程的本地内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方本地内存中的变量,线程间变量值的传递均需要通过主内存来完成。下面AB线程为例:
A线程先把本地内存的值写入主内存。
B线程从主内存中去读取出A线程写的值。
原子操作在此交互过程中,Java内存模型定义了8种操作来完成,虚拟机实现必须保证每一种操作都是原子的、不可再拆分的(double和long类型例外)
read 读取,作用于主内存把变量从主内存中读取到本本地内存。
load 加载,主要作用本地内存,把从主内存中读取的变量加载到本地内存的变量副本中
use 使用,主要作用本地内存,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。、
assign 赋值 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store 存储 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write 写入 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
lock 锁定 :作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
三大特性一个线程在执行的过程中不仅会用到CPU资源,还会用到IO,IO的速度远远比不上CPU的运算速度;当一个线程要请求IO的时候可以放弃CPU资源,这个时候其他线程就可以使用CPU,这就提高了CPU的利用率,当然线程之间的切换也会有额外的资源消耗,但多线程带来回报更大。而有了多线程就存在线程安全的问题,在Java并发编程中的一种思路就是通过原子性、可见性和有序性这三大特性切入点去考虑;在并发编程中,必须同时保证程序的原子性、有序性和可见性才能够保证程序的正确性。
原子性
定义:一个操作或多个操作做为一个整体,要么全部执行并且必定成功执行,要么不执行;简单理解就是程序的执行是一步到位的。
一个或者多个操作在 CPU 执行的过程中不被中断的特性。由于线程的切换,导致多个线程同时执行同一段代码,带来的原子性问题。
就如我们常见i++也并不是原子操作;i++分为三步,第一步先读取x的值,第二步进行x+1,第三步x+1的结果写入到内存中。
还有如我们前面将单例设计模式的双层检测锁时的instance = new DoubleCheckSingleton() 这一行代码jvm内部执行3补步,1先申请堆内存,2对象初始化,3对象指向内存地址;2和3由于jvm有指令重排序优化所以存在3先执行可能会导致instance还没有初始化完成,其他线程就得到了这个instance不完整单例对象的引用值而报错。
在java当中,直接的读取操作和赋值(常量)属于原子性操作。对于原本不具有原子性的操作我们可以通过synchronized关键字或者Lock接口来保证同一时间只有一个线程执行同一串代码,从而也具有了原子性。
有序性