当前位置: 首页 > article >正文

第12章 volatile关键字的介绍(Java高并发编程详解:多线程与系统设计)

1. 初识volatile关键字

这段程序分别启动了两个线程,一个线程负责对变量进行修改,一个线程负责对变量进行输出,根据本书第一部分的知识讲解,该变量就是共享资源(数据),那么在多线程操作的情况下,很有可能会引起数据不一致等线程安全的问题。

import java.util.concurrent.TimeUnit;

public class VoliateFoo {
    // init_value的最大值
    final static int MAX=5;
    // init_value的初始值
    static int init_value=0;

    public static void main(String[] args) {
        // 启动一个Reader线程,当发现local_value和init_value不同时,则输出init_value被修改的消息
        new Thread(
                ()->{
                    int localValue = init_value;
                    while(localValue < MAX ) {
                        if ( init_value != localValue ) {
                            System.out.printf("The init_value is updated to [%d]\n", init_value);
                            //
                            localValue = init_value;
                        }
                    }
                }
        ,"Reader").start();

        // 启动Updater线程,主要用于对init_value的修改,当local_value>=5的时候则退出生命周期
        new Thread(
                () -> {
                    int localValue = init_value;
                    while(localValue < MAX ) {
                        // 修改 init_value
                        System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
                        init_value = localValue;

                        try {
                            TimeUnit.SECONDS.sleep(2);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                    }
                }
        ,"Updater").start();
    }
}

运行结果:

Connected to the target VM, address: '127.0.0.1:65218', transport: 'socket'
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
Disconnected from the target VM, address: '127.0.0.1:65218', transport: 'socket'

static int init_value=0; 修改为 static volatile int init_value=0; 添加volatile关键字后运行结果如下:

Connected to the target VM, address: '127.0.0.1:65471', transport: 'socket'
The init_value will be changed to [1]
The init_value is updated to [1]
The init_value will be changed to [2]
The init_value is updated to [2]
The init_value will be changed to [3]
The init_value is updated to [3]
The init_value will be changed to [4]
The init_value is updated to [4]
The init_value will be changed to [5]
The init_value is updated to [5]
Disconnected from the target VM, address: '127.0.0.1:65471', transport: 'socket'

注意:volatile关键字只能修饰类变量和实例变量, 对于方法参数、局部变量以及实例常量, 类常量都不能进行修饰, 比如上面代码中的MAX就不能使用volatile关键字进行修饰。

2.机器硬件CPU

2.1 CPU Cache模型

由于两边速度严重的不对等, 通过传统FSB直连内存的访问方式很明显会导致CPU资源受到大量的限制,降低CPU整体的吞吐量,于是就有了在CPU和主内存之间增加缓存的设计,现在缓存的数量都可以增加到3级了,最靠近CPU的缓存称为L1,然后依次是L2,L3和主内存,CPU缓存模型如图所示。

Cache的出现是为了解决CPU直接访问内存效率低下问题的,程序在运行的过程中,会将运算所需要的数据从主存复制一份到CPUCache中,这样CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写入,当运算结

束之后,再将CPU Cache中的最新数据刷新到主内存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU的吞吐能力,有了CPU Cache之后,整体的CPU和主内存之间交互的架构大致如图所示。

2.2 CPU缓存一致性问题

由于缓存的出现, 极大地提高了CPU的吞吐能力, 但是同时也引人了缓存不一致的问题,比如i++这个操作,在程序的运行过程中,首先需要将主内存中的数据复制一份存放到CPU Cache中, 那么CPU寄存器在进行数值计算的时候就直接到Cache中读取和写人,当整个过程运算结束之后再将Cache中的数据刷新到主存当中, 具体过程如下。

1) 读取主内存的i到CPU Cache中。

2)对i进行加一-操作。

3) 将结果写回到CPU Cache中。

4)将数据刷新到主内存中。

为了解决缓存不一致性问题,通常主流的解决方法有如下两种。

  • 通过总线加锁的方式。
  • 通过缓存一致性协议。

第一种方式常见于早期的CPU当中, 而且是一种悲观的实现方式, CPU和其他组件的通信都是通过总线(数据总线、控制总线、地址总线)来进行的,如果采用总线加锁的方式, 则会阻塞其他CPU对其他组件的访问, 从而使得只有一个CPU(抢到总线锁) 能够访问这个变量的内存。这种方式效率低下,所以就有了第二种通过缓存一致性协议的方式来解决不一致的问题(见图)。

在缓存一致性协议中最为出名的是Intel的MESI协议, MESI协议保证了每一个缓存中使用的共享变量副本都是一致的, 它的大致思想是, 当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量, 也就是说在其他的CPU Cache中也存在一个副本, 那么进行如下操作:

1) 读取操作, 不做任何处理, 只是将Cache中的数据读取到寄存器。

2) 写入操作, 发出信号通知其他CPU将该变量的Cacheline置为无效状态, 其他CPU在进行该变量读取的时候不得不到主内存中再次获取。

3. Java内存模型

Java的内存模型(Java Memory Mode, JMM) 指定了Java虚拟机如何与计算机的主存(RAM) 进行工作, 如图所示, 理解Java内存模型对于编写行为正确的并发程序是非常重要的。在JDK 1.5以前的版本中, Java内存模型存在着一定的缺陷, 在JDK 1.5的时候,JDK官方对Java内存模型重新进行了修订, JDK 1.8及最新的JDK版本都沿用了JDK 1.5修订的内存模型。

Java的内存模型决定了一个线程对共享变量的写入何时对其他线程可见, Java内存模型定义了线程和主内存之间的抽象关系,具体如下。

  • 共享变量存储于主内存之中,每个线程都可以访问。
  • 每个线程都有私有的工作内存或者称为本地内存。
  • 工作内存只存储该线程对共享变量的副本。
  • 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存。
  • 工作内存和Java内存模型一样也是一个抽象的概念, 它其实并不存在, 它涵盖了缓存、寄存器、编译器优化以及硬件等。

Java的内存模型是一个抽象的概念, 其与计算机硬件的结构并不完全一样, 比如计算机物理内存不会存在栈内存和堆内存的划分,无论是堆内存还是虚拟机栈内存都会对应到物理的主内存, 当然也有一部分堆栈内存的数据有可能会存人CPU Cache寄存器中。图所示的是Jave内存模型与CPU硬件架构的交互图。


http://www.kler.cn/a/517430.html

相关文章:

  • SpringBoot+Vue使用Echarts
  • 设计新的 Kibana 仪表板布局以支持可折叠部分等
  • RabbitMQ 多种安装模式
  • 【玩转全栈】----Django连接MySQL
  • macOS如何进入 Application Support 目录(cd: string not in pwd: Application)
  • 挖掘机的市场现状和发展前景:全球增长潜力,重塑基础设施建设新篇章
  • Lua语言的图形用户界面
  • Vue3 插槽(Slots)用法总结
  • 一组开源、免费、Metro风格的 WPF UI 控件库
  • DBeaver下载安装及数据库连接(MySQL)
  • 初步理解数据结构
  • 每日一题 419. 棋盘上的战舰
  • GESP2024年6月认证C++六级( 第三部分编程题(2)二叉树)
  • react native i18n插值:跨组件trans
  • 麒麟操作系统基础知识保姆级教程(二十一)进入单用户模式
  • UE5 特效
  • 面试-二维数组
  • Oracle 创建用户和表空间
  • 第15章 监控任务的生命周期(Java高并发编程详解:多线程与系统设计)
  • Servlet 详解
  • EMC常用器件选型(一)
  • 提示词的艺术 ---- AI Prompt 进阶(提示词框架)
  • 三、双链表
  • 算法基础 -- Trie压缩树原理
  • 浏览器hid 和蓝牙bluetooth技术区别
  • WPF 打印功能实现