PRELOADER

本站基于Hexo创建,收藏java相关技术文档。

当前文章 : 《java并发之3内存模型》

3/18/2019 —— 

Java内存模型

在并发起源中,说到了java安全性问题,是因为多线程的可见性、有序性、原子性三个问题引起,是多线程bug的起源,它们是多线程中的共性问题,java作为多线程编程中处于领先地位的人,自然有针对于此的解决方案,先说说解决可见性和有序性问题的方法,Java内存模型

什么是java内存模型

  1. 当Java并发程序出现问题时,对代码的检查,消除bug,需要我们对三个bug之源根源了解,还要了解java解决三者问题的方式,才能解决问题。我们知道,导致可见性是因为缓存、导致有序性是因为编译优化,那我们如果直接禁用两者,那么服务性能将十分堪忧;为了解决这个问题,我们就需要按需禁用,分场景和实际需要来禁用;站在我们程序猿的角度,就是让程序按照我们指定的场景禁用缓存和优化。
  2. Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方式,具体来说,他们包括volatile、synchronized和final三个关键字和Happens-before规则。

Volatile

  • volatile关键字并不是java特有的,在C语言里就有,它最原始的语意是禁用CPU缓存。

    //告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入
    volatile int x = 0;
    

    这个语意看上去十分明确,但是在使用时,却会带来困惑。

    class VolatileExample{
        int x = 0;
        volatile boolean v = false;
        public void write(){
            x = 42;
            v = true;
        }
        public void read(){
            if(v == true){
                System.out.println(x);
            }
        }
    }
    

    假如线程A执行write方法,按照volatile语意,会把变量v=true存入内存,假设线程B执行read方法,同样按照volatile语意,线程B会从内存中读取变量v,当v==true时,线程B看到的会是多少了。如果jdk低于1.5,可能是42,也可能是0,高于等于1.5,看到的x一定是42。

Happens-before

  • 望文生义,很多书面也是这么解释的,叫做先行发生,其实他并不是说前面一个操作发生在后续操作的前面,它真正要表达的意思是:前面一个操作的结果对后续操作是可见的,其规则就是要保证线程之间操作的结果可以及时被感知。这种行为,虽然允许编译器优化,但是要求编译器优化后一定要遵守Happens-before规则。

  • happens-before是java内存模型中最晦涩的内容,和程序猿相关的规则有六项,都是关于可见性的。

1.程序的顺序性规则

指在一个线程中,按照程序顺序,前面的操作happens-before后续的任意操作。比如上面的代码中,第六行x=42;Happens-before与第七行代码v=true;,这就是规则1的内容,也比较符合单线程里面的思维,程序前面对某个变量的修改,一定是对后续操作可见的。

2.volatile变量规则

是指对一个volatile变量的写操作,happens-before于后续对这个变量的读操作。就是说禁用缓存,写操作强制刷新到内存,单看此规则与1.5之前没有区别,但是和第三条联合起来,就有区别了。

3.传递性

这条规则是指,A happens-before于B,B happens-before于C,那么A一定happens-before与C。

线程a --> v=42
v=42 --> v=true
线程b --> v==true
v==true --> 读变量x

4.管程中的锁规则

管程中对一个锁的解锁happens-before于后续线程对这个锁的加锁。这个规则中,管程指的是一种通用的同步原语,在Java中指的就是synchronized,是java对管程的实现。管程中的锁在Java中是隐式实现的,例如下面的代码,进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁解锁都是编译器自动帮我们实现。

synchronized (this){
    //x是共享变量,初始值10
    if(this.x <12)this.x = 12;
    }
}

线程a进入代码块,加锁,对x赋值为12,程序执行结束并释放所,那么后续线程再执行这段代码时,x的值为12对于后续线程是可见的。

5.线程start规则

指主线程a启动子线程b后,子线程b能够看到主线程在启动子线程b之前的操作如果a线程调用b线程的start方法(在a中启动b),那么该start操作happens-before于线程b的任意操作。

@Test
public void testStart(){
    Thread a = new Thread(() -> {
        final int var = 66;
        System.out.println(var);

        Thread b = new Thread(() -> System.out.println("线程b看到的var是:"+var));
        // 此处对共享变量 var 修改var = 77;然后再启动线程b,其实b也是可以看到的。
        b.start();
    });
    a.start();
}

6.线程join规则

主线程A中调用线程B并等待线程B操作完成,在线程A中调用线程B的join方法实现即可实现,线程A可以看到线程B的所有操作线程B的任意操作happens-before于线程B的join操作

@Test
public void testStart(){

    Thread a = new Thread(() -> {
        final int var = 66;
        Thread b = new Thread(() -> {
            //加入这里修改了var,var=77;线程b执行结束后,线程a就可以看见var变成了77
            System.out.println("线程b看到的var是:"+var)
        });

        b.start();
        try {
            b.join();//等待线程b执行结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //这里线程a其实已经可以看到var变成了77,由于函数式编程导致无法通过修改var来观测,
        //但是该条规则实际意思是这个意思
        System.out.println("我要等到线程b输出后才能输出,我看到var是77");
    });

    a.start();
}

7.线程中断规则

对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

8.对象终结规则

一个对象的初始化完成(构造函数执行结束)happens-before于它的finalize()方法的开始。