JUC详解-16-JMM及Volatile三大特性

JUC 详解 -> JMM及Volatile

谈谈对Volatile的理解

  • Volatile是Java虚拟机提供轻量级的同步机制
    • 保证可见性
    • 不保证原子性
    • 禁止指令重排

可见性 ——>JMM

1. JMM:Java Memory Model

  • JMM(Java Memory Model)即为JAVA 内存模型。

  • 作用:缓存一致性协议,用于定义数据读写的规则。

  • JMM定义了JVM在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。

  • 从抽象的角度来看,JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

  • 本地内存是JMM的一个抽象概念、约定,是不真实存在的东西。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

  • 关于JMM的一些同步的约定:

    • 线程解锁前,必须把共享变量立刻刷回驻村;
    • 线程加锁前,必须读取主存种的最新值到工作内存种;
    • 加锁和解锁是同一把锁。
      JMM
JMM的 8种内存交互操作

  • 内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外

    • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
    • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
    • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
    • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
    • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
    • write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
    • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
    • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • 规则

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

假如线程A修改了共享变量的值,但是B线程不能及时可见!

代码测试:

import java.util.concurrent.TimeUnit;

public class JMMDemo {
    private static int number = 0;
    public static void main(String[] args) {

        new Thread(()->{
            while(number == 0){

            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        number = 1;
        System.out.println(number);
    }
}

问题:输出1,程序不停止!即线程不知道主内存的值已经被修改过了!

  • 要解决共享对象可见性这个问题:
    • 使用volatile关键字
    • 使用synchronized同步机制
    • final
2. Volatile
2.1 保证可见性

代码测试:解决上述问题

import java.util.concurrent.TimeUnit;

public class JMMDemo {
    //如果不加volatile 程序会死循环
    //加了volatile 可以保证可见性
    private static volatile int number = 0;
    public static void main(String[] args) {

        new Thread(()->{         //线程A对主内存的变化不知道
            while(number == 0){

            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        number = 1;
        System.out.println(number);
    }
}
2.2 不保证原子性
  • 什么叫原子性? 不可分割
    • 线程A在执行任务时,不能被打扰,也不能被分割,要么同时成功,要么同时失败。

代码测试

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//Volatile 不保证原子性验证
//Lock 和 synchronized 可以保证原子性
public class VolatileDemo {
    private volatile static int num = 0;
    //private static Lock lock = new ReentrantLock();

    //public synchronized static void add(){
    public static void add(){
        num ++;
//        lock.lock();
//        try{
//            num++;
//        }catch(Exception e){
//            e.printStackTrace();
//        }finally {
//            lock.unlock();
//        }

    }
    //理论上num结果应该为20000
    public static void main(String[] args) {
        for (int i = 1; i <= 20 ; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }

            },String.valueOf(i)).start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName()+" " + num);
    }
}
  • 不用Lock和synchronized如何保证原子性?
    • num ++ ; 不是一个原子性操作
      num++
  • 使用原子类,解决原子性问题!
    atomic

代码测试

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    //原子类的Integer
    private static AtomicInteger num = new AtomicInteger();
    public static void add(){
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+num);
    }
}

原子类的高级性?

这些类的底层都和操作系统挂钩。 在内存中修改值!Unsafe类是一个很特殊的存在! CAS

2.3 禁止指令重排

指令重排

  • 什么是指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
  • 源代码 --> 编译器优化的重排 --> 指令并行也可能重排 --> 内存系统也会重排 --> 执行
  • 处理器在进行指令重排的时候,会考虑:数据之间的依赖性!

实例

int x = 1;  //1
int y = 2;  //2
x = x + 5;  //3
y = x * x;  //4

我们所期望的执行是1234  但是可能执行的是2134 1324
但不可能是4123

可能造成影响的结果 a b x y 默认都是0

线程A线程B
x = ay = b
b = 1a = 2

正常的结果: x = 0; y = 0

线程A线程B
b = 1a = 2
x = ay = b

指令重排的异常结果:x = 2; y = 1;

  • volatile可以避免指令重排:
    • 内存屏障,CPU指令。作用:
      • 保证特定的操作的执行顺序!
      • 可以保证某些变量的内存可见性!(利用这些特性,volatile实现了可见性)
        内存屏障
总结

  1. volatile可以保证可见性;
  2. volatile不能保证原子性,可以用Lock、synchronized,也可以用JUC中的atomic类;
  3. 由于内存屏障,volatile可以避免指令重排的现象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值