一,谈谈JAVA线程的运行内存模型【JAVA内存模型】

 2019-12-10 15:48  阅读(866)
文章分类:Java Core

程序,纠集到底就是对内存数据的操作,并把计算的结果持久话. 争议

JAVA中执行的最小单位是线程.JVM实现了各个CPU,操作系统等的差异. 线程的运行模型最终可以抽象的看成如下:

20191210001315\_1.png

每一条线程都有自己的work memory, 而且共享一个main memory.

JMM的主要问题如下:

原子性,原子级别的操作,每个线程运行时是相互独立,包括里面未声明为volatile的变量都是独立一份,但会进行work memory 和 main memory的同步;

可见性, 线程间的通讯. 即主内存的变量可见的,把值从work memory同步到main memory 进行线程间的通讯,通过synchronize或者volatile可靠性;

有序性,这里主要针对读和写及同步主内存的的有序, 线程的操作一般是

read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容

其中read and load和store and write 都是多次运行,取决于JVM

1,每一条线程中运行的变量只是从main memory 的copy,存放于缓存之中

常见的并发问题, 在内存中各自加,未同步到主内存,比如

package org.benson.threadgroup;

    import java.util.concurrent.CountDownLatch;
    /**
     * TODO share the thread memory operation 
     * @author Benson QQ 107966750
     *
     */
    class Data{
        public static int count=0;
    }
    public class TreadTest extends Thread{
        private CountDownLatch countDCH=null;
        public TreadTest(CountDownLatch countDCH) {
            this.countDCH=countDCH;
        }
        @Override
        public void run() {
            for(int i=0;i<10000;i++)
    //            synchronized (Data.class) {
                    Data.count++;
    //            }
            countDCH.countDown();
        }

        public static void main(String[] args) throws InterruptedException {
            final int threadSize=10;
            CountDownLatch countDCH=new CountDownLatch(threadSize);
            for(int i=0;i<threadSize;i++)
            new Thread(new TreadTest(countDCH)).start();
            countDCH.await();
            System.out.println(Data.count);
        }
    }

输出

91660

这里由于读写没及时有序的同步到主内存造成,小于预期值100000,释放同步锁后正常

当第二线程运行完毕时始终输出正确计算值,原理看synchronize关键词

2,JAVA调用代码的次序是无序性的.(如DCL等问题)

一,bad实践,依靠变量线程通讯,这个不容易重现,但执行了N遍还是正确的,取决于JVM的心情问题,代码如下

package org.benson.threadgroup;
    /**
     * TODO share load variable sort
     * @author Benson QQ107966750
     * 
     */
    class Process4SortA{
        public int variableA=0;
        public Process4SortA() {
            variableA=100;
        }
    }
    class Process4SortB extends Thread{
        public boolean startFlag=false;
        public int variableA;
        public void test4SortRun(){
            //do something another stuff
            variableA=100;
            startFlag=true; //maybe will process this code before the variableA=100
        }
        @Override
        public void run() {
            while(true){
                if(startFlag){
                System.out.println(variableA);
                //do something another stuff
                break;
                }
            }
        }
    }
    public class TreadTestSort {
        public static void main(String[] args) throws Exception {
            Process4SortB proB=new Process4SortB();
            proB.start();
            proB.test4SortRun();
        }
    }

代码输出可能会等于0. 因为在方法test4SortRun()里,执行的顺序是不一定的

二,bad实践,DCL的问题,这里引出最常见的,一个double checked load(DCL) 问题,也是由于无序造成的

class Foo { 
    private Resource res = null ; 
    public Resource getResource() { 
    if (res == null ) 
    res = new Resource(); 
    return res; 
    } 
    }

相关资料很多,其实可以把

res = new Resource();

看成

Resource()

res = Resource地址引用;

这个是正确执行顺序,因为无序的原因可能是

res = Resource地址引用;

Resource()

所以其他得到了res!=null,就执行了,构造函数根本没执行完

三,bad实践,单例模式

这个和DCL类似
java Singleton 几种方式解析(实在找不到原帖了,BS下转贴不贴出地址的)

当然最后一个volatile的可以,因为volatile每次都是读到主内存中最新的值

相关关键词

synchronize
1,获取并挂起monitor,
2,从main memory copy最新的值
3,执行
4,从work memory copy最新的值到main memory
5,释放monitor

final

不可变,只能在初始化时赋值,

但非静态可以在构造方法中赋值,应用时时注意顺序,如果下面这样调用 final变量也成了可变了

用如下代码证明

package org.benson;
/**
* TODO to share the java load class sort
* @author Benson QQ 107966750
*
*/
class Base{
public Base() {
System.out.println(“init base,the variableA is “+((Child)this).variableA);
}
public void display(){
System.out.println(“display in base,the variableA is “+((Child)this).variableA);
}
}
class Child extends Base{
final int variableA;
public Child(){
variableA=100;
System.out.println(“init child,the variableA is “+this.variableA);
this.display();
}
}
public class Test4Sort {
public static void main(String[] args) {
new Child();
}
}

结果如下

init base,the variableA is 0
init child,the variableA is 100
display in base,the variableA is 100

这样final值也成了可变了

JAVA的初始化顺序 base and child class adress,static–>base class variable–>base class constructor–>child class variable–>child class constructor

volatile

在线程中,读和写都是原子性的,volatile的最佳实践就是用synchronize加锁写(或者用不依赖当前volatile的值的写),然后读可以不用锁,可以保证读到最新值,类似一个共享锁的实现

1,保证的是线程的可见性,即每次得到的都是最新值,显然写入是不保证的,所以不能做计数器

2,如果一个线程里读和写同时发生,它可以保证写先于读,比如 a=new instance(); 可以保证先把instance()初始化完毕后赋值给a,返回完整的instance

如一个线程的写入远少于,一个线程不停的读取,就可以对变量进行volatile了,能保证读到的是最新值(但写入必须不依赖当前值),这种情况可以考虑和synchronize同时用,参考从ConcurrentHashMap类学习高并发程序的设计思路【深入JDK源码】

参考资料

双重检查锁定及单例模式

Java内存模型(找不到原帖)

Java多线程发展简史

Java 多线程与并发编程专题

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 一,谈谈JAVA线程的运行内存模型【JAVA内存模型】

相关推荐