并发编程 - volatile

159 阅读5分钟

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式. 三大特性分别是原子性 ,可见性,有序性

JMM关于同步规定:

1.线程解锁前,必须把共享变量的值刷新回主内存

2.线程加锁前,必须读取主内存的最新值到自己的工作内存

3.加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成。

volatile特性

1.可见性

通过前面对JMM的介绍,我们知道 各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的. 这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.

class Data{
    volatile int number =0;
    public void assignment(){
        this.number=1;
    }
}

/**
 * 可见性验证
 * 
 */
public class VolatileDemo {
    public static void main(String[] args) {
//        seeOkByVolatile();

    }

    /**
     * volatile可以保证可见性,物理内存值被修改及时通知了其他线程
     */
    private static void seeOkByVolatile() {
        Data data = new Data();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.assignment();
            System.out.println("A线程修改值为"+data.number);
        },"A").start();
        while (data.number==0){

        }
        System.out.println("main线程值"+data.number);
    }
}

2.不保证原子性

/**
 * 不保证原子性测试
 * volatile变量自增运算测试
 *  丢失写值,导致小于200000
 * @author zzm
 */
public class VolatileTest {
    public static volatile int race = 0;

    public static void increase() {
        race++;
    }
    AtomicInteger atomicInteger=new AtomicInteger(0);

    public  void increaseAtimic() {
        atomicInteger.getAndIncrement();
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) throws InterruptedException {
        VolatileTest volatileTest = new VolatileTest();
        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();
                        volatileTest.increaseAtimic();
                    }
                }
            });
            threads[i].start();
        }
        TimeUnit.SECONDS.sleep(1);
        // 等待所有累加线程都结束
        while (Thread.activeCount() > 2)
            Thread.yield();

        System.out.println("非原子性:"+race);
        System.out.println("原子性:"+volatileTest.atomicInteger);
    }
}

3.有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3中

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致. 处理器在进行重新排序是必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

 int a ,b ,x,y=0;
 线程1	线程2
 x=a;	y=b;
 b=1;	a=2;
	
 x=0 y=0	
 如果编译器对这段代码进行执行重排优化后,可能出现下列情况:
 线程1	线程2
 b=1;	a=2;
 x=a;	y=b;
	
 x=2 y=1	
这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程使用的变量能否保持一致是无法确定的.

使用场景:

1.单例模式DCL代码

public class SingletonDemo {

    private static volatile SingletonDemo instance=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 构造方法");
    }

    /**
     * 双重检测机制
     * @return
     */
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 1; i <=10; i++) {
            new Thread(() ->{
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();

        }
    }
}

2 代理模式volatile分析

DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排 原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化. instance=new SingletonDem(); 可以分为以下步骤(伪代码)

memory=allocate();//1.分配对象内存空间 instance(memory);//2.初始化对象 instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null

步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的. memory=allocate();//1.分配对象内存空间 instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完. instance(memory);//2.初始化对象 但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性 所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.

总结