浅谈volatile

简介

volatile是Java语言中的一种轻量级的同步机制,它可以确保共享变量的内存可见性,也就是当一个线程修改了共享变量的值时,其他线程能够立即知道这个修改。跟synchronized一样都是同步机制,但是相比之下,synchronized属于重量级锁,volatile属于轻量级锁。

JMM概述

JMM就是Java内存模型(Java Memory Model),是Java虚拟机规范的一种内存模型,屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。

Java内存模型规定了Java程序的变量(包括实例变量,静态变量,但是不包括局部变量和方法参数)全部存储在主内存中,定义了各种变量(线程的共享变量)的访问规则,以及在JVM中将变量存储到主内存与从主内存读取变量的底层细节。

JMM的规定

  • 所有共享变量都存在于主内存(包括实例变量,静态变量,但是不包括局部变量和方法参数),因为局部变量是线程私有,不存在竞争问题。
  • 每个线程都有自己的工作内存,所需要的变量是主内存中的副本。
  • 线程对变量的读、写操作都只能在工作内存中完成,不能直接参与读写主内存的变量。
  • 不同的线程也不能去直接访问不同线程的工作内存的变量,线程间的变量传递需要通过主内存来中转完成。

volatile的特性

1、可见性

volatile可以保证线程的可见性,即当多个线程访问同一个变量的时候,此变量发生改变,其他线程也能实时获得到这个修改的值。

在java中,变量都会被放在推内存(所有线程共享的内存)中,多个线程对共享内存是不可见的,当每个线程去获取这个变量的值时,实际上是copy一份副本在线程自身的工作内存中。

举个例子

我们将main作为主线程,MyThread为子线程。在子线程中定义一个共享变量flag,主线程会去访问这个共享变量。在不加volatile的时候,flag在主线程读到的永远是为false,因为两个线程是不可见的。

public class T2_Volatile01 {
public static void main(String[] args) { // 主线程
MyThread my = new MyThread();
my.start();
while (true) {
if (my.isFlag()) System.out.println("进入等待...");
}
}
} class MyThread extends Thread { // 子线程
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flag = true;
System.out.println("flag 修改完毕!");
} public boolean isFlag() {
return flag;
} public void setFlag(boolean flag) {
this.flag = flag;
}
}

实际上是已经修改了的,只是线程读的都是自己的工作内存中的数据,然而,要解决这个问题,可以使用synchronized加锁和volatile修饰共享变量来解决,这两种都能让主线程拿到子线程修改的变量的值。

synchronized (my) {
if (my.isFlag()) System.out.println("进入等待...");
}

加了synchronized锁,首先该线程会获得锁对象,接着会去清空工作内存,再从主内存中copy一份最新的值到工作变量中,接着执行代码, 打印输出,最后释放锁。

当然还能使用volatile关键字去修饰共享变量。一开始子线程从主内存中获取变量的副本到自己的工作内存,进行改值,此时还未写回主内存,主线程从主内存获取的变量的值也是一开始的初始值,等到子线程写回到主内存时,接下来其他线程的工作内存中此变量的副本将会失效,也就是类似于监听。在需要对此变量进行操作的时候,将会到主内存获取新的值保存到线程自身的工作内存中,从而确保了数据的一致。

总结

volatile能够保证不同线程对共享变量的可见性,也就是修改过的volatile修饰的共享变量只要被写回到主内存中,其他线程就能够马上看到最新的数据。

当一个线程对volatile修饰的变量进行写的操作时候,JMM会立即把该线程自身的工作内存的共享变量刷新到主内存中。

当对线程进行读操作的时候,JMM会立即把当前线程自身的工作内存设置无效,从而从主内存中去获取共享变量的数据。

2、无法保证原子性

原子性指的是一项操作要么都执行,要么都不执行,中途不允许中断也不受其他线程干扰。

举个例子

我们看以下案例代码,简单描述一下,AutoAccretion是一个线程类,里面定义了一个共享变量count,并去执行1万次的自增,在main线程中调用多线程去执行自增。我们所期望的结果是最终count的值是1000000,因为每个线程自增1万次,一共100个线程。

public class T3_Volatile01 {
public static void main(String[] args) {
Runnable thread = new AutoAccretion();
for (int i = 1; i <= 100; i++) {
new Thread(thread, "线程" + i).start();
}
}
} class AutoAccretion implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
count++;
System.out.println(Thread.currentThread().getName() + "count ==> " + count);
}
}
}

分析

count++操作首先会从主内存中拷贝变量副本到工作内存中,在工作内存中进行自增操作,最后将工作内存的数据写回主内存中。运行之后会发现,count的值是没办法到达1百万的。主要原因是count++自增操作并不是原子性的,也就是说在进行count++的时候可能被其他线程打断。

当线程1拿到count=0,进行自增后count=1,但是还没写到主内存,线程2获取的数据可能也是count=0,经过自增count=1,两者在写回内存,就会导致数据的错误。

使用volatile对原子性测试

现在通过volatile去修饰共享变量,运行之后,发现任然没办法达到一百万。

使用锁的机制

通过使用synchronized锁对代码快进行加锁,从而确保原子性,确保某个线程对count进行操作不受其他线程的干扰。

class AutoAccretion implements Runnable {
private volatile int count = 0; // 并发下可见性
@Override
public void run() {
synchronized (this) {
for (int i = 1; i <= 10000; i++) {
count++;
System.out.println(Thread.currentThread().getName() + "count ==> " + count);
}
}
}
}

通过验证可以知道能够实现原子性。

总结

在多线程下,volatile关键字可以保证共享变量的可见性,但是不能保证对变量操作的原子性,因此,在多线程下即使加了volatile修饰的变量也是线程不安全的。要保证原子性就得通过加锁的机制。

除了这个方法,Java还能用过原子类(java.util.concurrent.atomic包) 来保证原子性。

3、禁止指令重排

什么是指令重排序

指令重排序:为了提高程序性能,编译器和处理器会对代码指令的执行顺序进行重排序。

良好的内存模型实际上会通过软件和硬件一同尽可能提高执行效率。JMM对底层约束尽量减少,在执行程序时,为了提高性能,编译器和处理器会对指令进行重排序。

一般重排序有以下三种:

  • 编译器优化的重排序:编译器在不改变单线程程序语义可以对执行顺序进行排序。
  • 指令集并行的重排序:如果指令不存在相互依赖,那么指令可以改变执行的顺序,从而能够减少load/store操作。
  • 内存系统的重排序:处理器使用缓存和读/写缓存区,使得加载和存储操作是乱序执行的。

重排序怎么提高执行速度

在不改变结果的时候,对执行进行重排序,可以提高处理速度。重排序后能够使处理指令执行的更少,减少指令操作。

重排序的问题所在

由于重排序,直接可能带来的问题就是导致最终的数据不对,通过以下例子来看,如果执行的顺序不同,最终得到的结果是不一样的。

public class T4_Reordering {
public static int a = 0, b = 0;
public static int i = 0, j = 0; public static void main(String[] args) throws InterruptedException {
int count = 0;
while (true) {
count++;
// 初始化
a = 0;
b = 0;
i = 0;
j = 0;
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
i = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
j = a;
}
});
one.start();
two.start();
one.join(); // 确保线程都执行完毕
two.join();
System.out.println("第" + count + "次线程执行:i = " + i + ", j = " + j );
if (i == 0 && j == 0) return;
}
}
}

正常当线程都执行结束之后,最后得到的值应该是i=1, j=1。通过不断的循环执行可以看到,出现的结果会出错,当先执行了j=a(此时a=0)在执行了a=1,i=b(此时b=0),b=1,最后就会导致i=0,j=0

volatile禁止指令重排序

使用volatile可以实现禁止指令重排序,从而确保并发安全,那么volatile是如何实现禁止指令重排序呢?就是通过使用内存屏障(Memory Barrier)。

内存屏障(Memory Barrier) 作用
  • 内存屏障****能够阻止屏障两侧的指令重排序,能够让cpu或者编译器在内存上的访问是有序的。
  • 强制把写缓冲区/高速缓存中的脏数据写回主内存,或让缓存相应的数据失效。他是一种cpu指令,用来控制特定情况下的重排序和内存可见性问题。
volatile内存屏障的插入策略

硬件层的内存屏障(Memory Barrier)有Load Barrier 和 Store Barrier即读屏障和写屏障。

Java内存屏障

  • StoreStore屏障:确保在该屏障之后的第一个写操作之前,屏障前的写操作对其他处理器可见(刷新到内存)。
  • StoreLoad屏障:确保写操作对其他处理器可见(刷新到内存)之后才能读取屏障后读操作的数据到缓存。
  • LoadLoad屏障:确保在该屏障之后的第一个读操作之前,一定能先加载屏障前的读操作对应的数据。
  • LoadStore屏障:确保屏障后的第一个写操作写出的数据对其他处理器可见之前,屏障前的读操作读取的数据一定先读入缓存。

在volatile修饰的变量进行写操作时候,会使用StoreStore屏障和StoreLoad屏障,进行对volatile变量读操作会在之后使用LoadLoad屏障和LoadStore屏障。

【多线程与高并发】- 浅谈volatile的更多相关文章

  1. 浅谈Volatile与多线程

        标题:浅谈Volatile与多线程 2011-04-19 22:49:17 最近看的比较杂,摘了一些人的笔记!随着多核的日益普及,越来越多的程序将通过多线程并行化的方式来提升性能.然而,编写正 ...

  2. 互联网大厂高频重点面试题 (第2季)JUC多线程及高并发

    本期内容包括 JUC多线程并发.JVM和GC等目前大厂笔试中会考.面试中会问.工作中会用的高频难点知识.斩offer.拿高薪.跳槽神器,对标阿里P6的<尚硅谷_互联网大厂高频重点面试题(第2季) ...

  3. 多线程与高并发(一)—— 自顶向下理解Synchronized实现原理

    一. 什么是锁? 在多线程中,多个线程同时对某一个资源进行访问,容易出现数据不一致问题,为保证并发安全,通常会采取线程互斥的手段对线程进行访问限制,这个互斥的手段就可以称为锁.锁的本质是状态+指针,当 ...

  4. 一篇博客带你轻松应对java面试中的多线程与高并发

    1. Java线程的创建方式 (1)继承thread类 thread类本质是实现了runnable接口的一个实例,代表线程的一个实例.启动线程的方式start方法.start是一个本地方法,执行后,执 ...

  5. java后端知识点梳理——多线程与高并发

    进程与线程 进程是一个"执行中的程序",是系统进行资源分配和调度的一个独立单位 线程是进程的一个实体,一个进程中一般拥有多个线程. 线程和进程的区别 进程是操作系统分配资源的最小单 ...

  6. 使用Redis中间件解决商品秒杀活动中出现的超卖问题(使用Java多线程模拟高并发环境)

    一.引入Jedis依赖 可以新建Spring或Maven工程,在pom文件中引入Jedis依赖: <dependency> <groupId>redis.clients< ...

  7. 浅谈volatile关键字

    volatile是一种轻量级的同步机制.它可以保证内存可见性以及防止指令重排序,但是不保证原子性 volatile和JMM机制是不可分割的,在谈volatile的时候有必要先了解以下JMM JMM(J ...

  8. C#网络编程 多线程和高并发

    在任何 TCP Server 的实现中,一定存在一个 Accept Socket Loop,用于接收 Client 端的 Connect 请求以建立 TCP Connection. 在任何 TCP S ...

  9. 多线程与高并发(四)volatile关键字

    上一篇学习了synchronized的关键字,synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁,而volatile是一个轻量级的同步机制. 前面学习了Java的内存模型,知 ...

  10. JAVA多线程下高并发的处理经验

    java中的线程:java中,每个线程都有一个调用栈存放在线程栈之中,一个java应用总是从main()函数开始运行,被称为主线程.一旦创建一个新的线程,就会产生一个线程栈.线程总体分为:用户线程和守 ...

随机推荐

  1. 腾讯云数据库SaaS致力于构建数据库分布式云,为更多更广的用户提供服务

    大数据时代,数据库 SaaS 是企业实现降本增效和业务创新的重要抓手.在腾讯全球数字生态大会数据库 SaaS 专场上,腾讯云发布了多项数据库 SaaS 产品能力升级,并重点分享了其在上云.日常运维.数 ...

  2. sql注入的一丢丢

  3. 【kafka】connect的timestamp模式无法同一秒插入多条记录问题解决

    一.现在问题 同时插入多条时间戳相同的记录 INSERT INTO "ABANK" VALUES ('1', 'CH', '00211', 'UBS Switzerland AG' ...

  4. python 异步写入文件

    # -*- coding:utf-8 -*-import asyncioimport aiofilesimport time#异步操作时,函数名前必须加上asyncasync def func1(): ...

  5. nuxt之vuex的使用

    先来了解一下官网:https://www.nuxtjs.cn/guide/vuex-store 一.首先在 store 文件下新建一个index.js文件 const state = () => ...

  6. [机器学习] Yellowbrick使用笔记1-快速入门

    Yellowbrick是一个机器学习可视化库,主要依赖于sklearn机器学习库,能够提供多种机器学习算法的可视化,主要包括特征可视化,分类可视化,回归可视化,回归可视化,聚类可视化,模型选择可视化, ...

  7. HBase详解(02) - HBase-2.0.5安装

    HBase详解(02) - HBase-2.0.5安装 HBase安装环境准备 Zookeeper安装 Zookeeper安装参考<Zookeeper详解(02) - zookeeper安装部署 ...

  8. 震网(Stuxnet)病毒深度解析:首个攻击真实世界基础设施的病毒

    摘要:震网病毒主要是通过改变离心机的转速,来破坏离心机,并影响生产的浓缩铀质量. 本文分享自华为云社区<[安全技术]震网(Stuxnet)病毒深度解析:首个攻击真实世界基础设施的病毒(1)[原创 ...

  9. Java实现BP神经网络MNIST手写数字识别

    Java实现BP神经网络MNIST手写数字识别 如果需要源码,请在下方评论区留下邮箱,我看到就会发过去 一.神经网络的构建 (1):构建神经网络层次结构 由训练集数据可知,手写输入的数据维数为784维 ...

  10. MySQL 合并查询union 查询出的行合并到一个表中

    在合并查询中,尤其是二分类的情况,在查询结果是相同列名的时候可以考虑合并查询.先查询出行的结果,再使用union或者union all合并查询结果. 另外如果 union 和 order by 一起使 ...