Java并发编程之volatile关键字

参考博客

1、简介

volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或方法)和volatile变量,相比于synchronized(重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

2、并发编程的3个基本概念

1.原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。
简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
Java中的原子性操作包括:
    1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
    2)所有引用reference的赋值操作
    3)java.util.concurrent.atomic.* 包中所有类的一切操作
2.可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改后的值。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
synchronizedLock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.有序性:即程序执行的顺序按照代码的先后顺序执行。
Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。
另外,可以通过synchronizedLock来保证有序性,synchronizedLock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。

/**
 * 单例模式
 */
public class InstanceDemo {
    private static volatile InstanceDemo instance;

    private InstanceDemo() {
    }

    public static InstanceDemo getInstance() {
        if (instance == null) {// 第一次检查
            synchronized (InstanceDemo.class) { // 加锁
                if (instance == null) { // 第二次检查
                    instance = new InstanceDemo(); // 问题的根源处在这里
                }
            }
        }
        return instance;
    }

    /**
     * 创建了一个对象,分为三个步骤(正常顺序)
     * memory = alloct();       //1:分配对象的内存空间
     * ctorinstance(memory);    //2:初始化对象
     * instance = memory;       //3: 设置instance指向刚分配的内存地址
     *
     * 重排序
     * memory = alloct();       //1:分配对象的内存空间
     * instance = memory;       //3: 设置instance指向刚分配的内存地址 (此时instance不为null但对象还未初始化)
     * ctorinstance(memory);    //2:初始化对象
     */
    
}

3、volatile变量的特性

1.保证可见性,不保证原子性
  1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
  2)这个写操作会导致其他线程中的volatile变量缓存无效。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
  1)重排序操作不会对存在数据依赖关系的操作进行重排序。比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
  2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3

重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果.

	private int a;
    private int b;
    private int c;
    private static int count;

    public static void demo01() {
        int i = 1;
        // 死循环,只有发生指令重排序时,count才不等于0,从而跳出循环
        do {
            VolatileDemo demo = new VolatileDemo();
            new Thread(demo::method01, "t1").start();
            new Thread(demo::method01_2, "t2").start();

            while (Thread.activeCount() > 1) Thread.yield();
            System.out.println(i++ + ":===============================");
        } while (count == 0);
        System.out.println(i);
        System.out.println(count);
    }

	// 给 a、b、c赋值
    private void method01() {
        try {
            Thread.sleep(1);
            a = 1;
            b = 2;
            c = 3;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

	// 比较abc变量的值,排查是否发生指令重排序
    private void method01_2() {
        try {
            Thread.sleep(1);
            System.out.println("线程:" + Thread.currentThread().getName() + " ==> a=" + a + " and b=" + b + " and c=" + c);
            if ((c == 3 && a + b != c) || (b == 2 && a != 1)) {
                System.err.println("++++++++++++++++++++++++++++++++线程:" + Thread.currentThread().getName() + " ==> a=" + a + " and b=" + b + " and c=" + c);
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

多次测试,基本上10W次以内就会发生一次指令重排序问题
在这里插入图片描述
在这里插入图片描述

使用volatile关键字修饰共享变量便可以禁止这种重排序。

	private volatile int a;
    private volatile int b;
    private volatile int c;
    private static int count;

运行100万次后,仍然没有发生指令重排序问题
在这里插入图片描述
当用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

4、volatile不能保证原子性

    private static volatile int inc;

    private static void demo02() {
        Thread t1 = new Thread(VolatileDemo::method2, "t1");
        Thread t2 = new Thread(VolatileDemo::method2, "t2");
        Thread t3 = new Thread(VolatileDemo::method2, "t3");
        t1.start();
        t2.start();
        t3.start();

        while (Thread.activeCount() > 1) Thread.yield();
        System.out.println(inc);
    }

    private static void method2() {
        String thName = Thread.currentThread().getName();
        System.out.println("线程:" + thName + " is ready >> " + inc);
        try {
            Thread.sleep(1000);
            for (int i = 0; i < 1000; i++) {
                inc++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:" + thName + " is over >> " + inc);
    }

多次测试,不能保证结果一致性
在这里插入图片描述

volatile int i = 1;
情景:已有i=1在主内存中,此时线程A跟线程B都需要对i进行加1操作
1)                线程A                                   线程B
    第一步:     读取了i(i=1)
    第二步:    执行写操作(+1)(本地缓存内存内进行)
    第三步:    更新i到主内存(i=2)
    第四步:                                            读取了i(i=2)
    第五步:                                            执行写操作(+1)
    第六步:                                            更新i到主内存(i=3)
  线程B在线程A更新操作之后再读取i,能读取到最新值的i,体现了可见性
=========================================================================
2)                线程A                   线程B              线程C
    第一步:     读取了i(i=1)                               读取了i(i=1)
    第二步:                             读取了i(i=1)
    第三步:     执行写操作(+1)                             执行写操作(+1)
    第四步:                             执行写操作(+1)
    第五步:     更新i到主内存(i=2)
    第六步:                            更新i到主内存(i=2)  更新i到主内存(i=2)
 线程B或线程C在线程A更新操作之前就读取i并进行写操作,ABC最后都会更新i到主内存,主内存的值以最后完成更新操作的值为准

解决方案之synchronized关键字

    /**
     * synchronized
     * 参数inc 不加 volatile
     */
    private static void demo03() {
        Thread t1 = new Thread(VolatileDemo::method03, "t1");
        Thread t2 = new Thread(VolatileDemo::method03, "t2");
        Thread t3 = new Thread(VolatileDemo::method03, "t3");
        t1.start();
        t2.start();
        t3.start();

        while (Thread.activeCount() > 1) Thread.yield();
        System.out.println(inc);
    }

	//粗粒度
	private synchronized static void method03() {
        String thName = Thread.currentThread().getName();
        System.out.println("线程:" + thName + " is ready >> " + inc);
        try {
            Thread.sleep(1000);
            for (int i = 0; i < 1000; i++) {
                inc++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("线程:" + thName + " is over >> " + inc);
    }
    //细粒度
    private static void method03() {
        String thName = Thread.currentThread().getName();
        System.out.println("线程:" + thName + " is ready >> " + inc);
        try {
            Thread.sleep(1000);
            synchronized (VolatileDemo.class) {
                for (int i = 0; i < 1000; i++) {
                    inc++;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("线程:" + thName + " is over >> " + inc);
    }

解决方案之ReentrantLock可重入锁

	/**
     * ReentrantLock
     * 参数inc 不加 volatile
     */
    private static void demo04() {
        Lock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> method04(lock), "t1");
        Thread t2 = new Thread(() -> method04(lock), "t2");
        Thread t3 = new Thread(() -> method04(lock), "t3");
        t1.start();
        t2.start();
        t3.start();

        while (Thread.activeCount() > 1) Thread.yield();
        System.out.println(inc);
    }

    private static void method04(Lock lock) {
        String thName = Thread.currentThread().getName();
        System.out.println("线程:" + thName + " is ready >> " + inc);
        try {
            Thread.sleep(1000);
            lock.lock();
            for (int i = 0; i < 1000; i++) {
                inc++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        System.out.println("线程:" + thName + " is over >> " + inc);
    }

解决方案之AtomicInteger原子操作类

    /**
     * AtomicInteger
     * 参数inc 不加 volatile
     */
    private static AtomicInteger atomicInc = new AtomicInteger();

    private static void demo05()  {
        Thread t1 = new Thread(VolatileDemo::method05, "t1");
        Thread t2 = new Thread(VolatileDemo::method05, "t2");
        Thread t3 = new Thread(VolatileDemo::method05, "t3");
        t1.start();
        t2.start();
        t3.start();
        
        while (Thread.activeCount() > 1) Thread.yield();
        System.out.println(atomicInc.get());
    }

    private static void method05() {
        String thName = Thread.currentThread().getName();
        System.out.println("线程:" + thName + " is ready >> " + atomicInc.get());
        try {
            Thread.sleep(1000);
            for (int i = 0; i < 1000; i++) {
                atomicInc.getAndIncrement();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("线程:" + thName + " is over >> " + atomicInc.get());
    }

5、volatile与synchronized

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值