首页 > 编程语言 > 详细

java基础----volatile

时间:2020-09-08 15:09:28      阅读:50      评论:0      收藏:0      [点我收藏+]

一.volatile是什么

  如果用一句话概括volatile的话,那volatile其实就是java虚拟机提供的轻量级的同步机制。它具有一下三个特点:

  1.保证可见性

  2.不保证原子性(因为不保证原子性,所以他是轻量级的)

  3.禁止指令重排

 

二.保证可见性

  首先,我们先看看下面的代码

import java.util.concurrent.TimeUnit;

class MyData{
    //int testData = 0;
    volatile int testData = 0;

    public void addTo60(){
        this.testData = 60;
    }
}

public class volatileDemo {
    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.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.testData);
        }, "test").start();

        while (myData.testData == 0)
        {
            //假如不使用volatile修饰变量
            //这里main线程会一直在这里等待
            //因为工作线程修改了变量,却不能对main线程可见,main线程会一直拿着初始值的副本,也就是0
        }

        System.out.println(Thread.currentThread().getName() + "\t mission is over");
    }
}

关于volatile是怎么实现可见性的,可以总结为以下两点:

  1.从主内存到工作内存<读>:每次使用变量前  先从主内存中刷新最新的值到工作内存,用于保证能看见其他现场对变量修改的最新值。

  2.从工作内存到主内存<写>:每次修改变量后必须立刻同步到主内存中,用于保证其他线程可以看到自己对变量的修改。(因为cpu比内存的速度要快很多,因此这中间有一个高速缓存的概念,一般修改后的变量都是先存进高速缓存,然后再刷新到主内存中)

  3.指令重排序:保证代码的执行顺序和程序的执行顺序一致。(并发环境下 代码的执行顺序与程序的执行顺序有时并不一致,会出现串行的现象固有指令重排序优化一说。JAVA1.5之后彻底修复了这个BUG在用volatile变量的时)

 

三.不保证原子性

  volatile有个比较不好的地方就是它不保证原子性,按照惯例,下面还是先上一段代码

import java.util.concurrent.TimeUnit;

class MyData{
    volatile int testData = 0;

    public void add(){
        this.testData ++;
    }
}

public class volatileDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 0; i < 20; i++){
            new Thread(() -> {
                for (int j = 0; j < 1000; j++){
                    myData.add();
                }
            },"test"+i).start();
        }

     //后台默认有main线程和gc线程,因此这里选择当线程数量大于2时候,main线程退出暂停。 while (Thread.activeCount() > 2){ Thread.yield(); }
     //这里每次运行出来的结果都是不同的 System.out.println(Thread.currentThread().getName() + "\t final number is " + myData.testData); } }

   上述代码运行出来的结果不是我们想要的结果,导致这种情况出现的原因是,在并发编程中,可能存到两个线程同时去主内存中拿到变量,然后进行计算操作,例如线程a、b,同时拿到主内存中的变量x1到自己的工作内存里面,然后计算后得出同样的x2,当要把x2写回主内存时,其中一个会将数据刷新到主内存中,而另一个线程处于挂起状态,当前一个线程写入操作完成后,后一个线程被唤醒,在还没获取变量最新值的时候,立即进行写入操作(写覆盖),这时候主内存中的变量被刷新了两次,但是它的数值只增加了1,因为两个线程算出来的结果时一样的,这时候就会导致变量最后自增的结果不是我们想要的结果。

  那么我们如何去解决这个问题呢?其实很简单,juc包里面有一个atomic类的数据,我们使用它来作为我们操作的对象就可以了。

import java.util.concurrent.atomic.AtomicInteger;

class MyData{
    volatile int testData = 0;

    public void add(){
        this.testData ++;
    }

    volatile AtomicInteger atomicInteger = new AtomicInteger();

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

public class volatileDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 0; i < 20; i++){
            new Thread(() -> {
                for (int j = 0; j < 1000; j++){
                    myData.add();
                }
            },"test"+i).start();
        }

        for (int i = 0; i < 20; i++){
            new Thread(() -> {
                for (int j = 0; j < 1000; j++){
                    myData.addAtom();
                }
            },"test-atomic"+i).start();
        }

        //后台默认有main线程和gc线程,因此这里选择当线程数量大于2时候,main线程退出暂停。
        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        //这里每次运行出来的结果都是不同的
        System.out.println(Thread.currentThread().getName() + "\t final number is " + myData.testData);
        System.out.println(Thread.currentThread().getName() + "\t final number is " + myData.atomicInteger);
    }
}

  可是为什么使用atomic类的对象就可以解决原子性的问题呢?其实atomic是通过CAS来保证他的原子性的。

  那,CAS又是什么呢?简单来说,CAS是compareAndSwap的缩写,意思是对比和交换。

  java中CAS操作依赖于Unsafe类,Unsafe类所有方法都是native的,直接调用操作系统底层资源执行相应任务,它可以像C一样操作内存指针,是非线程安全的。

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                   
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); 
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

  而atomicInteger在更新他的value的时候,就是调用了unsafe类中的compareAndSwapInt方法去更新的。

  举个例子,在线程需要对一个变量进行写操作的时候,会先对比这个变量是否符合预期值,如果符合,则会进行写操作,如果不符合,代表有其他线程对这个变量进行了修改,则获取变量当前值作为最新值,返回重新进行计算操作,依次循环,知道在对比的时候符合预期值。例如当前变量x1=1,同时有线程a,b过来获取变量,并进行+1操作,此时线程a计算完毕,对比主内存中变量是否为1,假如是,就将结果2写进主内存中,此时线程b也计算完毕了,对比主内存中的变量值,发现2!=1,意思是有其他线程已经对这个变量进行修改了,就会把2拿回去,重新进行+1的操作,然后再次对比。

 

java基础----volatile

原文:https://www.cnblogs.com/QicongLiang/p/13627591.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!