美文网首页
第十二章 Java内存模型与线程

第十二章 Java内存模型与线程

作者: 骊骅 | 来源:发表于2017-03-28 09:33 被阅读54次

    2、硬件的效率与一致性

    在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory),如图所示。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作

    3、Java内存模型

    3.1 主内存与工作内存

    • Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节
    • 此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问
    • 所有的变量都存储在主内存(Main Memory)
    • 每条线程还有自己的工作内存(Working Memory)
    • 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变

    3.2 内存间交互操作

    操作名 作用内存域 描述
    lock(锁定) 主内存 把一个变量标识为一条线程独占的状态
    unlock(解锁) 主内存 它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
    read(读取) 主内存 把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
    load(载入) 工作内存 把read操作从主内存中得到的变量值放入工作内存的变量副本中
    use(使用) 工作内存 把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
    assign(赋值) 工作内存 把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
    store(存储) 工作内存 把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
    write(写入) 主内存 把store操作从工作内存中得到的变量的值放入主内存的变量中

    3.3 对于volatile型变量的特殊规则

    • 第一个语义是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的

    以下两个场景适合使用volatile
    【1】运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
    【2】变量不需要与其他的状态变量共同参与不变约束


    【不适合例子1】

    package study12;
    
    /**
     * Created by haicheng.lhc on 06/04/2017.
     *
     * @author haicheng.lhc
     * @date 2017/04/06
     */
    public class VolatileTest {
        public static volatile int race = 0;
    
        public static void increase() {
            race++;
        }
    
        private static final int THREADS_COUNT = 20;
    
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[THREADS_COUNT];
            for (int i = 0; i < THREADS_COUNT; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 10000; i++) {
                            increase();
                        }
                    }
                });
                threads[i].setName("theead"+i);
                threads[i].start();
            }
    
            // 等待所有累加线程都结束
    //        while (Thread.activeCount() > 1)
    //            Thread.yield();
            Thread.sleep(10000);
    
            System.out.println(race);
        }
    }
    
    

    这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能够正确并发的话,最后输出的结果应该是200000。读者运行完这段代码之后,并不会获得期望的结果,而且会发现每次运行程序,输出的结果都不一样,都是一个小于200000的数字.

    原因分析:从字节码层面上很容易就分析出并发失败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中。

    public static void increase();
      Code:
       Stack=2, Locals=0, Args_size=0
       0:   getstatic       #2; //Field race:I
       3:   iconst_1
       4:   iadd
       5:   putstatic       #2; //Field race:I
       8:   return
      LineNumberTable: 
       line 14: 0
       line 15: 8
    
    

    【适合例子】

     volatile boolean shutdownRequested;
        public void shutdown(){
            shutdownRequested = true;
        }
    
        public void doWork(){
            while(!shutdownRequested){
                //do stuff
            }
        }
    
    • 第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致

    3.4 对于long和double型变量的特殊规则

    • Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都具有原子性,但是对于64位的数据类型(long和double)允许非原子性协定.
    • 目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的long和double变量专门声明为volatile

    3.5 原子性、可见性与有序性

    • 原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和double的非原子性协定,读者只要知道这件事情就可以了,无须太过在意这些几乎不会发生的例外情况)。

    • 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。上文在讲解volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

    • 除了volatile之外,Java还有两个关键字能实现可见性,即synchronizedfinal

    • 有序性(Ordering):Java内存模型的有序性在前面讲解volatile时也详细地讨论过
      了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

    • Java语言提供了volatilesynchronized两个关键字来保证线程之间操作的有序性

    3.6 先行发生原则

    【问题】如果Java内存模型中所有的有序性都仅仅靠volatile和synchronized来完成,那么有一些操作将会变得很烦琐,但是我们在编写Java并发代码的时候并没有感觉到这一点呢?
    【答案】这是因为Java语言中有一个“先行发生”(happens-before)的原则

    • 先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值发送了消息调用了方法等。

    下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用

    • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
    • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序
    • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序
    • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
    • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束Thread.isAlive()的返回值等手段检测到线程已经终止执行。
    • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
    • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
    • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

    相关文章

      网友评论

          本文标题:第十二章 Java内存模型与线程

          本文链接:https://www.haomeiwen.com/subject/readottx.html