在 Java 多线程中如何保证线程的安全性?那我们可以使用 Synchronized 同步锁来给需要多个线程访问的代码块加锁以保证线程安全性。使用 synchronized 虽然可以解决多线程安全问题,但弊端也很明显:加锁后多个线程需要判断锁,较为消耗资源。所以就引出我们今天的主角——volatile 关键字,一种轻量级的解决方案。

首先我们得了解量两个概念:多线程和 JMM。

多线程

进程和线程的概念

创建线程的两种方法

线程的生命周期

Java 内存模型(JMM)

JMM 的概念

JMM 的结构组成部分

volatile 关键字作用

内存可见性

禁止指令重排

1、多线程

(1)进程和线程

进程:一个正在执行中的程序,动态的,是系统进行资源分配和调度的独立单位。

线程:进程中一个独立的控制单元,线程控制着进程的执行。一个进程中至少有一个线程。

(2)创建线程:(Thread 和 Runable)

继承 Thread 类三步走:定义类继承 Thread 类、重写 run 方法、调用线程的 start 方法。

public class ThreadDemo {

public static void main(String[] args) {

// step2:创建该类的对象

Lefthand left = new Lefthand();

Righthand right = new Righthand();

// step3:调用start方法启动线程

left.start();

right.start();

}

}

// step1:继承Thread类,在子类中必须实现run方法

class Lefthand extends Thread {

public void run() {

for (int i = 0; i < 6; i++) {

System.out.println("You are Students!");

try {

sleep(500);

} catch (InterruptedException e) {

}

}

}

}

class Righthand extends Thread {

public void run() {

for (int i = 0; i < 6; i++) {

System.out.println("I am a Teacher!");

try {

sleep(300);

} catch (InterruptedException e) {

}

}

}

}

实现 Runable 接口三步走:定义类实现 Runable 接口、实现 run 方法、通过 Thread 类建立线程对象、start方法。

public class TwoThreadsDemo2 {

public static void main(String[] args) {

SimpleThread2 th1 = new SimpleThread2("Jack");

SimpleThread2 th2 = new SimpleThread2("Tom");

// step3

Thread thread1 = new Thread(th1);

Thread thread2 = new Thread(th2);

thread1.start();

thread2.start();

}

}

// step1

class SimpleThread2 implements Runnable {

String name;

public SimpleThread2(String str) {

name = str;

}

// step2

public void run() {

for (int i = 0; i < 8; i++) {

System.out.println(i + " " + name);

try {

Thread.sleep((long) (Math.random() * 1000));

} catch (InterruptedException e) {

}

}

System.out.println("DONE!" + name);

}

}

两种方式的区别:

实现方式避免了单继承的局限性,线程代码存在接口子类的 run 方法中;继承方式线程代码存放在 Thread 子类的 run 方法中。

(3)线程的生命周期:就绪状态(线程 new 后)、可执行状态(start 方法启动线程,调用 run 方法)、阻塞状态(sleep 方法 和 wait 方法)、死亡状态(stop 方法)

2、Java 内存模型

(1)概念:Java 虚拟机定义的一种抽象规范,使 Java 程序在不同平台上的内存访问效果一致。它决定一个线程对共享变量的写入何时对另一个线程可见。

(2)结构组成:(类比 CPU、高速缓存 、内存 间的关系)

主内存:所有线程共享;共享变量在主内存中存储的是其“本身”;

工作内存:每个线程有自己的工作空间;共享变量在主内存中存储的是其“副本”;

线程对共享变量的所有操作全在工作内存中进行;每个线程只能访问自己的工作内存;变量值的传递只能通过主内存完成。

3、volatile 关键字(用来修饰被不同线程访问和修改的变量)

(1)内存可见性:

某线程对 volatile 变量的修改,对其他线程都是可见的。即获取 volatile 变量的值都是最新的。

Java 中存在一种原则——先行发生原则(happens-before)。其表示两个事件结果之间的关系:如果一个事件发生在另一个事件之间,其结果必须体现。volatile 的内存可见性就体现了该原则:对于一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

例:

volatile static int a = 0;

//线程 A 在其工作内存中写入变量 a 的新值 1

a = 1 ;

//线程 B 在主内存中读取变量 a 的值输出

System.out.println(a);

需要注意的是 volatile 能保证内存的可见性,但不能保证变量的原子性。

某一线程从主内存获取到共享变量的值,当其修改完变量值重新写入主内存时,并没有去判断主内存的值是否发生改变,有可能会出现意料之外的结果。

例如:当多个线程都对某一 volatile 变量(int a=0)进行 count++ 操作时,由于 count++ 操作并不是原子性操作,当线程 A 执行 count++ 后,A 工作内存其副本的值为 1,但线程执行时间到了,主内存的值仍为 0 ;线程 B又来执行 count++后并将值更新到主内存,主内存此时的值为 1;然后线程 A 继续执行将值更新到主内存为 1,它并不知道线程 B 对变量进行了修改,也就是没有判断主内存的值是否发生改变,故最终结果为 1,但理论上 count++ 两次,值应该为 2。

所以要使用 volatile 的内存可见性特性的话得满足两个条件:

能确保只有单一的线程对共享变量的只进行修改。

变量不需要和其他状态变量共同参与不变的约束条件。

(2)禁止指令重排:

指令重排:JVM 在编译 Java 代码时或 CPU 在执行 JVM 字节码时,对现有指令顺序进行重新排序,优化程序的运行效率。(在不改变程序执行结果的前提下)

指令重排虽说可以优化程序的执行效率,但在多线程问题上会影响结果。那么有什么解决办法呢?答案是内存屏障。内存屏障是一种屏障指令,使 CPU 或编译器对屏障指令之前和之后发出的内存操作执行一个排序的约束。

四种类型:LoadLoad 屏障、StoreStore 屏障、LoadStore 屏障、StoreLoad 屏障。(Load 代表读取指令、Store 代表写入操作)

在 volatile 变量上的体现:(JVM 执行操作)

在每个 volatile 写入操作前插入 StoreStore 屏障;

在写操作后插入 StoreLoad 屏障;

在读操作前插入 LoadLoad 屏障;

在读操作后插入 LoadStore 屏障;

volatile 禁止指令重排在单例模式上有所体现,之前文章有所介绍(链接)。上边介绍的操作只是针对 volatile 读和 volatile 写这种组合情况。还有其他的情况就不一一展开了。

总结:

(1)内存可见性的保证是基于屏障指令的。

(2)禁止指令重排在编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织重排。

(3)synchronized 关键字可以保证变量原子性和可见性;volatile 不能保证原子性。

volatile 关键字:

当多个线程进行操作共享数据时,可以保证内存中的数据可见。 相较于 synchronized 是一种较为轻量级的同步策略。

缺点:

1. volatile 不具备“互斥性”

2. volatile 不能保证变量的“原子性”

内存可见性是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的。

我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的 volatile 变量。

下面用一个案例说明,代码如下:

public class TestVolatile {

public static void main(String[] args) {

ThreadDemo td = new ThreadDemo();

new Thread(td).start();

while (true) {

if (td.isFlag()) {

System.out.println("------------------进来");

}

}

}

}

class ThreadDemo implements Runnable {

private boolean flag = false;

@Override

public void run() {

// 延迟一秒

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

}

flag = true;

System.out.println("flag=" + isFlag());

}

public boolean isFlag() {

return flag;

}

public void setFlag(boolean flag) {

this.flag = flag;

}

}

flag是共享数据存在于主存中,ThreadDemo启动以后主要是来改变flag的值,而main函数本身也是一个线程来读取flag的值。为了让效果更加明显,在ThreadDemo线程里面run方法里面休眠了1秒,目的是先让flag的值先改变,不然main线程里面可能会先读取到flag的值。按理说ThreadDemo线程先改变了flag的值为true,然后while(true)里面读取flag的值应该是true,然后输出,但是运行结果并不是这样。

当一个共享变量被volatile修饰时,外汇返佣它会保证修改的值立即被更新到主存“, 这里的”保证“ 是如何做到的?和 JIT的具体编译后的CPU指令相关吧?

  volatile特性

  内存可见性:通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。

  volatile的使用场景

  通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,但是使用volatile必须满足两个条件:

  1、对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的;

  2、该变量没有包含在具有其它变量的不变式中,这句话有点拗口,看代码比较直观。

public class NumberRange {

  private volatile int lower = 0;

  private volatile int upper = 10;

  public int getLower() { return lower; }

  public int getUpper() { return upper; }

  public void setLower(int value) {

     if (value> upper)

     throw new IllegalArgumentException(...);

     lower = value;

  }

  public void setUpper(int value) {

     if (value < lower)

     throw new IllegalArgumentException(...);

     upper = value;

   }

}

上述代码中,上下界初始化分别为0和10,假设线程A和B在某一时刻同时执行了setLower(8)和setUpper(5),且都通过了不变式的检查,设置了一个无效范围(8, 5),所以在这种场景下,需要通过sychronize保证方法setLower和setUpper在每一时刻只有一个线程能够执行。

上述如果没有了解volatile的作用,那么看下下面的例子可以看出volatile在实际中的作用

下面是我们在项目中经常会用到volatile关键字的两个场景:

  1、状态标记量

  在高并发的场景中,通过一个boolean类型的变量isopen,控制代码是否走促销逻辑,该如何实现?

public class ServerHandler {

  private volatile isopen;

  public void run() { 

if (isopen) {

//促销逻辑     

} else {

//正常逻辑   

}  

}

public void setIsopen(boolean isopen) {

this.isopen = isopen    

}      

}

上述一个简单的案例我们可以清楚的看到,现实场景中用户执行了多线程中run()方法,如果需要开启促销逻辑,那么只需要后台设置调用setIsopen(true) 方法,就能很好的控制多线程中方法控制的问题了,该放说明volatile关键字的作用就是告诉该执行方法时时获取最新变量值。

Java中volatile关键字及其作用是什么?的更多相关文章

  1. Java 中 volatile 关键字及其作用

    引言 作为 Java 初学者,几乎从未使用过 volatile 关键字.但是,在面试过程中,volatile 关键字以及其作用还是经常被面试官问及.这里给各位童靴讲解一下 volatile 关键字的作 ...

  2. 深入解析Java中volatile关键字的作用

    转(http://m.jb51.net/article/41185.htm)Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制 在java线 ...

  3. java中volatile关键字的作用

    一.内存模型的相关概念 大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存 ...

  4. 【转】java中volatile关键字的含义

    java中volatile关键字的含义   在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  5. 转:java中volatile关键字的含义

    转:java中volatile关键字的含义 在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  6. 【Java_基础】Java中Native关键字的作用

    本篇博文转载与:Java中Native关键字的作用

  7. Java中Volatile关键字详解 (转自郑州的文武)

    java中volatile关键字的含义:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html 一.基本概念 先补充一下概念:J ...

  8. 就是要你懂Java中volatile关键字实现原理

    原文地址http://www.cnblogs.com/xrq730/p/7048693.html,转载请注明出处,谢谢 前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是j ...

  9. 【转】Java学习---Java中volatile关键字实现原理

    [原文]https://www.toutiao.com/i6592879392400081412/ 前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.c ...

随机推荐

  1. vue ui 开启无效记录

    换了台电脑,输入vue ui 无法开启图形化界面 1.首先vue ui 没成功 我找到vue.cmd路径配置到环境变量依旧没有解决 然后使用vue -h 显示没有vue ui这个命令 重新安装npm ...

  2. Ansible自动化部署K8S集群

    Ansible自动化部署K8S集群 1.1 Ansible介绍 Ansible是一种IT自动化工具.它可以配置系统,部署软件以及协调更高级的IT任务,例如持续部署,滚动更新.Ansible适用于管理企 ...

  3. Oracle - 单表查询相关

    -- 单表查询 -- 查询表的所有数据, *代表所有 -- select * from [表名]; -- 查询表中指定字段的值 -- select [字段1], [字段2] ... from [表名] ...

  4. 阿里云高磊:API网关加速能力聚合与生态集成

    导读:本文中,阿里云高级技术专家高磊(花名:埃兰)将聚焦API网关加速能力聚合与生态集成,讲述API如何实现系统间的衔接和API网关的产品升级进程,重点展示了一些新功能.新体验和新变化. 大家下午好, ...

  5. 【dart学习】-- Dart之元数据

    一,概述 元数据概述  元数据(Metadata),又称中介数据.中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置.历 ...

  6. CTSC2019

    (upd:随机立方体AC) 太菜了只会部分分.以后慢慢补坑吧…… 随机立方体: 30分: 正常人都能想到的的转移状态(我的确是弱智),从大往小填数,记录有多少个极大值点和三个方向上各占了多少.转移可以 ...

  7. delphi dll创建及调用

    第一章 DLL简单介绍由于在目前的学习工作中,需要用到DLL文件,就学习了下,在这里作个总结.首先装简单介绍下DLL:1,减小可执行文件的大小DLL技术的产生有很大一部分原因是为了减小可执行文件的大小 ...

  8. 一个类似indexOf()的功能的函数

    之前面试的时候遇到了这样的一道题,不过写的时候有些细节没注意到,现在重新写了一下. 写一个类似indexOf()的功能的函数 var str = "dafdfgvdahjfbhyuyvtur ...

  9. Python的从头再来

    虽然各种视频,文档看了不少.但是都没有系统的总结.现在要把Python从最基础开始总结,回归.也当作自己的复习.

  10. (9)centos7 安装与解压

    1.zip/unzip zip 新file 旧file或文件夹 # 把旧文件和文件夹压缩成新文件 -r是文件夹下所有文件 zip -r a.zip ./doc #压缩当前目录 doc下的所有文件变成 ...