zoukankan      html  css  js  c++  java
  • 深入理解java虚拟机(6)---内存模型与线程 & Volatile

    其实关于线程的使用,之前已经写过博客讲解过这部分的内容:

    http://www.cnblogs.com/deman/category/621531.html

    JVM里面关于多线程的部分,主要是多线程是如何实现的,以及高效并发。

    1.Java内存模型

    CPU在运行的时候,不可能把所有的东西都放在寄存器里面,所有需要使用内存。这个内存就是我们知道的那个内存。

    但是实际情况是,内存的读写速度于CPU的指令操作差了几个数量级。所以为了跟高效的使用CPU,就有高速缓存这么一个东西。

    以下是Intel 酷睿i7 6700K参数:

    三级缓存8MB

    百度以下就知道这个“三级缓存”是个神马东西。

    而java的内存模型与物理结构非常相识,有一个主内存,对应我们计算机的内存,还有每个线程都有一个工作内存,对应于高速缓存。

    可以看到,每个java线程都有自己独立的内存。

    这也就解释了,为什么不同线程,如果不同步的话,变量就会有并发的问题。

    这里关于工作内存和主内存的拷贝问题,是由JVM实现的,并不是正真意义上的内存复制。

    2.内存间操作

    1)lock,作用于主内存变量,把一个变量标记为线程独占。

    2)unlock,与lock正相反。

    3)read,作用于主内存变量,它把一个变量从主内存传输到工作内存中。

    4)load,作用于工作内存变量,把从read里面获取的变量放入工作内存的变量副本中。

    5)use,作用于工作内存变量,把变量的值传递给执行引擎。

    6)assign,作用于工作内存变量,把执行引擎的值 复制给工作内存变量。同use相反

    7)store,作用于工作内存变量,把工作内存变量传输到主内存中。

    8)write,作用于主内存变量,把store获取的值,写入到住内存中的变量。

    read & load, store & write成对出现。

    还有其他一些规则,目的就是保证内存变量的 操作合理,有序。

    3.并发编程的三个概念

    1)原子性

    计一个操作要么全部执行,要么不执行,不能被打断。

    jvm通过lock & unlock指令来保证代码的原子性。反映到java代码就是synchronized.

    2)可见性

    可见性是指当一个程序修改变量以后,其他程序可以立即获得这个修改的值。

    3)有序性

    JVM在编译java代码,优化的时候,会重现排布java代码的顺序。但是会保证结果时候java代码的顺序结果一致的。

    public Runnable mRun1 = new Runnable() {
    @Override
    public void run() {
    int a = readFileName();
    writeFile(a);
    initialized = true;
    }
    };

    上面readFileName 和initialized = true;没有必然关系,所以在实际执行的时候,可能会先执行initialized = true;

    对于这个线程内的结果没有影响。

    但是如果是多线程的情况下:

        public Runnable mRun2 = new Runnable() {
            @Override
            public void run() {
                while (!initialized)
                {
                    try {
                        TraceLog.i("sleep");
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                doSomeThing(context);
            }
        };

    initialized = true的执行顺序对线程2的结果有直接的影响。所有有序性在这种情况下,需要保证。

    一般java里面用synchronized就可以保证。但是过多的synchronized会对性能有很大的损失。

    4.volatile关键字

    volatile关键字修饰后的变量.有2个作用:

    1)用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新.

    当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.

    但是volatile 不能保证线程是安全的,因为java里面的运算并非原子操作。2)volatile还有一个特性就是保证指令不重新排序。现在编译器,为了优化代码,都会重新排序指令。如果在多个线程里面,就会有很大的问题。

    但是指令重排是JVM在它认为合理的情况下做的,所以很难模拟出这一情况。

        boolean aBoolean = false;
        public Runnable mRun1 = new Runnable() {
            @Override
            public void run() {
                aBoolean = false;
                while (!aBoolean)
                {
                    doSomeThing();
                }
            }
        };
    
        public Runnable mRun2 = new Runnable() {
            @Override
            public void run() {
                aBoolean = true;
            }
        };

    只是2个线程的例子,线程2用来关闭线程1.一般情况下,它会运行良好,但是有小概率情况下,会有问题。

    aBoolean 在赋值为true的时候,没有立刻被同步到主内存,而这时候线程1的工作内存aBoolean 的拷贝是false。

    所以会陷入死循环。

    volatile关键字就可以避免这种情况的发生。

    1)当aBoolean = true;发生后,线程2会立即把aBoolean 的值更新到主内存。

    2)线程1在使用到aBoolean 是,会首先到主内存重新获取新的值。然后更新工作内存中的值,这个时候 aBoolean就是true了,循环退出。

    5.volatile 保证原子操作吗?

    volatile不能保证线程是安全的。

    package com.joyfulmath.jvmexample.multithread;
    
    import com.joyfulmath.jvmexample.TraceLog;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * @author deman.lu
     * @version on 2016-05-26 14:34
     */
    public class VolatileTest2 {
        public volatile int inc = 0;
        static CountDownLatch countDownLatch = new CountDownLatch(10);
        public void increase() {
            inc++;
        }
    
        public static void main() {
            TraceLog.i();
            final VolatileTest2 test = new VolatileTest2();
            for(int i=0;i<200;i++){
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        for(int j=0;j<50;j++)
                            test.increase();
    
                        countDownLatch.countDown();
    //                    TraceLog.i(String.valueOf(Thread.currentThread().getId()));
                    }
                }.start();
            }
    
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(test.inc);
            TraceLog.i(String.valueOf(test.inc));
        }
    }
    05-26 14:48:06.060 15209-15209/com.joyfulmath.jvmexample I/System.out: 9950
    05-26 14:48:06.061 15209-15209/com.joyfulmath.jvmexample I/VolatileTest2: main: 9950 [at (VolatileTest2.java:41)]

    结果并不是10000,原因就是 自增函数不是原子操作,而Volatile只能保证数值是更新到住内存,但是,当线程1执行过程中假设inc=5,线程2可能已经获取了inc的值。

    这个时候,线程1,++以后变为6,线程2也是6,而且因为主内存的值 & 线程2的值一致,就不会触发其他线程无效的情况,所以线程3取到的值,还是6.所有这个数值的结果是无法确认的,但是<10000.

    But, 我在android23下编译,发现一直是10000.不清楚原因???

    6.volatile的有序性

    volatile只能保证部分有序性,比如说:

    1 volatile boolean initialized = false;
    2         public void run() {
    3             context = readFileName();
    4             writeFile(context);
    5             initialized = true;
    6             play();
    7             Drawable();
    8         }

    上面,3,4两行语句顺序是乱序的,6,7也是,但是5 一定在3,4之后运行。 也就是5的执行为止不变,而且,3,4 不能和6,7互换执行顺序。这就是volatile有限的有序性。

    参考:

    http://www.cnblogs.com/dolphin0520/p/3920373.html

    《深入理解java虚拟机》周志明

  • 相关阅读:
    造轮子 | 怎样设计一个面向协议的 iOS 网络请求库
    win7 激活码 秘钥
    python-pptx
    pycharm
    itop 环境
    Ubuntu上安装MongoDB(译)
    python之fabric(二):执行模式(转)
    python之fabric(一):环境env
    Windows下pip安装包报错:Microsoft Visual C++ 9.0 is required Unable to find vcvarsall.bat
    vagrant系列教程(四):vagrant搭建redis与redis的监控程序redis-stat(转)
  • 原文地址:https://www.cnblogs.com/deman/p/5531321.html
Copyright © 2011-2022 走看看