volatile 是什么?指令重排是什么?JMM模型三大特性?DCL双端检锁?

2 篇文章 0 订阅

volatile 可以理解为乞丐版的synchronized,唯一差的一点就是原子性!

volatile三大特性:

1.可见性


顺带一提

数据一般存储在内存中,如nosql的redis,而像mysql这样的数据则存在硬盘,CPU则只是负责运算。

所以数据的读取熟读为:

硬盘<内存<CPU


简单说一下线程工作原理,先去内存读取数据,然后将数据获取复制到线程的工作空间,将数据处理完毕后,才会重新写入内存,内存收到新写入的数据后,会通知其他的线程,数据变了!你们要拷贝数据,记得拷贝新数据哦!这就是volatile的可见性。

例:
首先一个普通的数据类,其中有一个int number=0,一个addint()方法,将自身的数据改为60。

class MyData{
	int number=0;
    public void addInt(){
        this.number=60;
    }
}

然后写个mian方法,这里一个线程中先睡他个3秒,等待3秒后,启用addInt()方法,将number赋值为60。
下面有个while循环,一开始number的值为0会一直循环,当3秒后,线程AAA将值改变后,number不等于0,此时理当跳出循环。

public class VolatileVisibility {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try {
            TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addInt();
            System.out.println(Thread.currentThread().getName()+"\t update a:"+myData.number);
        },"AAA").start();

        while (myData.number==0){}

        System.out.println("main come in");
	}
}

但结果却并不然,因为默认是没有可见性的,所以mian线程并不知道数据已经被修改,就会在陷入死循环。

此时只需要在一开始MyData中的number属性,用volatile修饰,就能保证他的可见性,这个时候AAA线程修改值以后,内存就会通知mian线程,值改了,那么最后就能输出mian come in,同学们自己去打打代码,练练手!

这就是volatile的神奇之处

2.不保证原子性

前面说了,volatile有可见性,但是不保证原子性就是它乞丐版的根本。


要知道JMM模型的特性有以下几点:

1.可见性(Visibility)
2.原子性(Atomicity)
3.有序性(Uniformity)


什么是原子性,所以原子就是最小的单位,不可再拆分,要保持其完整性,也就是这个线程不可被其他线程加塞或者分割,整个线程的任务要么全部成功,要么全部失败。

但是前面也说了volatile不保证原子性,为什么这么说呢?上代码

class MyData{
    volatile int number=0;
    public void addPlusPlus(){
        this.number++;
    }
}

还是一样的MyData,不过这次我们改为addPlusPlus每次都自增1。
实验方法是:使用20个线程,每个线程修改1000次,如果正常走的话,应该是有20000个!来看看结果如何?

public class StudyVolatile {
    public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 1; i <=20; i++) {
            new Thread(()->{
                for (int j = 1; j <=1000 ; j++) {
                    myData.addPlusPlus();
                }
            }).start();
        }
        while (Thread.activeCount()>2){//
            Thread.yield();
        }
        System.out.println(myData.number);
    }
}

可以看到无论怎么输出,基本都比20000小

18832

Process finished with exit code 0
---
19234

Process finished with exit code 0
---
19110

Process finished with exit code 0

这就是因为volatile不能保持原子性,导致重复读,例如有3个线程同时读到一个数字0,A、B、C三个线程都拿到自己的工作空间,写好了,A放到内存去了,B线程刚才被加塞了一下,也继续写下去了,内存还没来得及通知新数据,所以就会造成这样的结果,重复读!

如何解决原子性问题

在不用synchronized情况下,我们怎么解决呢?

既然volatile不保证原子性,我们可以使用JUC中的原子性数据类型,如数字的AtomicInteger()类。

class MyData{
    volatile int number=0;
     AtomicInteger atomicInteger = new AtomicInteger();
	  public void addInt(){
	       this.number=60;
	   }
	   public void addPlusPlus(){
	       this.number++;
	   }
	   public void addAtomicPlusPlus(){
	       this.atomicInteger.getAndIncrement();
	   }
	}

public class StudyVolatile {
   public static void main(String[] args) {
       MyData myData = new MyData();
       for (int i = 1; i <=20; i++) {
           new Thread(()->{
               for (int j = 1; j <=1000 ; j++) {
                   myData.addPlusPlus();
                   myData.addAtomicPlusPlus();
               }
           }).start();
       }
       while (Thread.activeCount()>2){
           Thread.yield();
       }
       System.out.println(myData.number);
       System.out.println(myData.atomicInteger);
   }
 }

这个时候,就可以看到原子性的int怎么操作都始终为20000

18100
20000

Process finished with exit code 0

那既然这样为什么还要用volatile呢?最后再说,先来看看volatile的最后一个特性

3.禁止指令重排

当计算机在执行程序时,为了提高性能,有可能对指令进行重排。

源代码->编译器优化重排->指令并行的重排->内存系统的重排->最终执行的指令

在单线程的情况下,无论如何重排都不影响整体的语义,保证执行结果一致。

指令重排需要尊崇 数据之间的依赖性。

但是在多线程的情况下,就有可能因为指令重排而导致结果无法确定。

如下:

x=1;//语句1
y=10;//语句2
x=x+5;//语句3
y=x+10;//语句4

指令可以重排为2134,这样并不影响输出结果。

也可以改为1324,这里同样不影响。

但是并不能将4提到前面去,因为这样就会印象最终的结果。

但是在多线程的情况下,我们就无法确定了

public class Demo2 {
    int a=0;
    boolean flag=false;

    public void method1(){
        a=1;
        flag=true;
    }
    public void method2(){
        if (flag){
            a+=5;
        }
    }
}

当多线程的情况下,指令重排就也可能导致method1的flag=true先执行,再执行a=1,这样method2中的a就只会等于5。这时就可以将目标变量用volatile修饰,让其前后都加上一层屏障,导致无法被重排!

什么情况下用volatile

单例模式

我们先写个普通的单例模式,如下:

public class SingleDemo {
    private static SingleDemo instance=null;
    private SingleDemo(){
        System.out.println(Thread.currentThread().getName()+" \t new SingleDemo");
    }
    public static SingleDemo getInstance(){
        if (instance==null){
                instance = new SingleDemo();
            }
        }
        return instance;
    }

    public static void main(String[] args) {

        System.out.println(SingleDemo.getInstance()==SingleDemo.getInstance());
        System.out.println(SingleDemo.getInstance()==SingleDemo.getInstance());
        System.out.println(SingleDemo.getInstance()==SingleDemo.getInstance());
    }
}

但是在多线程并行的情况下,就有可能会导致这种问题

public class SingleDemo {
    private static SingleDemo instance=null;
    private SingleDemo(){
        System.out.println(Thread.currentThread().getName()+" \t new SingleDemo");
    }
    public static SingleDemo getInstance(){
        if (instance==null){
            instance = new SingleDemo();
        }
        return instance;
    }

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

多个线程抢着去新建对象,这样就违背了我们的单例模式原则,怎么解决了?可以用DCL控制

//DCL(Double Check Lock 双端检锁机制)
public static SingleDemo getInstance(){
        if (instance==null){//在锁之前判断一次
            synchronized (SingleDemo.class){
                if (instance==null)//锁之后再判断一次
                    instance = new SingleDemo();
            }
        }
        return instance;
    }

两次判断就能保证这个对象不为空,这样每次在创建时,就只有一个线程能进行创建。

但是讲到这里,就无需用到volatile了?再往深了讲,创建对象其实也是要分成几个对象的。

1.分配地址
2.将对象放入地址
3.获取地址引用

但也有可能指令重排后,变成刚分配地址,线程就跑来获取地址引用了,这个时候有地址了,表面上是不为null了,但是对象还没住进去呢!可以说是有名无实了!也就是说你找到他家了,但是他只是签了合同,还没搬进来!你就要来这里找他,这显然不可能,所以我们需要保证他住进来了,我们再去找他!就可以用volatile修饰他,避免流程走错了!

    private static volatile SingleDemo instance=null;

学单例模式,有6种形态,掌握这一个,再去融会贯通!就够了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值