多线程编程 之java内存模型(JMM)可见性有序性问题解决方案
Posted 踩踩踩从踩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程编程 之java内存模型(JMM)可见性有序性问题解决方案相关的知识,希望对你有一定的参考价值。
前言
在java多线程编程中涉及到线程之间数据交互,会涉及到很多不可控性,包括cpu中缓存机制,以及jvm中为提高代码运行效率,从而进行的指令重排,jit解释器优化,缓存技术等等,而JMM则是使多线程可控,而提出的规范;本篇文章主要讲解java内存模型详解和多线程数据的可见性问题
java内存模型
定义
- java语言规范描述java语言特性,包括基本类型、强类型语言、泛型、各种特性等等;
- java虚拟机规范则描述的是虚拟机运行时内存数据区域,内存回收机制等等;
- java内存模型指的是java语言规范提出来的,它包含当多个线程修改了共享内存中的值时,应该读取到哪个值的规则。由于这部分规范类似于不同硬件体系结构的内存模型,因此这些语义称为Java编程语言内存模型。这部分是没有规定如何执行多线程程序。它只描述允许多线程程序的合法行为就是规则,因此多线程如何执行是由cpu进行执行的,而且是不可控制的。
为什么要有java内存规范
public class Visibility {
int i = 0;
boolean isRunning = true;
public static void main(String args[]) throws InterruptedException {
Visibility v = new Visibility();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("here i am...");
while(v.isRunning){
v.i++;
}
System.out.println("i=" + v.i);
}
}).start();
Thread.sleep(3000L);
v.isRunning = false;
System.out.println("shutdown...");
}
}
这里主要让主线程 等待3秒钟,看子线程对i进行自增长后,最后得到i的值,但是实际情况是只打印了shutdown,并没有打印i的值
shutdown...
这个isRunning一直为true,一直没有被修改,循环没有被退出
在jvm中运行参数添加下面的参数,则i值会出现下面的情况 (不同的jit编译器)
从内存区域到内存模型
因为这种内存结构,在多线程下数据交互会有各种情况出现 ,然后分析isrunning未改变的原因
数据不可见问题
- 子线程的内存区域是分配在cpu和物理内存中,cpu中也有高速缓存。因为有这个原因就有可能会导致数据可能马上不可见。
这就多线程之间操作数据,有可能出现数据马上不可见的问题。但cpu隔一段时间会去更新主内存中isrunning的值,然后子内存不断查看的时候应该是更新的。
- cpu指令重排也可能导致线程之间数据不可见
- jit编译器进行指令重排。不是执行前编译器, 而是即时编译器。
脚本语言 与 编译语言的区别
解释执行:即脚本,在执行时,由语言的解释器将其一条条翻译成机器可识别的指令。
编译执行:将编写的程序,直接编译成机器可以识别的指令码。
Java介于脚本语言与编译语言之间,也是为了提升java运行效率的,在运行时进行优化。
在运行时会把数据优化isrunning并缓存起来,发现数据问题;这个问题是jvm为了优化效率而采用的缓存起来方式,所以不算问题,java语言规范中由java内存模型提出规范防止这个数据缓存起来
解决办法
可见性:让一个线程对共享变量的修改,能够及时的被其他线程看到 。
Java内存模型中规定:
- 对某个 volatile 字段的写指令 happens-before 每个后续对该 volatile 字段的读指令。(这在jvm虚拟机中有规范)
- 对 volatile 变量 v 的写指令,与所有其他线程后续对 v 的读同步 指令
- 禁止缓存;volatile变量的访问控制符会加个ACC_VOLATILE
- 对volatile变量相关的指令不做重排序;
java内存模型基本内容
shared variables
可以在线程之间共享的内存称为共享内存或堆内存。
所有实例字段、静态字段和数组元素都存储在堆内存中,这些字段和数组都是标题中提到的共享 变量。
冲突:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
这些能被多个线程访问的共享变量是内存模型规范的对象。
线程间操作
- 线程间操作指:一个程序执行的操作可被其他线程感知或被其他线程直接影响。读写也会产生冲突。
- Java内存模型只描述线程间操作,不描述线程内操作(是没有冲突的),线程内操作按照线程内语义执行。
线程间操作有:
- read操作 (一般读,即 非volatile读)
-
write 操作 ( 一般写,即 非 volatile 写
- volatile read
- volatile write
- Lock unlock
- 线程的第一个和最后一个操作 (线程的生命周期 thread.state 默认 在jvm执行时加了happens-before指令的)
- 外部操作 访问db
对于同步的规则定义
- 对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步
- 对于监视器m的解锁与所有后续操作对于m的加锁同步
默认情况下多线程之间 频繁加锁,解锁;代码是不能指令重排的。
线程2进行操作执行对i和y操作,然后线程1操作过后,马上进行抢锁,读取值,一定是可见的。
- 对于每个属性写入默认值(0, false,null)与每个线程对其进行的操作同步
初始默认值在堆中内存值,一定所有线程都可见的。并不是赋值的这里 ,这里赋值并不是每个线程同步的
class S{
private int a=20;
private String b="20";
}
-
启动线程的操作与线程中的第一个操作同步
-
线程 T2的 最后操作 与线程 T1 发现线程 T2 已经结束同步。( isAlive ,join可以判断线程是否终结) 修改的过程是不能缓存的
-
如果线程 T1 中断 了 T2,那么线程 T1 的中断操作与其他所有线程发现 T2 被中断了同步通过抛出 InterruptedException 异常,或者调用 Thread.interrupted 或 Thread.isInterrupted
Happens-Before先行发生原则
happens-before关系用于描述两个有冲突的动作之间的顺序,如果一个action happends before另一个action,则第一个操作被第二个操作可见 ,jvm需要实现如下的happens-before规则- 某个线程中的每个动作都 happens-before 该线程中该动作后面的动作。
- 某个管程上的 unlock 动作 happens-before 同一个管程上后续的 lock 动作
- 对某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作
- 在某个线程对象上调用 start()方法 happens-before 被启动线程中的任意动作
-
如果在线程t1中成功执行了t2.join(),则t2中的所有操作对t1可见
-
如果某个动作 a happens-before 动作 b,且 b happens-before 动作 c,则有 a happens-before c.
final在JMM中的处理
- final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版 本。伪代码示例:f = new finalDemo(); 读取到的 f.x 一定最新,x为final字段。
public class Demo2Final {
final int x;
int y;
static Demo2Final f;
public Demo2Final(){
x = 3;
y = 4;
}
static void writer(){
f = new Demo2Final();
}
static void reader(){
if (f!=null){
int i = f.x; //一定读到正确构造版本
int j = f.y; //可能会读到 默认值0
System.out.println("i=" + i + ", j=" +j);
}
}
public static void main(String args[]) throws InterruptedException {
// Thread1 writer
// Thread2 reader
}
}
也就是说创建的时候,final修饰时,一定不会被缓存。
- 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值; 伪代码示例:public finalDemo(){ x = 1; y = x; }; y会等于1;
public class Demo3Final {
final int x;
int y;
static Demo2Final f;
public Demo3Final(){
x = 3;
//#### 重点 语句 #####
y = x; //因为x被final修饰了,所以可读到y的正确构造版本
}
static void writer(){
f = new Demo2Final();
}
static void reader(){
if (f!=null){
int i = f.x; //一定读到正确构造版本
int j = f.y; //也能读到正确构造版本
System.out.println("i=" + i + ", j=" +j);
}
}
public static void main(String args[]) throws InterruptedException {
// Thread1 writer
// Thread2 reader
}
}
- 读取该共享对象的final成员变量之前,先要读取共享对象。
-
通常被static final修饰的字段,不能被修改 。然而 System.in、System.out、System.err被static final修饰,
word tearing字节处理
这个问题有时候被称为“字分裂(word tearing)”,更新单个字节有难度的处理器,就需要寻求其它方式来解决问题。
double和long的特殊处理
总结
以上是关于多线程编程 之java内存模型(JMM)可见性有序性问题解决方案的主要内容,如果未能解决你的问题,请参考以下文章
Java多线程的三大特性,线程池,JMM(Java内存模型)