java 多线程与并发之volatile详解分析

目录
  • CPU、内存、缓存的关系
  • CPU缓存
    • 什么是CPU缓存
    • 为什么要有多级CPU Cache
  • Java内存模型(Java Memory Model,JMM)
  • JMM导致的并发安全问题
  • 可见性
  • 原子性
  • 有序性
  • volatile
    • volatile特性
    • volatile 的实现原理
  • 总结

CPU、内存、缓存的关系

要理解JMM,要先从计算机底层开始,下面是一份大佬的研究报告

计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的!如果我们计算一次a+b所需要的的时间:

  • CPU读取内存获得a,100纳秒
  • CPU读取内存获得b,100纳秒
  • CPU执行一条指令 a+b ,0.6纳秒

也就是说99%的时间花费在CPU读取内存上了,那如何解决速度不均衡问题?

早期计算机中cpu和内存的速度是差不多的,但在现代计算机中cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了

CPU缓存

什么是CPU缓存

在计算机系统中,CPU高速缓存(英语:CPU Cache,在本文中简称缓存)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。

下图是一个典型的存储器层次结构,我们可以看到一共使用了三级缓存:

为什么要有多级CPU Cache

在计算机系统中,寄存器划是L0级缓存,接着依次是L1,L2,L3(接下来是内存,本地磁盘,远程存储)。越往上的缓存存储空间越小,速度越快,成本也更高;越往下的存储空间越大,速度更慢,成本也更低。从上至下,每一层都可以看做是更下一层的缓存,即:L0寄存器是L1一级缓存的缓存,L1是L2的缓存,依次类推;每一层的数据都是来至它的下一层,所以每一层的数据是下一层的数据的子集

下图是我电脑的三级缓存,可以看到层级越小容量越小。速度越快价格越高!!

在现代CPU上,一般来说L0, L1,L2,L3都集成在CPU内部,而L1还分为一级数据缓存(Data Cache,D-Cache,L1d)和一级指令缓存(Instruction Cache,I-Cache,L1i),分别用于存放数据和执行数据的指令解码。每个核心拥有独立的运算处理单元、控制器、寄存器、L1、L2缓存,然后一个CPU的多个核心共享最后一层CPU缓存L3。

为了充分利用 CPU Cache,Java提出了内存模型这个概念

Java内存模型(Java Memory Model,JMM)

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的工作内存(Local Memory),工作内存中存储了该线程以读/写共享变量的副本。

举个栗子:多个线程去修改主内存中的变量a。线程不能直接修改主内存中的数据,先把数据拷贝到工作内存,线程对私有的工作内存修改然后再同步到主内存。那这样做会带来什么问题呢?

JMM导致的并发安全问题

从JMM角度看,如果两个线程同时调用 a=a+1这个函数(假设a的初始值是0),A、B线程同时从主内存中拷贝a=0,然后修改写回,最后主内存为a=1,咋搞?

如下是代码栗子

public class MainTest {

    private  long count = 0;

    public  void incCount() {
        count += 1;
    }

    public static void main(String[] args) throws InterruptedException {

        MainTest test = new MainTest();
        Count count = new Count(test);
        Count count1 = new Count(test);
        count.start();
        count1.start();
        Thread.sleep(5);
        System.out.println("result is :" + test.count);
    }

    private static class Count extends Thread{
        private MainTest m;

        public Count(MainTest m){
            this.m = m;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                m.incCount();
            }
        }
    }
}

执行结果

// 第一次执行
> Task :lib-test:MainTest.main()
result is :11861

// 第二次执行
> Task :lib-test:MainTest.main()
result is :10535

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量a,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 a 的操作对于线程 B 而言就不具备可见性了 。

要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

我们都知道CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。而任务的切换大多数是在时间片段结束以后。

那么线程切换为什么会带来bug呢?因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高级语言里的一条语句。比如count++,在java里就是一句话,但高级语言里一条语句往往需要多条 CPU 指令完成。其实count++包含了三个CPU指令

有序性

即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

在单线程的情况下,CPU执行语句并不是按照顺序来的,为了更高的执行效率可能会重新排序,单线程下是可以提高执行效率且保证正确。但在多线程下反而变成了安全问题,Java提供volatile来保证一定的有序性。此处不做深入!

volatile

volatile特性

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

【面试题】为什么volatile不能保证a++的线程安全问题
:线程执行a++要经历读取主内存-加载-使用-赋值-写内存-写回主内存几个阶段,而且a++不是原子操作,至少可以分为三步执行。线程A、B同时从主内存读取a的值,A线程执行到加载阶段切换上下文交出CPU使用权,B线程完成整个操作并刷新了主内存中a的值。此时A线程继续赋值等其他操作,已经造成了安全问题。可见性是保证线程每次读取时必须读取主内存的值,对后续的操作没有限制,不会因为主内存中的值改变而中断了操作。如果是原子性则可以,synchronized可以保证原子性。

volatile 的实现原理

有volatile修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令

  • 将当前处理器缓存的数据写回到系统内存
  • 这个写回内存的操作会使其他CPU里缓存了该地址的数据无效

单例模式的双重锁为什么要加volatile

public class TestInstance{
	private volatile static TestInstance instance;
	public static TestInstance getInstance(){        //1
		if(instance == null){                        //2
			synchronized(TestInstance.class){        //3
				if(instance == null){                //4
					instance = new TestInstance();   //5
				}
			}
		}
		return instance;                             //6
	}
}

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。
instance = new TestInstance()可以分解为3行伪代码

a. memory = allocate() //分配内存

b. ctorInstanc(memory) //初始化对象

c. instance = memory //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象

总结

因为CPU与内存的速度差距越来越大,为了弥补速度差距引入了CPU缓存,又因为缓存导致线程安全问题,从前到后缕出一条线来就很容易理解了。如果只是单线程完全不担心什么指令重排,想要更高的执行效率必然付出安全风险。知其然,知其所以然!

到此这篇关于java 多线程与并发之volatile详解分析的文章就介绍到这了,更多相关Java volatile内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java关键字volatile知识点总结

    volatile是什么 volatile关键字是Java提供的一种轻量级同步机制.它能够保证可见性和有序性,但是不能保证原子性 可见性 对于volatile的可见性,先看看这段代码的执行 flag默认为true 创建一个线程A去判断flag是否为true,如果为true循环执行i++操作两秒后,创建另一个线程B将flag修改为false 线程A没有感知到flag已经被修改成false了,不能跳出循环 这相当于啥呢?相当于你的女神和你说,你好好努力,年薪百万了就嫁给你,你听了之后,努力赚钱.3年之

  • 并发编程之Java内存模型volatile的内存语义

    1.volatile的特性 理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对单个读/写操作做了同步. 代码示例: package com.lizba.p1; /** * <p> * volatile示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 21:34 */ public class VolatileFeatureExample { /** 使用volatile声明64位的long型

  • 详解Java volatile 内存屏障底层原理语义

    目录 一.volatile关键字介绍及底层原理 1.volatile的特性(内存语义) 2.volatile底层原理 二.volatile--可见性 三.volatile--无法保证原子性 四.volatile--禁止指令重排 1.指令重排 2.as-if-serial语义 五.volatile与内存屏障(Memory Barrier) 1.内存屏障(Memory Barrier) 2.volatile的内存语义实现 六.JMM对volatile的特殊规则定义 一.volatile关键字介绍及底

  • java高并发的volatile与Java内存模型详解

    public class Demo09 { public static boolean flag = true; public static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println("线程" + this.getName() + " in"); while (flag) { ;

  • Java并发编程之Volatile变量详解分析

    目录 一.volatile变量的特性 1.1.保证可见性,不保证原子性 1.2.禁止指令重排 二.内存屏障 三.happens-before Volatile关键字是Java提供的一种轻量级的同步机制.Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量, 相比synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度. 但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其

  • Java中JMM与volatile关键字的学习

    目录 JMM volatile关键字 可见性与原子性测试 哪些地方用到过volatile? 单例模式的安全问题 你知道CAS吗? CAS底层原理 CAS缺点 ABA问题 总结 JMM JMM是指Java内存模型,不是Java内存布局,不是所谓的栈.堆.方法区. 每个Java线程都有自己的工作内存.操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存. JMM可能带来可见性.原子性和有序性问题. 1.可见性:指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改.显然

  • java 多线程与并发之volatile详解分析

    目录 CPU.内存.缓存的关系 CPU缓存 什么是CPU缓存 为什么要有多级CPU Cache Java内存模型(Java Memory Model,JMM) JMM导致的并发安全问题 可见性 原子性 有序性 volatile volatile特性 volatile 的实现原理 总结 CPU.内存.缓存的关系 要理解JMM,要先从计算机底层开始,下面是一份大佬的研究报告 计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的!如果我们计算一次a+b所需要的的时间: CPU读取内存获得a,1

  • Java多线程之搞定最后一公里详解

    目录 绪论 一:线程安全问题 1.1 提出问题 1.2 不安全的原因 1.2.1 原子性 1.2.2 代码"优化" 二:如何解决线程不安全的问题 2.1 通过synchronized关键字 2.2 volatile 三:wait和notify关键字 3.1 wait方法 3.2 notify方法 3.3 wait和sleep对比(面试常考) 四:多线程案例 4.1 饿汉模式单线程 4.2 懒汉模式单线程 4.3 懒汉模式多线程低性能版 4.4懒汉模式-多线程版-二次判断-性能高 总结

  • Java基础面试题之volatile详解

    目录 1.volatile保证可见性 1.1.什么是JMM模型? 1.2.volatile保证可见性的代码验证 1.2.1.无可见性代码验证 1.2.1.volatile保证可见性验证 2.volatile不保证原子性 2.1 什么是原子性? 2.2 不保证原子性的代码验证 2.3 volatile不保证原子性的解决方法 2.3.1 方法1:使用synchronized 2.3.2 方法1:使用JUC包下的AtomicInteger 3.volatile禁止指令重排 3.1 什么是指令重排? 3

  • Java多线程模拟银行系统存钱问题详解

    目录 一.题目描述 二.解题思路 三.代码详解 多学一个知识点 一.题目描述 题目:模拟一个简单的银行系统,使用两个不同的线程向同一个账户存钱. 实现:使用特殊域变量volatile实现同步. 二.解题思路 创建一个类:SynchronizedBankFrame,继承JFrame类 写一个内部类Bank 定义一个account变量,来表示账户. deposit():一个存钱的方法 getAccount():显示账户余额的方法. 写一个内部类Transfer,实现Runnable接口 在run方法

  • Java多线程案例之阻塞队列详解

    目录 一.阻塞队列介绍 1.1阻塞队列特性 1.2阻塞队列的优点 二.生产者消费者模型 2.1阻塞队列对生产者的优化 三.标准库中的阻塞队列 3.1Java提供阻塞队列实现的标准类 3.2Blockingqueue基本使用 四.阻塞队列实现 4.1阻塞队列的代码实现 4.2阻塞队列搭配生产者与消费者的代码实现 一.阻塞队列介绍 1.1阻塞队列特性 阻塞队列特性: 一.安全性 二.产生阻塞效果 阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.阻塞队列能是一种线程安全的数据结构, 并且具有

  • Java多线程中ReentrantLock与Condition详解

    一.ReentrantLock类 1.1什么是reentrantlock java.util.concurrent.lock中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为Java类,而不是作为语言的特性来实现.这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法.性能特性或者锁定语义.ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票.定时锁等候和可中断锁等候的一些特性.此外,它还提供了在激烈争用情况下更

  • java多线程关键字final和static详解

    这篇文章主要介绍了java多线程关键字final和static详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 final关键字 1.final关键字在单线程中的特点: 1)final修饰的静态成员:必须在进行显示初始化或静态代码块赋值,并且仅能赋值一次. 2)final修饰的类成员变量,可以在三个地方进行赋值:显示初始化.构造代码块和构造方法,并且仅能赋值一次. 3)final修饰的局部变量,必须在使用之前进行显示初始化(并不一定要在定义是

  • Java多线程之哲学家就餐问题详解

    一.题目 教材提供一个哲学家就餐问题的解决方案的框架.本问题要求通过pthreads 互斥锁来实现这个解决方案. 哲学家 首先创建 5 个哲学家,每个用数字 0~4 来标识.每个哲学家作为一个单独的 线程运行. 可使用 Pthreads 创建线程.哲学家在思考和吃饭之间交替.为了模拟这两种活动,请让线程休眠 1 到 3 秒钟.当哲学家想要吃饭时,他调用函数: pickup_forks(int philosopher _number) 其中,philosopher _number 为想吃饭哲学家的

  • Java微服务开发之Swagger详解

    目录 一.Swagger的作用和概念 1.Swagger 的优势 2.SwaggerUI 特点 2.SpringBoot集成Swagger 3.配置Swagger 4.实体配置 5.其他皮肤 一.Swagger的作用和概念 ​ 官方地址:https://swagger.io/ ​ Swagger 是一个规范且完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务以及 集成Swagger自动生成API文档. ​ Swagger 的目标是对 REST API 定义一个标准且和

  • Java多线程读写锁ReentrantReadWriteLock类详解

    目录 ReentrantReadWriteLock 读读共享 写写互斥 读写互斥 源码分析 写锁的获取与释放 读锁的获取与释放 参考文献 真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的.所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率. 读写锁表示两个锁,一个是读操作相关的锁

随机推荐