Java并发编程5_Volatile关键字

发布于:2022-12-17 ⋅ 阅读:(429) ⋅ 点赞:(0)

目录

一、Volatile关键字


一、Volatile关键字

 概念

  • Volatile 是 JVM 提供的轻量级的同步机制,是一个关键词,特点有:
    • 保证可见性
    • 不保证原子性
    • 禁止指令重排
  • JMM:Java内存模型,是一个概念或者约定:
    • 线程解锁前,必须把 共享变量 立刻 更新新到主内存(每个线程都有自己一块内存称为 工作内存,操作的变量是自己内存块的变量,但是实际存在的位置是主内存,因此每次操作完之后需要 更新 主内存)
    • 线程加锁前:必须读取主内存中的最新值加载到线程工作内存中

1.JMM的8种操作:

  • read、load
  • use、assign
  • write、store
  • lock、unlock

在这里插入图片描述

  1. lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
  2. read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
  3. load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  4. use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  5. assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  6. store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  7. write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  8. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对8种内存交互操作制定的规则: 

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
  • 不允许线程将没有assign的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

很多并发编程都使用了volatile关键字,主要的作用包括两点: 

  • 保证线程间变量的可见性。
  • 禁止CPU进行指令重排序。

2.使用volatile解决不可见性场景 

 代码举例:

package cn.test;

import java.util.concurrent.TimeUnit;

public class Demo {
    // 主内存的变量
    private static Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        // 1.不可见性
//        new Thread(()->{
//            while(flag){
//
//            }
//        },"其他线程").start();
//
//        try {
//            TimeUnit.SECONDS.sleep(3);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//
//        System.out.println("主线程修改了flag,但是另外一个线程工作空间的flag仍然是true.");
//        flag = false;


        // 2.synchronized解决不可见性
        new Thread(() -> {
            while (flag) {
                // System.out.println(flag);  用这个也可以实现,因为底层用了synchronized
                synchronized (Demo.class) {// lock: flag要从主内存中拿最新的值

                }
            }
        }, "其他线程").start();


        TimeUnit.SECONDS.sleep(3);

        System.out.println("主线程修改了flag,但是另外一个线程工作空间的flag仍然是true.");
        flag = false;
    }
}

 解决不可见性的方案:

  • 增加Lock锁或synchronized关键字
  • 只给成员变量增加volatile关键字:private static volatile Boolean flag = true;

 3.使用volatile不保证原子性场景

代码举例:

package cn.test;

public class Demo {
    // 主内存的变量
    private static volatile int num = 0;

    public static void main(String[] args) {
        // 模拟10个线程
        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        // 确保上面的线程执行完,只剩下 main 和 gc 线程
        while(Thread.activeCount() > 2){
            Thread.yield();// 停止当前主线程,让其他线程获取到CPU执行
        }

        System.out.println("正常结果为10000,输出结果为===》" + num); // 正常结果为10000,输出结果为===》9182
    }

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

 结果分析:

  • 多个线程操作数据时,会出现不准确结果

  • 加上volatile人仍然不能保证原子性,因为num++的底层并不是一个原子性操作,对应三个字节指令

在这里插入图片描述

4.加上synchronized关键字 、Lock锁可以保证原子性

代码举例: 

package cn.test;

public class Demo {
    // 主内存的变量
    private static volatile int num = 0;

    public static void main(String[] args) {
        // 模拟10个线程
        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        // 确保上面的线程执行完,只剩下 main 和 gc 线程
        while(Thread.activeCount() > 2){
            Thread.yield();// 停止当前主线程,让其他线程获取到CPU执行
        }

        System.out.println("正常结果为10000,输出结果为===》" + num); // 正常结果为10000,输出结果为===》9182
    }

    public synchronized static void add(){
        num++;
    }
}


synchronized保证原子性的原理:

  • 对add()方法增加synchronized修饰后,保证同一时间只有一个线程进行num++,就不会出现数据安全问题

5. 解决使用volatile不能保证原子性问题

  •  使用java.util.concurrent.atomic 下的一个小型工具包,配合volatile支持单个变量上的无锁线程安全编程

  • // 创建 可原子更新的int 值对象
  • private static volatile AtomicInteger num = new AtomicInteger(0);
  • // 该对象有方法+1,原理是调用Unsafe类的方法,底层使用的CAS
  • num.getAndIncrement();

 代码举例: 

package cn.test;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo {
    // 主内存的变量
    private static volatile AtomicInteger num = new AtomicInteger();

    public static void main(String[] args) {
        // 模拟10个线程
        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        // 确保上面的线程执行完,只剩下 main 和 gc 线程
        while(Thread.activeCount() > 2){
            Thread.yield();// 停止当前主线程,让其他线程获取到CPU执行
        }

        System.out.println("正常结果为10000,输出结果为===》" + num); // 正常结果为10000,输出结果为===》9182
    }

    public static void add(){
        // 相当于num++
        num.getAndIncrement();
    }
}

6.禁止指令重排

 概念:

  • 你写的程序,计算机并不是按照你写的那样去执行的
  • 程序执行大致流程:源代码—>编译器优化的重排—>指令并行的重排—>内存系统也会重排—>最后再执行
  • 处理器在进行指令重排的时候会考虑数据之间的依赖性
  • 加入volatile会避免指令重排,它有一个内存屏障,其作用
    • 可以保证某些变量的内存可见性
    • 只要加了volatile就会在volatile的上面和下面加上内存屏障,以此来避免上下指令的顺序交换,从而来保证指令的有序执行

 


网站公告

今日签到

点亮在社区的每一天
去签到