JUC之volatile学习理解

前言

Java util Concurrent 简称JUC,是javaEE里面很重要的一个知识点,下面学习一下juc里的关键词volatile。

Volatile的特点

1、保证可见性
2、不保证原子性
3、禁止指令重排

一、保证可见性

多线程并发编程时,程序的可见性是指,一个线程对资源进行变更后,立即写回主内存,并通知其他线程。

主内存和工作内存

java在运行时,对象信息都是存储在主内存当中,每个线程要对对象信息进行操作时,将会在工作内存中创建主内存里对象信息的一个副本,然后对对象进行操作,操作完成后,再将对象信息写回主内存,每个线程获取到数据操作后,其他线程并不知道,需要保证属性的可见性,才能减少并发编程中出现一些问题。

举例说明:

class MyTestData {
    public int a = 0;

    public void addData() {
        a++;
    }
}
public class TestConcurrent {
    public static void main(String[] args) {
        MyTestData myTestData=new MyTestData();
        new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myTestData.addData();
        }).start();
        while (myTestData.a==0){

        }
        System.out.println("end");
    }

}

假如a不修饰为volatile,则在新起的线程里修改了变量a,但是主线程里的a并不会变化,还是等于0。因为新起线程的里,改变了a的值,并把a 写回了主内存,但是并没有通知其他线程进行更新,这样主线程里工作内存里的a 并不会变化。
尝试修改为 public volatile int a = 0;
则正常结束,应该主线程收到通知后,就去主内存中,重新获取了a 的值。

二、不保证原子性

volatile在JUC中,只能算是一种轻型的同步,他比synchronize 更加轻量级,所以当我们只需要关系同步时的可见性时,那不需要使用synchronize关键词,只需要用更轻量级的volatile来修饰即可满足。但是volatile的轻量级,也是有缺陷的,因为它无法保证原子性。那么原子性又是个什么概念呢?
在JUC中,原子性是指一段代码在执行的过程中,不会被其他线程打断,所谓的要么不开始,要开始就执行到结束。调整一下上面的例子。

class MyTestData {
    public volatile int a = 0;

    public void addData() {
        a++;
    }
}
public class TestConcurrent {
    public static void main(String[] args) {
        MyTestData myTestData=new MyTestData();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    myTestData.addData();
                }

            }).start();
        }

        while (Thread.activeCount()>2){

        }
        System.out.println(myTestData.a);
    }

}

在这里,我们起20个线程,每个线程循环1000遍a++,按理20*1000=20000,但是结果a的值却不是2万。
原因是因为a++这个操作,并不是一个原子操作,即便a 修饰了volatile 。
a++这个操作,可以拆分成以下4段。
1、从主内存中获取a ,并在工作内存中创建一个a的副本。
2、执行运算a+1
3、将a+1的结果赋值给工作内存中的a
4、讲工作内存中的a,赋值给主内存中的a,(修饰了volatile,并通知其他线程a值被更新了)。
那只要当前线程在取值后,到写入主内存之前,有其他线程已经将值写入了,那当前线程再写入主内存的时候,就会将其他线程的写入操作丢失了,这和数据库的事务操作很像。
原因大概了解了,那么要如何解决这种原子性的问题呢。
一种方法,就是使用synchronize关键词,或者使用ReentrantLock 来保证 a++操作的原子性。
另一种方法是使用JUC当中新的类型,AtomicInteger,JUC中危java的几种基本类型,提供了线程安全的对象方法类。
以下是解决的代码实现:

class MyTestData {
    public volatile AtomicInteger a =new AtomicInteger();

    public void addData() {
        a.getAndIncrement();
    }
}
public class TestConcurrent {
    public static void main(String[] args) {
        MyTestData myTestData=new MyTestData();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    myTestData.addData();
                }

            }).start();
        }

        while (Thread.activeCount()>2){

        }
        System.out.println(myTestData.a);
    }

}

AtomicInteger 的getAndIncrement 就相当于线程安全的a++,重新执行一下,得到结果为20000。

三、禁止指令重排

指令重排是什么:一段代码要被执行,首先要被JVM编译成字节码,然后CPU再执行这些字节码,进行执行。
那么在这个过程中,JVM编译,CPU执行,都会都代码的顺序进行调整。
指令重排是为了在不改变执行结果的前提下,一种机器自我提高编译执行效率的方法。
举个例子:
int a=1; ——1
int b=2; ——2
int c=a+b;——3
这是我们自己编写的代码,正常的执行顺序应该是1,2,3。
但是实际上,1,2的执行顺序,并不影响后面的执行,所以指令重排的话,很有可能给你优化成2, 1, 3执行。
指令重排,在单线程环境中时没有任何问题的,但是到了多线程的时候,优化的顺序可能会对其他线程造成影响,导致结果不一致。
Volatile 是如何实现的呢,这里涉及到一个新的名词:内存屏障又称内存栅栏。
内存屏障可以保证被volatile修饰的变量,在执行前面都不会被指令重排,并且保证变量的可见性。

小结

以上是我看了volatile的一点自己的理解,如有不对,还请指出,大家共同学习,共同进步。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值