学习状况记录java
学习Java多线程,要了解多线程可能出现的并发现象,了解Java内存模型的知识是必不可少的。编程
对学习到的重要知识点进行的记录。缓存
注:这里提到的是Java内存模型,是和并发编程相关的,不是JVM内存结构(堆、方法栈这些概念),这两个不是一回事,别弄混了。安全
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各类硬件和操做系统的访问差别的,保证了Java程序在各类平台下对内存的访问都能获得一致效果的机制及规范。目的是解决因为多线程经过共享内存进行通讯时,存在的原子性、可见性(缓存一致性)以及有序性问题。
先看计算机硬件的缓存访问操做:多线程
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。并发
加入高速缓存带来了一个新的问题:缓存一致性。若是多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,须要一些协议来解决这个问题。函数
Java的内存访问操做与上述的硬件缓存具备很高的可比性:post
Java内存模型中,规定了全部的变量都存储在主内存中,每一个线程还有本身的工做内存,工做内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。线程只能直接操做工做内存中的变量,不一样线程之间的变量值传递须要经过主内存来完成。学习
Java 内存模型定义了 8 个操做来完成主内存和工做内存的交互操做this
Java 内存模型保证了 read
、load
、use
、assign
、store
、write
、lock
和 unlock
操做具备原子性,例如对一个 int 类型的变量执行 assign 赋值操做,这个操做就是原子性的。可是 Java 内存模型容许虚拟机将没有被 volatile 修饰的 64 位数据(long
,double
)的读写操做划分为两次 32 位的操做来进行,也就是说基本数据类型的访问读写是原子性的,除了long
和double
是非原子性的,即 load
、store
、read
和 write
操做能够不具有原子性。书上提醒咱们只须要知道有这么一回事,由于这个是几乎不可能存在的例外状况。
虽然上面说对基本数据类型的访问读写是原子性的,可是不表明在多线程环境中,如int类型的变量不会出现线程安全问题。详细的例子能够参考范例一。
想要保证原子性,能够尝试如下几种方式:
可见性指的是,当一个线程修改了共享变量中的值,其余线程可以当即得知这个修改。Java 内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
可见性的错误问题范例比较难以模拟,有兴趣的能够借助此篇文章更好的理解。
想要保证可见性,主要有三种实现方式:
volatile
synchronized
final
范例一中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,由于 volatile 并不能保证操做的原子性。
有序性是指:在本线程内观察,全部操做都是有序的。在一个线程观察另外一个线程,全部操做都是无序的,无序是由于发生了指令重排序。在 Java 内存模型中,容许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
想要保证可见性,主要如下实现方式:
volatile
synchronized
有序性这块比较难比较深的内容其实是指令重排序这块的知识。我这就借花献佛,引一篇我认为讲的比较清楚的文章。内存模型之重排序
JVM 内存模型下,规定了先行发生原则,让一个操做无需任何同步器协助就能先于另外一个操做完成。若是两个操做之间的关系不在此列,而且没法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机能够对他们随意的进行重排序。
单一线程规则 - Single Thread Rule
管道锁定规则 - Monitor Lock Rule
volatile 变量规则 - Volatile Variable Rule
线程启动规则 - Thread Start Rule
线程加入规则 - Thread Join Rule
线程中断规则 - Thread Interruption Rule
对象终结规则- Finalizer Rule
传递性 - Transitivity
在多线程状况下,时间前后顺序和先行发生原则之间基本没有太大的关系,咱们衡量并发安全问题的时候不要受到时间顺序的告饶,一切必须以先行发生原则为准。
/** * 内存模型三大特性 - 原子性验证对比 * * @author Richard_yyf * @version 1.0 2019/7/2 */ public class AtomicExample { private static AtomicInteger atomicCount = new AtomicInteger(); private static int count = 0; private static void add() { atomicCount.incrementAndGet(); count++; } public static void main(String[] args) { final int threadSize = 1000; final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executor.execute(() -> { add(); countDownLatch.countDown(); }); } System.out.println("atomicCount: " + atomicCount); System.out.println("count: " + count); ThreadPoolUtil.tryReleasePool(executor); } }
atomicCount: 1000 count: 997
能够借助下图帮助理解。
count++
这个简单的操做根据上面的原理分析,能够知道内存操做实际分为读写存三步;由于读写存这个总体的操做,不具有原子性,count
被两个或多个线程读入了一样的旧值,读到线程内存当中,再进行写操做,再存回去,那么就可能出现主内存被重复set同一个值的状况,如上图所示,两个线程进行了count++
,实际上只进行了一次有效操做。
class Foo { private int x = 100; public int getX() { return x; } public int fix(int y) { x = x - y; return x; } } public class MyRunnable implements Runnable { private Foo foo =new Foo(); public static void main(String[] args) { MyRunnable r = new MyRunnable(); Thread ta = new Thread(r,"Thread-A"); Thread tb = new Thread(r,"Thread-B"); ta.start(); tb.start(); } public void run() { for (int i = 0; i < 3; i++) { this.fix(30); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " :当前foo对象的x值= " + foo.getX()); } } public int fix(int y) { return foo.fix(y); } }
Thread-A:当前foo对象的的x值= 70 Thread-B:当前foo对象的的x值= 70 Thread-A:当前foo对象的的x值= 10 Thread-B:当前foo对象的的x值= 10 Thread-A:当前foo对象的的x值= -50 Thread-B:当前foo对象的的x值= -50
这个案例是案例一的变体,只是代码有点复杂有点绕而已,实际上就是存在两个线程,对一个实例的共享变量进行-30
的操做。
read
的操做发生在x-y
的x处,至关于两个线程第一次fix(30)
的时候,对x变量作了两次100-30
的赋值操做。
public class Test { // 是不是原子性? int i = 1; public static void main(String[] args) { Test test = new Test(); } }
请问上述 int i = 1
是不是原子性的呢?
实际上很微妙。
本案例中的int a = 1
在java中叫显式初始化,它实际上包含两次赋值,第一次java自动将a初始化为0,第二次再赋值为1。从这个角度看,这条语句包含了两步操做,并非原子的。
可是因为这句代码是在构造方法中,而从类的实例化角度看,通常认为构造方法中对当前实例的初始化过程是原子的。这是由于在实例化完成以前,通常是没法从别的代码中访问到当前实例的。因此从这个角度看,int a = 1
其实是原子的。