引入高速缓存概念

  1. 在计算机在执行程序时,以指令为单位来执行,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。

  2. 由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行指令的速度很快,而从内存读取数据和向内存写入数据的过程相对很慢,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此就引入了高速缓存

  3. 特性:缓存(Cache memory)是硬盘控制器上的一块内存,是硬盘内部存储和外界接口之间的缓冲器。

高速缓存作用呢?

  1. 预读取

    ​ 相当于提前加载,猜测你可能会用到硬盘相邻存储地址的数据,它会提前进行加载到缓存中,后面你需要时,CPU就不需要去硬盘读取数据,直接读取缓存中的数据传输到内存中就OK了,由于读取缓存的速度远远高于读取硬盘时磁头读写的速度,所以能够明显的改善性能。

  2. 对写入动作进行缓存

    ​ 硬盘接到写入数据的指令之后,并不会马上将数据写入到盘片上,而是先暂时存储在缓存里,然后发送一个“数据已写入”的信号给系统,这时系统就会认为数据已经写入,并继续执行下面的工作,而硬盘则在空闲(不进行读取或写入的时候)时再将缓存中的数据写入到盘片上。

  3. 换到应用程序层面也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据同步到主存当中

举个简单的例子,比如下面的这段代码:

i = i + 1;
  • 当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

  • 这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了(存在临界区)。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存区(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。

比如有两个线程像下列执行顺序:

  1. 线程一执行 i = i + 1,线程二执行var = i
  2. 线程二此时去主存中获取变量 i,线程一只是在高速缓存中更新了变量,还未将变量i写会主存
  3. 线程二读到的i不是最新值,此时多线程导致数据不一致

​ 类似上面这种情况即为缓存一致性问题读写场景、双写场景都会存在缓存一致性问题,但读读不会。前提是需要在多线程运行的环境下,并且需要多线程去访问同一个共享变量。

​ 这里的共享又可以回到上文中,即为上面所说,他们每个线程都有自己的高速缓存区,但是都是从同一个主存同步获取变量。

那么这种问题应该怎样解决呢?

解决缓存不一致问题(硬件层面)

  1. 总线加锁模式

    • 由于CPU在执行命令和其他组件进行通信的时候都需要通过总线,倘若对总线加锁的话,线程一执行i = i + 1 整个命令过程中,其他线程是无法访问主存的。
    • 优缺只有一个,可以解决本问题;缺点的话除了优点全是缺点,效率低,成本高·····(谁也不会让一个主存同时只能干一件事)
  2. 缓存一致性协议
    • 协议可以保证每个缓存中使用的共享变量的副本是一致的,原理:CPU对主存中的共享变量有写入操作时,会立即通知其他CPU将该变量缓存行置为无效状态。其他CPU发现该变为无效状态时,就会重新去主存中读取该变量最新值。
    • 优点就是可以解决问题,读多写少效率还OK;缺点就是实现繁琐,较耗费性能,在对于写多的场景下效率很不可观

问题线程为什么会不安全?

​ 答:共享资源不能及时同步更新,归根于 分时系统 上下文切换时 指令还未执行完毕 (没有写回结果) 更新异常

引入并解释并发编程特性

​ 众所周知现在的互联网大型项目,都是采用分布式架构同时具有其“三高症状”高并发、高可用、高性能。高并发为其中最重要的特性之一,在高并发场景下并发编程就显得尤为重要,其并发编程的特性为原子性、可见性、有序性

原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题。

  • 变量赋值问题:

    • b 变量赋值的底层字节码指令被分为两步:第一步先定义 int b;第二步再赋值为 10。

    • 两条指令之间不具有原子性,且在多线程下会发生线程安全性问题

      int b = 10;

可见性指的是当前线程对共享变量的修改对其他线程来说是可见的。以下案例中假设不会出现多线程原子性问题(比如多个线程写入覆盖问题等),即保证一次变量操作底层执行指令为原子性的。

例如上述变量在读写场景下,不能保证其可见性,导致写线程完成修改指令时但为同步到主存中,读线程并不能获得最新值。这就是对于B线程来说没有满足可见性。

  • 案例解析:final关键字

    • final 变量可以保证其他线程获取的该变量的值是唯一的。变量指成员变量或者静态变量

    • b 变量赋值的底层字节码指令被分为两步:第一步先定义 int b;第二步再赋值为 10

      final a = 10;             int b = 10;
    • final修饰的变量在其指令后自动加入了写屏障,可以保证其变量的可见性

    • a 可以保证其他线程获取的值唯一;b 不能保证其他线程获取到的值一定是 10,有可能为 0。

    • 读取 final 变量解析 :

      • 不加 final 读取变量时去堆内存寻找,final 变量是在栈空间,读取速度快
      • 读取 final 变量时,直接将其在栈中的值复制一份,不用去 getstatic ,性能得到提升
      • 注意:不是所有被 final 修饰的变量都在栈中。当数值超过变量类型的 MAX_VALUE 时,将其值存入常量池中
      • 读取变量的速度:栈 > 常量池 > 堆内存
  • final 可以加强线程安全,而且符合面向对象编程开闭原则中的close,例如子类不可继承、方法不可重写、初始化后不可改变、非法访问(如修饰参数时,该参数为只读模式)等

有序性指的是程序执行的顺序按照代码的先后顺序执行。

在Java中有序性问题会时常出现,由于我们的JVM在底层会对代码指令的执行顺序进行优化(提升执行速度且保证结果),这只能保证单线程下安全,不能保证多线程环境线程安全,会导致指令重排发生有序性问题。

案例:排名世界第一的代码被玩坏了的单例模式

DCL(double checked):加入 volatile 保证线程安全,其实就是保证有序性。

上代码:其中包括了三个问题并且有详细注释解释。(鸣谢itheima满一航老师)

  1. 为什么加入 volatile 关键字?
  2. 对比实现3(给静态代码块加synchronized) 说出这样做的意义?
  3. 为什么要在这里加空判断,之前不是判断过了吗?
final class SingletonLazyVolatile {
private SingletonLazyVolatile() { }
// 问题1:为什么加入 volatile 关键字?
// 答: 防止指令重排序 造成返回对象不完整。 如 TODO
private static volatile SingletonLazyVolatile INSTANCE = null;
// 问题2:对比实现3(给静态代码块加synchronized) 说出这样做的意义?
// 答:没有锁进行判断、效率较高
public static SingletonLazyVolatile getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
// 问题3:为什么要在这里加空判断,之前不是判断过了吗?
// 答:假入t1 先进入判断空成立,先拿到锁, 然后到实例化对象这一步(未执行)
// 同时 线程 t2 获取锁进入阻塞状态,若 t1 完成创建对象后,t2 没有在同步块这进行判空,t2 会再新创建一个对象,
// 导致 t1 的对象被覆盖 造成线程不安全。
synchronized (SingletonLazyVolatile.class) { // t1
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new SingletonLazyVolatile(); // t1 这行代码会发生指令重排序,需要加入 volatile
// 如:先赋值指令INSTANCE = new SingletonLazyVolatile,导致实例不为空,下一个线程会判空失败直接返回该对象
// 但是构造方法()指令还没执行,返回的就是一个不完整的对象。
return INSTANCE;
}
}
}

通过对并发编程的三要素介绍,也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

补充volatile知识:

  • volatile 只保证可见性(多线程下对变量的修改是可见的)、有序性(禁止进行指令重排序)

  • volatile 的底层实现原理是内存屏障(内存栅栏),Memory Barrier(Memory Fence),内存屏障会提供3个功能:

    • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
    • 它会强制将对缓存的修改操作立即写入主存
    • 如果是写操作,它会导致其他CPU中对应的缓存行无效
  • volatile修饰之后的变量会加入读写屏障

    • 写屏障(sfence):保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    • 读屏障(lfence):保证在该屏障之后的, 对共享变量的读取,加载的是主存中的最新数据

    • 对 volatile 变量的写指令后会加入写屏障

    • 对 volatile 变量的读指令前会加入读屏障

关于volatile 的用途像两阶段终止、单例双重锁等等:

两阶段终止--volatile

    @Log
public class TwoPhaseStop { // 监控线程
private Thread monitorThread; // 多线程共享变量 单线程写入(停止线程) 多线程读取 使用 volatile
private volatile boolean stop = false; // 启动监控线程
public void start() {
monitorThread = new Thread(() -> {
log.info("开始监控");
while (true) {
log.info("监控中");
Thread currentThread = Thread.currentThread();
if (stop) {
log.info("正在停止");
break;
}
try {
log.info("正常运行");
Thread.sleep(5000);
} catch (InterruptedException e) {
// sleep出现被打断异常后、被打断后会清除打断标记
// 需要重新打断标记
currentThread.interrupt();
}
}
log.info("已停止");
},"monitor");
monitorThread.start();
} // 停止监控线程
public void stop() {
stop = true;
monitorThread.interrupt();
} }

·

·

·

·

下篇预告:synchronized 和 volatile 区别和底层原理

从缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析案例的更多相关文章

  1. C++11 并发指南三(Lock 详解)(转载)

    multithreading 多线程 C++11 C++11多线程基本使用 C++11 并发指南三(Lock 详解) 在 <C++11 并发指南三(std::mutex 详解)>一文中我们 ...

  2. Asp.Net MVC3 简单入门第一季(三)详解Controller之Filter

    前言 前面两篇写的比较简单,刚开始写这个系列的时候我面向的对象是刚开始接触Asp.Net MVC的朋友,所以写的尽量简单.所以写的没多少技术含量.把这些技术总结出来,然后一简单的方式让更多的人很好的接 ...

  3. Java 并发编程 | 线程池详解

    原文: https://chenmingyu.top/concurrent-threadpool/ 线程池 线程池用来处理异步任务或者并发执行的任务 优点: 重复利用已创建的线程,减少创建和销毁线程造 ...

  4. C++11 并发指南三(Lock 详解)

    在 <C++11 并发指南三(std::mutex 详解)>一文中我们主要介绍了 C++11 标准中的互斥量(Mutex),并简单介绍了一下两种锁类型.本节将详细介绍一下 C++11 标准 ...

  5. 并发编程——IO模型详解

    ​ 我是一个Python技术小白,对于我而言,多任务处理一般就借助于多进程以及多线程的方式,在多任务处理中如果涉及到IO操作,则会接触到同步.异步.阻塞.非阻塞等相关概念,当然也是并发编程的基础. ​ ...

  6. Android并发编程之白话文详解Future,FutureTask和Callable

    从最简单的说起Thread和Runnable 说到并发编程,就一定是多个线程并发执行任务.那么并发编程的基础是什么呢?没错那就是Thread了.一个Thread可以执行一个Runnable类型的对象. ...

  7. 详解 Java 中的三种代理模式

    代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 这里使用 ...

  8. java 并发编程lock使用详解

    浅谈Synchronized: synchronized是Java的一个关键字,也就是Java语言内置的特性,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,执行代码块时,其 ...

  9. Java并发编程的艺术笔记(一)——volatile和syncronized关键字

    一.线程间的通信 volatile和syncronized关键字 volatile 修饰变量,告知任何对该变量的访问必须从共享内存获取,对它的改变必须同步刷新至共享内存,由此保证可见性. syncro ...

随机推荐

  1. Hippo4J v1.3.1 发布,增加 Netty 监控上报、SpringCloud Hystrix 线程池监控等特性

    文章首发在公众号(龙台的技术笔记),之后同步到博客园和个人网站:xiaomage.info Hippo4J v1.3.1 正式发布,本次发布增加了 Netty 上传动态线程池监控数据.适配 Hystr ...

  2. 解决线程安全问题_同步方法和解决线程安全问题_Lock锁

    解决线程安全问题_同步方法 package com.yang.Test.ThreadStudy; import lombok.SneakyThrows; /** * 卖票案例出现了线程安全的问题 * ...

  3. 整数分解和for循环

    整数的分解: 一个整数是由多位数字组成的,那么如何能分解出整数的各个位上的数字呢 对一个整数做%10的操作,就可以得到它的个位数 对一个整数做/10的操作,就去掉了他的个位数 然后再对2的结果做%10 ...

  4. Linux上安装java

    1,输入命令,查看是否已经安装了Openjdk:rpm -qa | grep java 如果有已经安装的java版本或者版本低于1.7,卸载该jdk:rpm -e 软件包名字 如果不能卸载,可以加上 ...

  5. CSS样式快速入门

    CSS样式快速入门 前言 前端基础的博客主要分为HTML.CSS和JavaScript,本类博客主要用于记录博主的学习过程和分享学习经验,由于博主学识浅薄,经验不足,难免会出现错误,欢迎大家提出问题. ...

  6. 说起分布式自增ID只知道UUID?SnowFlake(雪花)算法了解一下(Python3.0实现)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_155 但凡说起分布式系统,我们肯定会对一些海量级的业务进行分拆,比如:用户表,订单表.因为数据量巨大一张表完全无法支撑,就会对其进 ...

  7. ApacheCon 首次亚洲大会 —— Incubator 专场介绍

    Apache 孵化器即为想要进入 Apache 软件基金会(ASF)的项目提供相关帮助和服务.它帮助进入的项目(称为"podling")采用 Apache 的治理风格,并引导使用 ...

  8. docker hung住问题排查

    背景:这个是之前遇到的老问题. # systemctl status lxcfs● lxcfs.service - FUSE filesystem for LXC Loaded: loaded (/u ...

  9. rcu使用遇到问题汇总

    1.3.10内核,在项目中遇到一种情况,我们根据sk指针hash到一个cpu上,然后访问该cpu对应分配的一个数据区. 然后系统会偶尔crash掉,crash掉有两种情况,一种是cred的rcu回收时 ...

  10. 刷题记录:Codeforces Round #725 (Div. 3)

    Codeforces Round #725 (Div. 3) 20210704.网址:https://codeforces.com/contest/1538. 感觉这个比上一个要难. A 有一个n个数 ...