【JUC多线程与高并发】volatile详解

博客地址(点击即可访问)github源码地址
深刻理解JMM(JAVA内存模型)https://github.com/zz1044063894/JMMprojcet
volatile详解https://github.com/zz1044063894/volatile
线程基础,java并发程序基础https://github.com/zz1044063894/thread-base
线程进阶,JDK并发包https://github.com/zz1044063894/JDK-concurrency
多线程进阶,性能优化之锁优化https://github.com/zz1044063894/lock-optimization
线程进阶,性能优化之无锁https://github.com/zz1044063894/no-lock

什么是volatile

我们使用翻译软件翻译一下volatile,会发现它有以下几个意思:易变的;无定性的;无常性的;可能急剧波动的;不稳定的;易恶化的;易挥发的;易发散的。这也正式使用volatile关键字的语义。

当你使用volatile去申明个变量时,就等于告诉了虚拟机,这个变量极有可能会被其他程序或者线程修改。为了确保这个变量被修改后,应用程序范围内所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊手段,保证这个变量的可见性等特点
比如,根据编译器的优化规则,如果不使用volatile申明变量,这个变量被修改后其他线程可能并不会被通知到,甚至在别的线程中,看到变量的修改程序都是反的。但是使用volatile,虚拟机就会谨慎的处理这种情况

volatile与JMM(volatile的特点)

在上一篇博客点击跳转→深刻理解JMM(JAVA内存模型)中我们讲到java内存模型主要有3个特点:原子性、可见性、有序性,而volatile的特点与JMM几乎不同,保证可见性,不保证原子性,禁止指令重排(有序性)

保证可见性

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

禁止指令重排

案例1:

int a = 5;//语句1
int b = 2;//语句2
a = a+2;//语句3
b = a*a;//语句4

结果与分析

  • 单线程
    单线程下的运行结果必然是1234,因为要遵循并行状态下的有序性。
  • 多线程
    如果上述4条语句在4个不同的线程下指令重排后可能会出现什么情况呢。
序号结果
1234
2134
1324

这样就会有同学问了,3,4能够在指令重排后变成第一条语句吗,答案是不可以的。以为在上一篇博客中提到,数据要遵循数据依赖,在代码编译成汇编指令时,要遵循一定的指令执行规则,就像我们代码的java代码中,没有定义变量就直接使用是会编译报错的,汇编指令在进行重排序的时候避免了这样的情况。

不保证原子性

上一篇博客中,我们介绍了一个MultiThreadLong的案例,相信我们任何人都不愿意写出这样的程序。在单线程工作中如果我们给变量t增加关键字volatile,上述的问题都能解决。从上面那个案例,我们可以看到,volatile对于保证操作的原子性是有非常大的保证,但是需要注意的是volatile并不能代替锁,它也无法保证符合操作的原子性,比如下面的例子:

案例1:

public class PlusTask implements Runnable {
    private volatile static int i = 0;

    @Override
    public void run() {
        for (int k = 0; k < 10000; k++) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int j = 0; j < 10; j++) {
            threads[j] = new Thread(new PlusTask());
            threads[j].start();
        }
        for (int j = 0; j < 10; j++) {
            threads[j].join();
        }

        System.out.println(i);
    }
}

案例结果与分析

序号结果
93496
70701
89536

结果分析:
如何执行上述代码,如果第6行i++是原子性的,那么最终的值应该是100000(10个线程各累加100000次)。但实际上,上述的输出总是会小于100000。证明了上述代码没有保证原子性。
接下请看案例2:

案例2

当然我们并不希望写出如上的代码,那么这个问题如何解决呢。首先大家想到的肯定是使用锁,毫无疑问锁肯定能解决这个问题,但是是不是有点大材小用了呢?接下来看看这个解决办法吧。请看代码

public class AtomicPlusTask implements Runnable{
    private  static AtomicInteger  atomicInteger = new AtomicInteger(0);

    @Override
    public void run() {
        for (int k = 0; k < 10000; k++) {
            atomicInteger.getAndIncrement();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int j = 0; j < 10; j++) {
            threads[j] = new Thread(new AtomicPlusTask());
            threads[j].start();
        }
        for (int j = 0; j < 10; j++) {
            threads[j].join();
        }

        System.out.println(atomicInteger);
    }
}

结果与分析

序号结果
100000
100000
100000

结果分析:Atomic即为原子性的,AtomicInteger是遵循原子性的int类型,除此之外还有AtomicLong长整型原子类,AtomicBoolean: 布尔型原子类,AtomicReferenceArray: 引用类型数组原子类等。这些类型都是在并发变成中出现的,他们常用来解决一些并发问题,如上述例子。

volatile与单例模式

单线程下的单例模式

示例1

public class MySingleton1 {
    private static MySingleton1 instance = null;

    private MySingleton1() {
        System.out.println(Thread.currentThread().getName() + " 单例模式构造方法");
    }

    public static MySingleton1 getInstance() {
        if (instance == null) {
            instance = new MySingleton1();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("*******************单线程:");
        System.out.println(MySingleton1.getInstance() == MySingleton1.getInstance());
        System.out.println(MySingleton1.getInstance() == MySingleton1.getInstance());
        System.out.println(MySingleton1.getInstance() == MySingleton1.getInstance());
        System.out.println(MySingleton1.getInstance() == MySingleton1.getInstance());

        System.out.println("*******************多线程:");
        for(int i=0;i<10;i++){
            new Thread(()->{
                MySingleton1.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

运行结果与分析

注意:运行时需要把非单线程和多线程的注释掉,否则会影响运行结果

*******************单线程:
main 单例模式构造方法
true
true
true
true
*******************多线程:
1 单例模式构造方法
0 单例模式构造方法

多线程下的单例模式

代码

public class MySingleton2 {
    private static MySingleton2 instance = null;

    private MySingleton2() {
        System.out.println(Thread.currentThread().getName() + " 单例模式构造方法");
    }

    //DLC(Double Lock Check 双端检索机制)
    public static MySingleton2 getInstance() {
        if (instance == null) {
            synchronized (MySingleton2.class) {
                if (instance == null) {
                    instance = new MySingleton2();
                }
            }
        }
        return instance;
    }
	//普通加锁机制
    /*public static synchronized MySingleton2 getInstance() {
        if (instance == null) {

            instance = new MySingleton2();
            
        }
        return instance;
    }*/
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                MySingleton2.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

运行结果与分析

0 单例模式构造方法

似乎看起来DCL和普通加锁机制运行的结果是一样的,但是他们本质上还是有很大的区别的。
DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化
instance = new SingletonDemo();可以分为以下3步完成
1.分配对象内存空间
2.初始化对象
3.设置instance指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中没有改变,因此这
种重排优化是允许的。

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值