juc学习一(volatile关键字及原子变量)

Posted mabaoying

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了juc学习一(volatile关键字及原子变量)相关的知识,希望对你有一定的参考价值。

JUC简介

    利用多线程提高效率,尽可能的利用cpu资源。java5以前多线程同步用了sychronized、volatile。在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中的 Collection 实现等。

JMM

JMM(java内存模型java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它的描述是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

1.线程解锁前,必须把共享变量的值刷新回主内存

2.线程加锁前,必须读取主内存的最新值到自己的工作内存

3.加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

 技术图片

只要有任何一个线程改变了,就会写入主内存,而且其他线程第一时间就能看见,这就是可见性。

 JMM三大特性(线程安全性获得保障)

1.可见性

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

 volatile可以保证可见性,及时通知其它线程,主物理内存的值已被修改。

2.原子性

原子性指:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完整,要么同时成功,要么同时失败。

3.有序性

  计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下三种

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

  单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

  处理器在重排时必须要考虑指令之间的数据依赖性。

  多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

volatile关键字

volatile是java虚拟机提供的轻量级的同步机制。遵守JMM规范。

volatile 关键字: 当多个线程进行操作共享数据时,可以保证内存中的数据是可见的;相较于 synchronized 是一种
较为轻量级的同步策略;volatile 不具备"互斥性";volatile 不能保证变量的"原子性";

三大特性:

  1.保证可见性

  2.不保证原子性

  3. 禁止指令重排序

验证volatile的可见性
/**
 * 1验证volatile的可见性
 *  1  假如number变量没有添加volatile关键字修饰,没有可见性
 *  2 加了volatile关键字修饰,具备了可见性
 */
public class VolatileDemo1 {

    public static void main(String[] args) {
        //volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
        MyData1 myData=new MyData1();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"	   初始的number值为:"+myData.number);
            try {
                //暂停一会儿线程
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.add();
            System.out.println(Thread.currentThread().getName()+ "	 更新后的number值为:"+myData.number);
        },"线程A").start();

        //第二个线程就是main线程
        while(myData.number==0){
            //main线程一直等待循环,直到number值不等于0
        }
        System.out.println(Thread.currentThread().getName()+"	 线程结束后number值为:"+myData.number);
    }
}

class MyData1{
    //共享变量
    volatile int number=0;

    public void add(){
        this.number=60;
    }
}

  如果MyData1中的number变量没有加volatile关键字修饰,运行VolatileDemo1的main方法,会出现main线程一直在while里死循环。虽然子线程修改了变量number的值,但共享变量对main线程来说不可见。加上了volatile关键字修饰number变量,就会执行while下面的属于语句。volatile可以保证可见性,及时通知其它线程,主物理内存的值已被修改。

验证volatile不保证原子性

/**
 *  验证volatile不保证原子性
 * 原子性指:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完整,
 *  要么同时成功,要么同时失败。
 */
public class VolatileDemo2 {

    public static void main(String[] args) {
        //验证volatile不保证原子性
        MyData2 myData=new MyData2();
        for(int i=1;i<=20;i++){
            new Thread(()->{
                for(int j=1;j<=1000;j++){
                    myData.addPlusPlus();
                }
            }).start();
        }
        //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值是多少
        while(Thread.activeCount()>2){//后台默认两个线程,一个main线程,二是gc线程
            Thread.yield();
            //yield 即 "谦让",也是 Thread 类的方法。它让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,
            // 并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。
        }
        System.out.println(Thread.currentThread().getName()+"	    sum==="+myData.number);
    }
}

class MyData2{
    //共享变量
    volatile int number=0;
    //number++在多线程下是非线程安全的,
    public void addPlusPlus(){
        this.number++;
    }
}

  创建20个线程,每个线程执行1000次i++,理论最后计算出总结果数为20000,但发现结果不一致。number++执行分为"读改写"三步操作,在写回主内存时出现写覆盖,导致数据丢失问题,所以值低于20000。其实原子性就是最终一致性能不能得到保证,中间操作时,不受其他干扰。

int temp = number;
number =number + 1;
number = temp;

解决原子性问题:

1.加synchronized关键字修饰addPlusPlus方法,synchronized 关键字,同步锁能保证数据的及时更新,能够解决问题,但是这样用会导致线程阻塞,影响效率。

 2.使用原子性变量,jdk1.5后java.util.concurrent.atomic包下提供了常用的原子变量。

  用volatile修饰变量保证内存可见性

  CAS(Compare-And-Swap)算法保证数据的原子性

/**
 *使用AtomicInteger原子变量
 */
public class VolatileDemo3 {

    public static void main(String[] args) {
        MyData3 myData=new MyData3();
        for(int i=1;i<=20;i++){
            new Thread(()->{
                for(int j=1;j<=1000;j++){
                    myData.addAtomic();
                }
            }).start();
        }
        //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值是多少
        while(Thread.activeCount()>2){//后台默认两个线程,一个main线程,二是gc线程
            Thread.yield();
            //yield 即 "谦让",也是 Thread 类的方法。它让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,
            // 并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。
        }
        System.out.println(Thread.currentThread().getName()+"	    sum==="+myData.atomicInteger);
    }
}

class MyData3{
    AtomicInteger atomicInteger=new AtomicInteger();

    public void addAtomic(){
        //先得到值后再自加一,想到于i++
        atomicInteger.getAndIncrement();
    }
}

 线程安全性获得保证:

工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题,可以利用volatile关键字解决,因为volatile的另一个作用就是禁止重排序优化。

 

以上是关于juc学习一(volatile关键字及原子变量)的主要内容,如果未能解决你的问题,请参考以下文章

JUC中的原子操作类及其原理

volatile关键字与内存可见性&原子变量与CAS算法

重点知识学习(8.2)--[JMM(Java内存模型),并发编程的可见性原子性有序性,volatile 关键字,保持原子性,CAS思想]

原子变量与CAS算法

JUC并发编程 详解Java关键字之 volatile

JUC - 多线程之JMM;volatile