Java-Volatile线程访问共享变量(一)-XXOO

一、Volatile是什么?

volatile是轻量级的synchronized。如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。

Java语言规范对volatile的定义:

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。

二、内存模型相关概念

操作系统语义

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。 CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。 有了CPU高速缓存虽然解决了效率问题, 但是它会带来一个新的问题:数据一致性。

在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中, 在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。

举一个简单的例子:i++

当线程运行这段代码时,首先会从主存中读取i( i = 1),然后复制一份到CPU高速缓存中, 然后CPU执行 + 1 (i = 2)的操作,然后将数据(i = 2)写入到缓存中,最后刷新到主存中。 其实这样做在单线程中是没有问题的,有问题的是在多线程中。

如下: 假如有两个线程A、B 都执行这个操作(i++),按照我们正常的逻辑思维主存中的i值应该=3, 但事实是这样么?

分析如下:

两个线程从主存中读取i的值(1)到各自的高速缓存中,然后线程A执行+1操作并将结果写入高速缓存中, 最后写入主存中,此时主存i==2,线程B做同样的操作,主存中的i仍然=2。所以最终结果为2并不是3。 这种现象就是缓存一致性问题。

解决缓存一致性方案有两种:

1.通过在总线加LOCK#锁的方式

2.通过缓存一致性协议

但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行, 其他CPU都得阻塞,效率较为低下。

第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。

其核心思想如下:

当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的, 因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。

三、Java内存模型

在并发编程中我们一般都会遇到这三个基本概念:原子性、可见性、有序性。

1.原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性就像数据库里面的事务一样,他们是一个团队,同生共死。

示例: (1)i=0; (2)j=i; (3)i++; (4)i=j+1; 这四个操作,那些是原子性?

(1)在Java中,对基本数据类型的变量和赋值操作都是原子性操作;

 (2)包含了两个操作:读取i,将i值赋值给j;

 (3)(4)包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;

在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同, Java只保证了基本数据类型的变量和赋值操作才是原子性的 (注:在32位的JDK环境下,对64位数据的读取不是原子性操作,如long、double)。 要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。 注意: volatile是无法保证复合操作的原子性

2.可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。 Java提供了volatile来保证可见性。 当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中, 当其他线程读取共享变量时,它会直接从主内存中读取。当然,synchronize和锁都可以保证可见性。

3.有序性

即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果, 但是对多线程会有影响。 Java提供volatile来保证一定的有序性。

最著名的例子就是单例模式里面的DCL(双重检查锁)。

四、Volatile原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。

在JVM底层volatile是采用“内存屏障”来实现的。

1.保证可见性、不保证原子性

2.禁止指令重排序

在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:

1.编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;

2.处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

 指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程的正确性。

原则:happens-before

happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before 原则中推导出来,那么他们就不能保证有序性,可以随意进行重排序。

 定义:

1.同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是, 在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说, 这一是规则无法保证编译重排和指令重排)。

 2.监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)

3.对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)

4.线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)

5.线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。

6.如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

JVM是如何禁止重排序?

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时, 会多出一个lock前缀指令。 lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。

volatile的底层就是通过内存屏障来实现的。

五、Volatile使用场景

1.对变量的写操作不依赖当前值;

2.该变量没有包含在具有其他变量的不变式中。

volatile经常用于两个两个场景:状态标记两、double check


示例

package com.yl.springboottest.consurrency.jmm.volatileT;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 描述: Volatile
 *
 * @author: yanglin
 * @Date: 2020-12-03-8:31
 * @Version: 1.0
 */
public class VolatileT1 {

    // 线程并发数量
    private static int threadTotal = 200;
    // 请求总数
    private static int clientTotal = 5000;

    private static long count = 0;

    public static void add(){
        count++;
    }

    /**
     * test运行时,使用两独立的变量来保存时间,避免因使用同步而对t1,t2造成影响
     */
    long time1;
    long time2;
    volatile boolean boolValue = true;


    public static void main(String[] args) throws InterruptedException {
        /**
         * Java只保证了基本数据类型的变量和赋值操作才是原子性的
         *
         * 64位JVM只有server模式(server模式会进行更多的优化),
         * 32位JVM默认使用client模式,我将32位JVM设置为server模式后,问题同样出现
         */
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 信号
        final Semaphore semaphore = new Semaphore(threadTotal);
        // 每次固定数量的线程获取许可
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int index = 0; index < clientTotal ; index++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 获取一个许可
                        semaphore.acquire();
                        add();
                        // 释放一个许可
                        semaphore.release();
                    } catch (Exception e) {
                        e.printStackTrace();

                    }// 计数减一
                    countDownLatch.countDown();
                }
            });

        }
        // 阻塞当前线程,直到计时器的值为0
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("计数  num : " +count);

        System.out.println("----------------------------抢票场景中-------------");
        /**      创建线程      */
        TicketRunnable ticketRunnable=new TicketRunnable();
        // 第1个人
        Thread t1=new Thread(ticketRunnable,"张三");
        // 第2个人
        Thread t2=new Thread(ticketRunnable,"李四");
        // 第3个人
        Thread t3=new Thread(ticketRunnable,"王五");
        // 第4个人
        Thread t4=new Thread(ticketRunnable,"赵六");
        // 第5个人
        Thread t5=new Thread(ticketRunnable,"田七");
        /**      启动线程      */
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        /**
         * Java虚拟机JVM是64位JVM还是32位
         */
        System.out.println("Java虚拟机JVM是64位JVM还是32位::"+System.getProperty("sun.arch.data.model"));

        /**
         * boolValue 变量是我们要验证的 volatile 变量。一开始 boolValue 初始化为 true,
         * 其次启动 t2 线程让其进入死循环;接着,t1 线程启动,并且让 t1 线程先执行,
         * 将 boolValue 改为 false。理论上来讲,此时 t2 线程应该跳出死循环,但是实际上并没有。
         * 此时 t2 线程读到的 boolValue 的值仍然为 true。所以这段程序一直没有打印出结果。
         * 这便是多线程间的不可见性问题,官方话术为: 线程 t1 修改后的值对线程 t2 来说并不可见。
         */
        // 测试个数
        int size = 5000;
        VolatileT1 vs[] = new VolatileT1[size];
        long timeSum = 0;
        for(int n = 0; n < size; n++){
            (vs[n] = new VolatileT1()).test();
        }
        /**
         * 统计出,所有线程从boolValue变为false到while(boolValue)跳出所花时间的总和
         */
        for(int n = 0; n < size; n++){
            timeSum += vs[n].time2 - vs[n].time1;
            System.out.print(n+"\t"+vs[n].time2 +'\t' + vs[n].time1+'\t'+(vs[n].time2 - vs[n].time1)+'\n');
        }
        System.out.println("响应时间总和(毫微秒):"+timeSum);
        long time1,time2;
        time1 = System.nanoTime();
        time2 = System.nanoTime();
        // 顺序执行两条语句的时间间隔,供参考
        System.out.println(time2-time1);
    }

    /**
     * 假设在抢票场景中,我们一共只有10张火车票,在最后一刻,我们已经卖出了9张火车票,仅剩最后一张。
     * 这个时候,系统发来多个并发请求,这些并发请求都同时读取到火车票剩余数量为1,然后都通过了这一个
     * 余量判断,最终导致超发。也就是说,本来我们只卖10张火车票,最多只生成10个订单,但因为线程不安全,
     * 用户的并发请求,导致抢票成功的用户订单超过了10个,这就是线程不安全。
     */
    static class TicketRunnable implements Runnable {
        // 剩余的票数
        static volatile int count = 10;
        // 抢到第几张票
        static int num = 0;
        // 是否售完票
        boolean flag = false;
        @Override
        public void run() {
            // TODO Auto-generated method stub
            // 票没有售完的情况下,继续抢票
            while (!flag) {
                sale();
            }
        }
        /**  售票    */
        private synchronized void sale() {
            if(count <= 0){
                flag = true;
                return;
            }
            // 剩余的票数 减1
            count--;
            // 抢到第几张票 加1
            num++;
            System.out.println(Thread.currentThread().getName()+"抢到第"+num+"张票,剩余"+count+"张票。");
        }
    }

    public void test() throws InterruptedException{
        Thread t2 = new Thread(){
            @Override
            public void run(){
                while(boolValue)
                    ;
                time2 = System.nanoTime();
            }
        };
        Thread t1 = new Thread(){
            @Override
            public void run(){
                time1 = System.nanoTime();
                boolValue=false;
            }
        };
        t2.start();
        Thread.yield();
        t1.start();
        /**
         * 保证一次只运行一个测试,以此减少其它线程的调度对 t2对boolValue的响应时间 的影响
         */
        t1.join();
        t2.join();
    }

}

以上

猜你喜欢

转载自blog.csdn.net/qq_35731570/article/details/111034705