文章
问答
冒泡
JAVA的多线程与高并发(三)volatile

volatile

volatile的作用

首先通过代码了解有无volatile关键字的区别

package com.rongyu.volatiletest;

import java.sql.Time;
import java.util.concurrent.TimeUnit;

public class VolatileDemo {
    volatile boolean running =true;

    void m(){
        System.out.println("m start");
        while(running){}
        System.out.println("m end");
    }

    public static void main(String[] args) {
        VolatileDemo t = new VolatileDemo();
        new Thread(t::m,"t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running = false;
    }
}

当running有volatile修饰的时候,得到结果如下:

m start
m end

当没有volatile修饰的时候得到结果如下:

m start

由此,我们可以推断出volatile关键字的作用:即使一个变量在多个线程间可见.


主线程和t线程都会用到running变量,java原本默认的是在线程t中保留一份running的copy,这样如果主线程对running做的修改,则线程t无法知道,因此会一直在while循环中执行.当running被volatile修饰后,主线程对running做的修改,在t线程中也可以读到,因此会跳出循环

volatile的作用总结:

  • 保证线程可见行

    • MESI

    • 缓存一致性协议

  • 禁止指令重排序

    • DCL单例

    • Double Check Lock

    • Mgr06.java

      • loadfence原语指令

      • storefence原语指令

1.保证线程的可见性

大家都知道java里面是有堆内存的,堆内存是所有线程共享里面的内存,出了共享内存外,每个线程都有自己专属的区域,都有自己的工作内存,如果说在共享内存里有一个变量,某几个线程都需要去访问这个值的时候,会将这个变量copy一份后放到自己的工作空间里面,然后该线程对这个变量做的任何操作都是在自己的工作空间进行改变,堆内存(也就是共享空间中)的值不会发生改变.只有该线程写操作完,写回去的时候,共享空间的变量的值才会发生改变(那么什么时候写回去呢?答:对此变量进行完操作后立刻写回去).因此,什么时候去检查有新的值也不好控制.


在一个线程中变量发生的改变,并不能即使反映到另外一个线程中,这就是线程间的不可见.volatile关键字修饰的变量可以保证一个线程对其改变后,另一个线程立刻可是获得到改变的值

2.禁止指令重新排序

指令重新排序和cpu有关,加了volatile关键字后,cpu每次写都会被线程读到.cpu原本执行指令的时候是一步一步的按顺序执行,后来为了提高效率,现在的cpu会并行地执行指令,指令一执行到一半地时候,指令二可能就已经开始执行了,类似于工厂的流水线,因此,叫做流水线式的执行.在这种新的架构设计基础上,就会要求你的编译器把源码编译完后的指令进行重新排序.因此volatile会降低cpu的效率

dcl单例

单例:保证jvm中永远只有单例类的一个示例.我们顺便熟悉一下java设计模式之单例模式


简单创建一个单例:

package com.rongyu.volatiletest;

/**
 * 饿汉式,jvm保证线程安全
 * 缺点:不管用不用,先创建实例
 */
public class SingleDemoOne {

    private static  final SingleDemoOne INSTANCE = new SingleDemoOne();

    //构造方法私有,不允许别人new,只有我自己能new我自己,其他类想用我需要调用getInstance,这样就保证了jvm中只有一个我
    private SingleDemoOne(){}

    public static SingleDemoOne getInstance(){return INSTANCE;}

    public void m(){System.out.println("m");}

    public static void main(String[] args) {
        SingleDemoOne m1 = SingleDemoOne.getInstance();
        SingleDemoOne m2 = SingleDemoOne.getInstance();
        System.out.println(m2==m1);
    }
}

结果为true;


当然饿汉式是最简单的创建单例的方法,缺点是不管我用不用这个对象,都会初始化这个对象到jvm中.下面看一下饱汉式(懒汉式)单例:

package com.rongyu.volatiletest;

/**
 * 饱汉式,
 * 使用时创建实例
 */
public class SingleDemoTwo {


    private static SingleDemoTwo singleDemoTwo = null;

    private SingleDemoTwo() {
    }

    public static SingleDemoTwo getInstance() {
        if (singleDemoTwo == null) {
            singleDemoTwo = new SingleDemoTwo();
        }
        return singleDemoTwo;
    }
    
    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        SingleDemoTwo m1 = SingleDemoTwo.getInstance();
        SingleDemoTwo m2 = SingleDemoTwo.getInstance();
        System.out.println(m2 == m1);

        for (int i = 0; i < 100; i++) {
            new Thread(() ->      System.out.println(SingleDemoTwo.getInstance().hashCode())).start();
        }
    }
}

但是,由于是使用的时候才创建,因此这种懒汉式的单例是非线程安全的.需要使用synchronized关键字.


但是因为是单例,其实只有第一次初始化的时候才会发生线程不安全的问题,如果直接加上synchronized,会浪费性能.所以理论上我们可以先判断getInstance()方法中singleDemoTwo是否为空,为空再加synchronized.不过这样还是有问题(比如此种情况:线程一判断为空还没执行下面过程,线程二也执行过来了,此时线程二拿到的也会为空.所以线程还是不安全的).当然我们也可以直接使用synchronized修饰getInstance()方法,但是实际需要同步的只有创建实例的部分,因此还是会浪费性能.


这时候,我们就需要用到volatile来解决这个问题.叫做双重检查锁,双重检查单例.


代码如下:

package com.rongyu.volatiletest;

/**
 * 饱汉式,
 * 使用时创建实例
 */
public class SingleDemoThree {


    private static volatile SingleDemoThree singleDemoTwo = null;

    private SingleDemoThree() {
    }

    public static SingleDemoThree getInstance() {
        if (singleDemoTwo == null) {
            synchronized (SingleDemoThree.class) {
                singleDemoTwo = new SingleDemoThree();
            }
        }
        return singleDemoTwo;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        SingleDemoThree m1 = SingleDemoThree.getInstance();
        SingleDemoThree m2 = SingleDemoThree.getInstance();
        System.out.println(m2 == m1);

        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingleDemoThree.getInstance().hashCode())).start();
        }
    }
}

分析:线程一执行,判断为空值,然后进行下面初始化.同时,线程二在线程一判断的时候也拿了instance进行判空,结果也是空(因为线程一还没执行到初始化的地方),线程二会停在执行if (singleDemoTwo == null) 后因为没有锁而停住.停住之后第一个线程执行完了初始化的过程,释放掉锁,同时因为volatile的存在,线程二拿到的instance已经不是null了,线程二在获取到锁后,会再次执行if (singleDemoTwo == null).因此实现线程安全


由此也可以看出volatile和synchronized作用上的不同

volatile和synchronized 对比

  • synchronized:锁的是对象不是代码,锁方法锁的是this,锁static方法锁的是class.

  • volatile:保证线程可见性,同时防止指令重新排序

多线程

关于作者

BenbobaBigKing
获得点赞
文章被阅读