在 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. 带你看懂LayoutInflater中inflate方法

    关于inflate问题,我想很多人多多少少都了解一点,网上也有很多关于这方面介绍的文章,但是枯燥的理论或者翻译让很多小伙伴看完之后还是一脸懵逼,so,我今天想通过三个案例来让小伙伴彻底的搞清楚这个东东 ...

  2. Codeforces 1163F 最短路 + 线段树 (删边最短路)

    题意:给你一张无向图,有若干次操作,每次操作会修改一条边的边权,每次修改后输出1到n的最短路.修改相互独立. 思路:我们先以起点和终点为根,找出最短路径树,现在有两种情况: 1:修改的边不是1到n的最 ...

  3. C++如何阻止一个类被实例化

    (1)定义一个无用的抽象函数,使得类成为抽象类 (2)将构造函数定义为private. 为什么要这样做? 一些工具类,没有被实例化的必要.

  4. linux上文件内容去重的问题uniq/awk 正则表达过滤操作

    .uniq:只会对相邻的行进行判断是否重复,不能全文本进行搜索是否重复,所以往往跟sort结合使用. 例子1: [root@aaa01 ~]# cat a.txt 12 34 56 12 [root@ ...

  5. 【leetcode】388. Longest Absolute File Path

    题目如下: Suppose we abstract our file system by a string in the following manner: The string "dir\ ...

  6. Andrdoid中对应用程序的行为拦截实现方式之----从底层C进行拦截

    之前的一篇概要文章中主要说了我这次研究的一些具体情况,这里就不在多说了,但是这里还需要指出的是,感谢一下三位大神愿意分享的知识(在我看来,懂得分享和细致的人才算是大神,不一定是技术牛奥~~) 第一篇: ...

  7. delphi 时间

    DELPHI高精度计时方法 //取毫秒级时间精度(方法一): var t1,t2:int64; r1:int64; begin t1:=GetTickCount;//获取开始计数 WINDOWS AP ...

  8. android ellipsize的使用及实现跑马灯效果总结

    参考资料: http://blog.csdn.net/huiwolf2008/article/details/7901084 http://www.cnblogs.com/Gaojiecai/arch ...

  9. Linux内核知识杂记

    1.内核调试手段 1.printk打印内核状态 2.产生opps时使用GDB查看调用栈 2.内核空间和用户空间区别,通信方式有哪些? Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此,L ...

  10. Openstack组件部署 — Nova_安装和配置Controller Node

    目录 目录 前文列表 Prerequisites 先决条件 To create the databases To create the service credentials Create the C ...