首页 > 编程语言 >[Java]volatile关键字

[Java]volatile关键字

时间:2024-04-20 09:11:09浏览次数:27  
标签:Java Thread t2 关键字 static 线程 内存 volatile

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/18031966
出自【进步*于辰的博客

启发博文:《Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)》(转发)。

参考笔记二,P73、P74.1。

目录
在学习此关键字之前,我们先了解一下JMM规范和并发编程中的三个概念。

1、JMM规范

JMM(Java module memory,Java内存模型)是一个抽象概念,并不真实存在于内存。它是用于定义程序中各个变量(成员变量、类变量、数组元素等)的一组规范和规则,指定变量的访问方式。

规定

  1. 线程解锁之前必须将共享变量刷新回主内存。
  2. 线程加锁之前必须读取主内存中变量的最新值到工作空间。
  3. 解锁和加锁必须是同一把锁。

大家可能不解其意,这就需要涉及另一个概念:线程空间.。

什么是线程空间?程序执行JMM规范的实体是线程,当线程创建时,JMM会为其创建一个私有内存(也称为工作内存、本地内存或栈空间)。JMM规定所有变量都保存在主内存,线程访问变量时需为变量创建一个副本至工作内存进行操作,完成后将变量值返回主内存,且线程通信在主内存进行。

注意:JMM作为抽象概念,规定中的“主内存”与“私有内存”等概念同样是抽象概念,并不一定真实对应CPU中的缓存和物理内存。

2、并发编程的三个概念

1:可见性
可见性指线程对变量的修改,其他线程可见,即由volatile修饰的变量,其私有内存失效。(具体说明见下文)

2:原子性
原子性指线程对变量的操作的整个过程不会被阻塞或分割。

原子性表示“拒绝多线程并发操作”,即同一时刻只能有一个线程进行操作。因此,在整个操作过程中不会被线程调度中断。

如:a = 1是原子操作,而a++不是,因为它分为读取、计算和赋值三个步骤。

在Java中,原子操作包括:

  1. 基本数据类型的读取与赋值,但限于将值赋给变量,变量间赋值不是原子操作。
  2. 引用赋值。
  3. java.util.concurrent.atomic包中所有类的一切操作。

3:有序性
有序性也称为“指令重排”,指程序运行时,编译器基于提高性能需要,以指令间的数据依赖性作为依据对指令进行重新排列。执行顺序:编译器重排 → 指令并行重排 → 内存系统重排

单线程环境下,无论指令如何重排,结果都不变,但多线程时可能会出现问题。

3、volatile

3.1 介绍

volatile是一种轻量级的同步机制,与synchronized有一些通性,但synchronized属于重量级(“级”是指对变量访问的限制程度)。

volatile遵循JMM规范实现了可见性和有序性,但不保证原子性。因此,限制线程在访问由volatile修饰的变量时,从主内存获取数据,而不是从工作内存。在数据操作完成后再刷新回主内存,故在保证原子性的情况下,线程安全。

如何保证原子性?
两种方法:

  1. 程序中不存在多线程对变量进行非原子性操作。
  2. 见下文。

3.2 volatile原理

在JVM底层,volatile是采用“内存屏障”来实现的。在所生成的汇编代码中可见,在volatile前多出了一条Lock前缀指令,这相当于内存屏障(也称为“内存栅栏”),其提供三项功能:

  1. 屏蔽指令重排。
  2. 强制私有内存的修改立即写入主内存。
  3. 执行写操作时,致使CPU中其他线程的私有内存无效。

这就是为何volatile可实现可见性和有序性,但不保证原子性的原因。

3.3 如何保证原子性?

大家先看个示例。

static volatile int x = 0;
private static void add() {
    x++;
}

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    Thread t2 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

请问最后的x200000吗?绝大概率不是,因为volatile不能保证原子性,而x++又不是原子操作,可谓必然会出现并发问题。

那么,volatile如何保证原子性?从上文【volatile原理】可得,volatile本身是无法保证原子性的,故需要采取其他方案。如下:

1:synchronized。

static int x = 0;
private synchronized static void add() {
    x++;
}

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    Thread t2 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

2:Lock。

static int x = 0;
// Lock的原理类似synchronized
static Lock lock = new ReentrantLock();

private static void add() {
    lock.lock();// 可视为内存屏障”,当然实际不是
    x++;
    lock.unlock();
}

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    Thread t2 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

3:AtomicInteger。

// 原子操作类是通过CAS循环的方式来保证原子性
static AtomicInteger x = new AtomicInteger();

private static void add() {
    x.getAndIncrement();
}

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    Thread t2 = new Thread(() -> {
        int i = 100000;
        while (i-- > 0) {
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

4、volatile的一个经典运用

从文章《[Java]单例模式》中截取这段代码:

public static Singleton newInstance() {
    if (instance == null) {--------------------A
        synchronized (Singleton.class) {-------B
            if (instance == null) {------------C
                instance = new Singleton();----D
            }
        }
    }
    return instance;---------------------------E
}

DCL,可解决“懒汉式”存在的线程安全问题。不过,仍有不足,问题在于D。

因为,实例化分为三步:

  1. 创建实例,分配内存。
  2. 实例初始化。
  3. instance指向此实例。

其中,2和3都依赖于1,而2与3之间没有依赖关系,故指令重排可能会将2与3调换。

调换有什么后果?假设一种情况,线程x调用newInstance(),执行D,但还未进行实例初始化(已执行了1、3),此时线程y调用newInstance(),判断A为false,直接执行E,此时返回的instance未初始化,导致异常。

异常出现的原因就是指令重排,用volatile禁止指令重排即可解决(用volatile修饰instance)。

最后

本文中的例子,是为了阐述volatile关键字和方便大家理解而简单举出的,不一定有实用性。

本文完结。

标签:Java,Thread,t2,关键字,static,线程,内存,volatile
From: https://www.cnblogs.com/cnb-yuchen/p/18031966

相关文章

  • GraalVM-云原生时代的JVM(Java)
    一、GraalVM是什么?GraalVM是Oracle开源的一款通用虚拟机产品,官方称之为UniversalGraalVM,是新一代的通用多语言高性能虚拟机。它可以运行多种编程语言,如Java、JavaScript、Python等,并提供了即时编译(JIT)和AOT编译(AOT)的支持。GraalVM还支持在不同语言之间互相调用,以及嵌入到其他......
  • java spring boot 2 开发实战笔记
    本案例是java spingboot 2.2.1  第一步搭建环境:安装依赖由于我们公司项目是1.8环境不能乱,我现在自己的电脑是1.8环境,所以本次整理的boot代码也只能用1.8boot版本为:2.2.1,新建项目后,在xml文件中复制上以下代码xml配置,最精简运行起来的  需要配置一个数据库,8.0以......
  • springboot java调用flask python写的
    服务a用flask,服务b用的springboot,服务a写的接口,用python很容易就调通了,java来调,坑有点多1、url最后的斜杠必须两边对应上,否则flask会先308,而且contenttype[text/html;charset=utf-8],连对应的HttpMessageConverter都没有org.springframework.web.client.RestClientException:......
  • JavaScript 的 Mixin 问题
    JavaScript从ES6开始支持class了,如何在现在的class上实现mixin呢?很多人推荐这种搞法Object.assign(MyClass.prototype,MyMixin);这个做法很丑,不能令人满意。我找到了一个更有趣的做法,和dart比较接近:"Real"MixinswithJavaScriptClasses他最终的做法是......
  • JavaWeb技术
    JavaWeb技术1、统一了项目的整体结构(标准化)。2、可以动态的加载jar包(导入依赖)。jdbc技术-----导入jar包---mysql数据库驱动包。3、便于项目的打包、部署、发布。一、JSP简介JSP其实就是JavaServerPages的缩写,是一种动态网页技术。能够支持的编程语言只有Java程序。......
  • JavaSE【9】-Java多线程
    JavaSE【9】-Java多线程synchronized修饰符(方法)------表示这个方法被同步了,就是基于线程安全的;集合容器----有一些集合容器是基于线程同步的(集合的内部使用的方法是基于synchronized来修饰的);一、线程相关概念进程和线程的概念:◆进程就是正在执行的程序,一个进程通常就是一个......
  • JavaScript技术
    JavaScript技术一、JavaScript的定义JavaScript是一种【基于对象】和【事件驱动】的【脚本语言】,在客户端执行,客户端主要实现数据的验证和页面的特效,大幅度提高网页的速度和交互的能力,在互联网中得到了广泛的运用。基于对象:js是基于面向对象的。事件驱动:使用的时候是结合......
  • day16_我的Java学习笔记 (Set、案例、Collections、Map、集合嵌套)
    1.Set系列集合1.1Set系列集系概述1.2HashSet元素无序的底层原理:哈希表JDK1.7HashSet原理解析:JDK1.8HashSet原理解析:1.3HashSet元素去重复的底层原理Set集合去重复的原因,先判断哈希值,再判断equals重写equals()和HashCode()方......
  • 记录在JavaScript中对事件循环的理解
    JavaScript事件循环通俗解释好的,用更通俗的话来说,事件循环就像是在一个大剧院里,有一个演员(JavaScript引擎)和两个重要的角色:一个是前台的表演者(调用栈),另一个是后台的候场区(事件队列)。前台表演者:这个演员在前台表演,一次只能表演一个节目(单线程执行)。当一个节目(函数)开始时,演员就上......
  • 前端如何使用Javascript实现一个简单的发布订阅模式
    在前端开发中,我们经常需要处理事件的订阅与发布,以实现组件之间的解耦和通信。本文将介绍如何使用JavaScript实现一个简单的发布订阅模式,通过分步写代码的方式,带领读者一步步完成实现过程。步骤一:定义EventEmitter类首先,我们需要定义一个名为EventEmitter的类,作为发布订阅......