浅析Java中volatile关键字及其作用

浅析Java中volatile关键字及其作用在Java多线程中如何保证线程的安全性?那我们可以使用Synchronized同步锁来给需要多个线程访问的代码块加锁以保证线程安全性。

大家好,又见面了,我是你们的朋友全栈君。

在 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、高速缓存 、内存 间的关系)

浅析Java中volatile关键字及其作用

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

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

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

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 不能保证原子性。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/138323.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • GoLand 2021.2.3 激活码(在线激活)

    GoLand 2021.2.3 激活码(在线激活),https://javaforall.net/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

    2022年3月14日
    521
  • linux修改mysql数据库密码

    linux修改mysql数据库密码linux修改mysql数据库密码

    2022年4月24日
    50
  • Linux3.10.0块IO子系统流程(7)– 请求处理完成

    Linux3.10.0块IO子系统流程(7)– 请求处理完成

    2022年4月3日
    33
  • Java面试题–较经典

    Java面试题–较经典1、出处:2016年360Java面试题:原题:首先 代码跑一边 保证正确性。分析:往方法中传参,传的仅仅只是地址,而不是实际内存,所以不要以为y=x程序的执行,是 b=a的执行。这两者是不相等的。 2、出处:2016年 阿里巴巴Java面试题:原题:分析:本题是一个自动拆装箱的考题(自动拆装箱JDK需在1.5上)参考:https://blog….

    2022年6月13日
    31
  • 弄懂SPI接口

    弄懂SPI接口SPI(SerialPeripheralInterface,串行外设接口)是Motorola公司提出的一种同步串行数据传输标准,在很多器件中被广泛应用。1.接口SPI接口经常被称为4线串行总线,以主/从方式工作,数据传输过程由主机初始化。如图1所示,其使用的4条信号线分别为:1)SCLK:串行时钟,用来同步数据传输,由主机输出;2) MOSI:主机输出从

    2022年6月18日
    44
  • 三点估算法怎么计算_比例估算法公式

    三点估算法怎么计算_比例估算法公式某公司接到一栋大楼的布线任务,经过分析决定将大楼的四层布线任务分别交给甲、乙、丙、丁四个项目经理,每人负责一层布线任务,每层面积为10000平米。布线任务由同一个施工队施工,该工程队有5个施工组。甲经过测算,预计每个施工组每天可以铺设完成200平米,于是估计任务完成时间为10天,甲带领施工队最终经过14天完成任务;乙在施工前咨询了工程队中有经验的成员,经过分析之后估算时间为12天,乙带领施工队最终…

    2022年10月30日
    0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号