一、Java内存模型
1.1 Java内存模型概念
Java内存模型(Java Memory Model)是一种抽象的概念,并不真实存在,它描述的一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。JVM运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java 内存模型中规定所有变量都存储在主内存,其主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存考吧到增加的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储这主内存中的变量副本拷贝,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。一文读懂Java内存模型(JMM)及volatile关键字 - SegmentFault 思否
1.2 Java内存模型的功能
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与 Java 编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
Java 内存模型中规定了所有的变量都存储在主内存中(可以与物理硬件的内存类比),每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。
1.3 八种交互操作
Lock(锁定):作用于主内存中的变量,把一个变量标识为一条线程独占状态。
Unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
Read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
Write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。
Load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
Store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。
Use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
Assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
以上八种交互操作必须满足以下规则:
1. read 和 load、store 和 write 操作之一不允许单独出现
2. 一个线程不允许丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
3. 新的变量只能在主内存中诞生,且在工作内存中不允许直接使用一个未经初始化的变量。
4. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现;
5. 不允许一个线程对另一个线程Lock住的变量执行Unlock操作;
6. 若一个线程事先没有对某个变量执行Lock操作,则不允许该线程对该变量执行Unlock操作;
7. 对一个变量执行Unlock操作之前,必须将此变量同步刷新回主存中。
1.4 先行发生原则
- 程序次序规则:在一个线程内,按照控制流顺序,书写在操作先行发生于书写在后面的操作;(此处即指控制流顺序);
- 管程锁定规则:一个unlock操作先行发生与后面对同一个锁的lock操作;
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的所有动作;
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 对象终结规则:一个对象的初始化完成先行发生于它后的finalize()方法的开始;
二、并发编程中的常见问题
2.1 可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行执行的程序来说,可见性问题并不存在,因为在任何一个操作中修改了某个变量的值,后续的操作中都能读取到这个变量,并且是最新值。
在多线程中,若某个对象是共享变量,则该变量在多个线程之间是相互可见的,每个线程都有自己独立的工作内存,里面保存了该线程使用到的变量的副本。可见性问题通俗易懂的说法即为某个线程修改了共享变量的值,但因为没有保障措施,使得其他线程无法感知到该数据值已经被修改。
2.2 有序性
所谓有序性,即程序的执行顺序按照代码的先后顺序执行。
为了更高效地执行程序,底层CPU通常会对我们所编写的代码进行指令重排序(即并非按照我们编码的顺序执行)。这种情况下,若为单线程则不会有任何的影响;但若为多线程并发环境下且不存在任何保障措施,就会出现多个线程的执行结果,与我们的编码在单线程串行执行下所得结果不相同。
2.3 原子性
一个或一组中的多个操作,要么全部执行且在执行途中不会被任何因素所干扰,要么全都不执行。在多线程并发环境下,如果没有相关保障措施的情况下,极可能会发生原子性问题,导致最终的执行结果与预期不一致。
三、Java如何保障可见性、有序性、原子性
结论:
1、synchronized 能够保障:可见性、有序性、原子性;
2、volatile 能够保障:可见性、有序性;
3.1 synchronized保障三大特性
- 原子性:synchronized能够保证同一时刻最多只有一个线程执行该段代码,已达到保证并发安全的效果。 synchronized保证对象锁只能被一个线程拿到进入同步代码块内,在该线程释放锁之前其他线程无法获得锁从而无法执行同步块内的代码,以保证了同步块内的代码的原子性。
- 可见性:执行synchronized时,会对应Lock原子操作,即会先获取主内存中共享变量的最新值,当操作执行完成之后,释放锁之前(执行Unlock操作之前),会将共享变量修改过后的值同步刷新回主内存当中,以确保能够被其他线程即时感知最新值的变更。
- 有序性:添加synchronized后,底层代码仍可能会发生优化以及重排序,但因为synchronized的存在,能够保证同一时刻只会有一个线程获得锁,只会有一个线程对临界区内的代码进行操作,从而保证了结果的正确性,进而保证了有序性。(相当于单线程环境下)
3.2 volatile保障可见性、有序性
- 可见性:当多个线程都操作主存中某个共享变量(带volatile修饰)时,只要某个线程修改了该共享变量的数据值,该线程就会将共享变量同步会主存中。同时,其他拥有该共享变量的线程便能够立刻感知,并且重新从主存中读取该共享变量的最新值。
- 有序性:volatile通过读写屏障来保障共享变量的有序性;“写屏障”:保证对volatile变量之前的变量的写操作,不会被指令重排序到volatile变量之后;“读屏障”:保证对volatile变量之后的变量的读操作,不会被指令重排序到volatile变量之前。通过该方式保障了程序的有序性。