浅析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)
上一篇 2022年5月31日 下午4:46
下一篇 2022年5月31日 下午4:46


相关推荐

  • Docker OpenClaw 生产环境部署指南(单机架构版)

    Docker OpenClaw 生产环境部署指南(单机架构版)

    2026年3月13日
    3
  • java 时间取整_java 小时时间就近取整[通俗易懂]

    /***时间就近取整*08:00->08:00,*08:20->08:30,*08:30->08:30,*08:45->09:00,*23:56->00:00**@paramtime*@returnoutTime*/publicstaticStringgetCompleteTime(Stringtime){String…

    2022年4月16日
    135
  • thinkphp一键清除缓存的方法

    thinkphp一键清除缓存的方法

    2021年11月3日
    56
  • 12864驱动例程

    12864驱动例程以下为并行方式的范例程序 液晶模块型号 DV12864G 11 或 G 11 不带字库 接口方式 并行接口 6800 时序 驱动 IC 是 ST7565R 本程序所带的 8×16 点阵及 5×8 点阵的 ASCII 码字符的数据都是从 DV GB2312 型号字库 IC 里读出来的国标的 include reg51 H sbitlcd cs1 P3 4 接口定义 CS 片选 sbitlcd reset P3 5 接口定义 RESET reg51 H

    2026年3月26日
    2
  • Centos查看系统版本「建议收藏」

    Centos查看系统版本「建议收藏」1.查看已经安装的CentOS版本信息CentOS的版本号信息一般存放在配置文件当中,在CentOS中,与其版本相关的配置文件中都有centos关键字,该文件一般存放在/etc/目录下,所以说我们可以直接在该文件夹下搜索相关的文件。其中存放其版本配置信息的文件为“centos-release”,翻译过来就是“CentOS的发行版”,所以说我们可以在这里查看CentOS相应的版本信息。upstream,上游的意思,centos是由RHEL衍生而来。ll/et…

    2022年6月24日
    99
  • golang 2021激活码【2021免费激活】

    (golang 2021激活码)2021最新分享一个能用的的激活码出来,希望能帮到需要激活的朋友。目前这个是能用的,但是用的人多了之后也会失效,会不定时更新的,大家持续关注此网站~IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html…

    2022年3月26日
    128

发表回复

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

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