熬不过的日子就让自己忙碌起来

目录

  • Volatile三大特性
    • 保证可见性
      • JMM内存模型
      • 原理
    • 不保证原子性
      • 原因
      • Atomic原子类
    • 禁止指令重排
      • 内存屏障
  • 应用场景
    • 状态标识
    • 一次性安全发布
    • 独立观察
    • volatile bean 模式
    • 开销较低的读-写锁策略
    • 双重检查(double-checked)

vloatile是什么?
java虚拟机提供的轻量级的同步机制

Volatile三大特性

保证可见性

JMM内存模型

JMM内存模型定义了程序中变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。这里不对JMM详解

三大性质
可见性
原子性
有序性

其中,可见性简单理解就是:

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

可见性是基于内存屏障(Memory Barrier)实现。

内存屏障,又称内存栅栏,是一个 CPU 指令。
结尾会讲到。
原理

MESI缓存一致性协议:
多个CPU从主内存读取同一条数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步回主内存,其他CPU通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效

底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定),并写回到主内存

代码验证一下(未加volatile关键字)

public class Demo {static int count = 0;public static void main(String[] args) throws Exception {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());threadPoolExecutor.execute(()->{try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}count=6;});while (count==0){}threadPoolExecutor.shutdown();}
}

程序将一直进行,不会结束,程序修改之后,则成功!

static volatile int count = 0;

不保证原子性

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

案例:
创建5个线程,每个线程对count通过for循环加10000次,意料中的答案应该是count=50000是吧,然而每次操作count大概率都小于50000。存在线程安全问题。

public class Demo {static volatile int count = 0;public static void main(String[] args) throws Exception {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());try {for (int i=0;i<5;i++) {threadPoolExecutor.execute(()->{for (int j = 0; j < 10000; j++) {count=count+1;}});}} catch (Exception e) {e.printStackTrace();}threadPoolExecutor.shutdown();System.out.println(count);}
}

怎么解决volatile不保证原子性问题

加锁----性能差,舍弃
使用原子类Atomic
public class Demo {static volatile AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws Exception {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());try {for (int i = 0; i < 5; i++) {threadPoolExecutor.execute(() -> {for (int j = 0; j < 1000; j++) {count.incrementAndGet();}});}} catch (Exception e) {e.printStackTrace();} finally {threadPoolExecutor.shutdown();}Thread.sleep(2000);System.out.println("count="+count);}
}
原因

当一个线程修改了共享变量在自己线程副本的值后,副本需要写会主内存,这时候汇编底层会加锁,只至写会成功,同时另一个线程也修改了共享变量在自己线程副本的值,副本需要写会主内存,这时候锁被另一个线程占用,导致写回操作等待。而第一个线程写回操作完成后,触发了MESI缓存一致性协议,导致其他线程缓存数据失效,其他线程需要从主内存重新拉取数据,那等待锁的线程将导致修改的数据丢失,出现不保证原子性的问题。

Atomic原子类

关于Atomic原子类的详细讲解请阅读我的这篇博客
关于JCU并发包中的Atomic原子类及其CAS

禁止指令重排

● 为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。

● Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

● JMM 会针对编译器制定 volatile 重排序规则表。

" NO " 表示禁止重排序。

内存屏障

为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。

在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

|

内存屏障 说明
StoreStore 屏障 禁止上面的普通写和下面的 volatile 写重排序。
StoreLoad 屏障 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。。
LoadLoad 屏障 禁止下面所有的普通读操作和上面的 volatile 读重排序。
LoadStore 屏障 禁止下面所有的普通写操作和上面的 volatile 读重排序。

应用场景

使用 volatile 必须具备的条件对变量的写操作不依赖于当前值。该变量没有包含在具有其他变量的不变式中。

状态标识

也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;
......
public void shutdown() { shutdownRequested = true; }
public void doWork() { while (!shutdownRequested) { // do stuff}
}

一次性安全发布

缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。

public class BackgroundFloobleLoader {public volatile Flooble theFlooble;public void initInBackground() {// do lots of stufftheFlooble = new Flooble();  // this is the only write to theFlooble}
}
public class SomeOtherClass {public void doWork() {while (true) { // do some stuff...// use the Flooble, but only if it is readyif (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble);}}
}

独立观察

安全使用 volatile 的另一种简单模式是定期 发布 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

public class UserManager {public volatile String lastUser;public boolean authenticate(String user, String password) {boolean valid = passwordIsValid(user, password);if (valid) {User u = new User();activeUsers.add(u);lastUser = user;}return valid;}
}

volatile bean 模式

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。

@ThreadSafe
public class Person {private volatile String firstName;private volatile String lastName;private volatile int age;public String getFirstName() { return firstName; }public String getLastName() { return lastName; }public int getAge() { return age; }public void setFirstName(String firstName) { this.firstName = firstName;}public void setLastName(String lastName) { this.lastName = lastName;}public void setAge(int age) { this.age = age;}
}

开销较低的读-写锁策略

volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。

如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。

安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafe
public class CheesyCounter {// Employs the cheap read-write lock trick// All mutative operations MUST be done with the 'this' lock held@GuardedBy("this") private volatile int value;public int getValue() { return value; }public synchronized int increment() {return value++;}
}

双重检查(double-checked)

单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。

class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) {syschronized(Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

应用场景参考自:https://www.jianshu.com/p/ccfe24b63d87

文章持续更新,可以微信搜索「 绅堂Style 」第一时间阅读,回复【资料】有我准备的面试题笔记。
GitHub https://github.com/dtt11111/Nodes 有总结面试完整考点、资料以及我的系列文章。欢迎Star。

掌握Volatile关键字及其牵扯的JUC并发包相关推荐

  1. 一文读懂 volatile 关键字

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:有了这 4 款工具,老板再也不怕我写烂SQL了个人原创+1博客:点击前往,查看更多 作者:对弈 来源:https ...

  2. 并发编程—Volatile关键字

    锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility).互斥即一次只允许一个线程持有某个特定的锁,因此可以保证一次就只有一个线程在访问共享数据.可见性要复杂一 ...

  3. volatile关键字和AtomicInteger

    在Java中,线程部分是一个重点,本篇文章说的JUC也是关于线程的.JUC就是java.util .concurrent工具包的简称.这是一个处理线程的工具包,JDK 1.5开始出现的.下面一起来看看 ...

  4. setnx是原子操作吗_谈谈Volatile关键字?为什么不能保证原子性?用什么可以替代?为什么?...

    大家好,欢迎关注我的公众号码猿bug,需要资料的话可以加我微信好友. 再谈volatile关键字之前,首先必须聊聊JMM内存模型! JMM主要的特性:可见性.原子性,顺序性 Java 虚拟机规范试图定 ...

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

    文章目录 1.JMM(Java Memory Model) 2.并发编程的可见性 3.并发编程的有序性 4.并发编程的原子性 5.volatile 关键字 6.保持原子性: 加锁,JUC原子类 加锁 ...

  6. java中实现具有传递性吗_Java中volatile关键字详解,jvm内存模型,原子性、可见性、有序性...

    一.Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作 ...

  7. volatile关键字之全面深度剖析

    引言 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字 ...

  8. c语言中volatile关键字的作用

    读文章之前 可以先看一下<程序员的自我修养 >第28页 过度优化. volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直 ...

  9. C语言volatile关键字详解

    volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据.如果没有volatile关键字,则编译器可能优化读取和存储 ...

最新文章

  1. 查看oracle的表空间使用,查看Oracle的表空间的使用情况
  2. Spring(一)——总体介绍
  3. 网络编程知识预备(4) ——了解应用层的HTTP协议与HTTPS协议
  4. 译 | .NET Core 基础架构进化之路(一)
  5. P3812-[模板]线性基
  6. Oracle入门(十二H)之设置、恢复和删除不可用列
  7. 源码调试debug_info 的作用和使用方法
  8. c语言开发实际,21实际c语言教程-1 (转)
  9. 所有快捷方式失效的解决方法
  10. 原理图和PCB设计流程概述
  11. MYSQL Error:You must SET PASSWORD before execut...
  12. Python 树表查找_千树万树梨花开,忽如一夜春风来(二叉排序树、平衡二叉树)
  13. 离线环境下的软件交付姿态
  14. 上帝模式下的shellcode
  15. 浅谈数字后端工程师的工作
  16. maven生成webapp
  17. UGUI动画快速制作
  18. 第13期《凤凰涅槃,浴火重生!》2月刊
  19. AndroidApp之图书管理系统(一)
  20. MATLAB中 feval 函数的用法

热门文章

  1. [Arrays]D. Liang 6.15 Revising selection sort
  2. Centos XMR
  3. 单片机基础入门:单片机电源电路设计,搞定电源不求人
  4. C语言实现杨辉等腰三角形及变形三角形(附代码)
  5. 参照《自己动手做CPU》自己动手做CPU
  6. 一种串口两线的 TTL 转 485的电路
  7. cad插件加载bplot成功用不了_Batchplot辅助插件常见问题解决方法
  8. 强大!Python 自动化清理微信僵尸好友
  9. CRM系统为企业带来的8个好处
  10. Intro.js 分步向导插件使用方法 1