浅析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)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 流程图的绘图规范_流程图绘制的基本规则

    流程图的绘图规范_流程图绘制的基本规则画了多年的流程图,你真的画规范了吗?|人人都是产品经理流程有哪些作用?我们为什么要画流程图呢?正确的画流程图规范是什么?流程图是一个很强大的工具,在我们的日常工作中经常会使用到。但我们也发现,有时看到别人流程图的画法、规范都不太一样,这是为什么呢?难道流程图就没有统一的标准或规范吗?基于这个疑问,我出于好奇认http://www.woshipm.com/zhichang/2329530.html以上为笔记来源出!一、流程图的符号要求 有几个重要且常用的符号:…

    2025年7月15日
    3
  • html右浮动代码_html+css+js

    html右浮动代码_html+css+js网上一般的网页浮动广告代码都是基于jquery的,受jiquery版本及浏览器限制,非常容易出现不兼容的情况,本站分享一段纯js网页浮动广告代码,简单兼容。html代码:javascript代码:window.onload=function(){varx=50,y=60;varxin=true,yin=true;varstep=1;vardelay=10;varobj=docu…

    2022年9月20日
    4
  • 医疗大数据平台的主流解决方案

    医疗大数据平台的主流解决方案多源异构数据汇聚分发系统:通过数据汇集和分发服务引擎,按照统一的数据格式和接口规范采集来自于不同厂家、不同设备类型、不同数据格式、不同传输协议的体征数据,然后进行数据存储,最后通过消息开放服务中间件实时分发至电子健康档案系统。   统一资源池的电子健康档案系统:电子健康档案系统是实施医疗信息 化管理和提供个性化医护服务的核心,它以用户或患者为中心,建立人口统计信息、既往病史、健康因素、家…

    2022年5月5日
    40
  • idea for mac 快捷键设置_macidea使用快捷键

    idea for mac 快捷键设置_macidea使用快捷键最强IDEA关于Mac版本的常用快捷键

    2022年8月31日
    6
  • oracle锁表处理三步骤

    oracle锁表处理三步骤selectsession_idfromv$locked_object; –425SELECTsid,serial#,username,osuserFROMv$sessionwheresid=425;ALTERSYSTEMKILLSESSION’425,9613′;

    2022年6月16日
    33
  • JQuery的安装与下载教程

    JQuery的安装与下载教程网页中添加jQuery可以通过多种方法在网页中添加jQuery。您可以使用以下方法:从jquery.com下载jQuery库 从CDN中载入jQuery,如从Google中加载jQuery下载jQuery有两个版本的jQuery可供下载:Productionversion-用于实际的网站中,已被精简和压缩。 Developmentversion-用于测试和开发(未压缩,是可读的代码)以上两个版本都可以从jquery.com中下载。…

    2022年5月3日
    44

发表回复

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

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