万字长文!一文彻底搞懂Java—volatile关键字

但是上面的方式会有一个问题,由于在锁住总线期间,其他 CPU 无法访问内存,导致效率低下。

在 JDK 1.6 之后,优化了 synchronized 声言 LOCK# 的方式,不再对总线进行锁定,转而采取了对 CPU 缓存行进行锁定,因为本篇文章不是介绍 synchronized 实现细节的文章,所以不再对这种方式进行详细介绍,读者只需要知道在优化之后,synchronized 的性能不再成为并发问题的瓶颈了。

MESI 协议就是缓存一致性协议,即 Modified(被修改)Exclusive(独占的) Shared(共享的) Or Invalid(无效的)。MESI 他的基本思想就是如果发现 CPU 操作的是共享变量,其他 CPU 也会出现这个共享变量的副本,在 CPU 执行代码期间,会发出信号通知其他人 CPU 自己正在修改共享变量,其他 CPU 收到通知后就会把自己的共享变量置为无效状态。

并发编程中的三个主要问题

===============================================================================

可见性问题

在单核 CPU 时代,所有的线程共用一个 CPU,CPU 缓存和内存的一致性问题容易解决,我们还是拿上面的 i = 1 + 1 来举例,CPU 和 内存之间如果用图来表示的话我想会是下面这样。

![(https://upload-images.jianshu.io/upload_images/26303042-a3ff0450c9a71e47.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在多核时代,每个核都能够独立地运行一个线程,每个 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上线时,这些线程使用的是不同的 CPU 缓存。

因为 i 没有经过任何线程安全措施的保护,多个线程会被并发修改 i 的值,所以我们认为 i 不是线程安全的,导致这种结果的出现是由于 aThread 和 bThread 中读取的 i 彼此不可见,所以这是由于 可见性 导致的线程安全问题。

原子性问题

当两个线程开始运行后,每个线程都会把 i 的值读入到 CPU 缓存中,再执行 + 1 操作,然后把 + 1 之后的值写入内存。因为线程间都有各自的虚拟机栈和程序计数器,他们彼此之间没有数据交换,所以当 aThread 执行 + 1 操作后,会把数据写入到内存,同时 bThread 执行 + 1 操作后,也会把数据写入到内存里,因为 CPU 时间片的执行周期是不确定的,所以会出现当 aThread 还没有把数据写入内存时,bThread 就会读取内存中的数据,然后执行任务 + 1操作,再写回内存,从而覆盖 i 的值。

有序性问题

在并发编程中还有带来让人非常头疼的 有序性 问题是,有序性顾名思义就是顺序性,在计算机中指的就是指令的先后执行顺序。一个非常显而易见的例子就是 JVM 中的类加载。

这是一个 JVM 加载类的过程图,也称为类的生命周期,类从加载到 JVM 到卸载一共会经历五个阶段 加载、连接、初始化、使用、卸载。这五个过程的执行顺序是一定的,但是在连接阶段,也会分为三个过程,即 验证、准备、解析 阶段,这三个阶段的执行顺序不是确定的,通常交叉进行,在一个阶段的执行过程中会激活另一个阶段。

在执行程序的过程中,为了提高性能,编译器和处理器通常会对指令进行重排序。重排序主要分为三类

  • 编译器优化的重排序:编译器在不改变单线程语义的情况下,会对执行语句进行重新排序。

  • 指令集重排序:现代操作系统中的处理器都是并行的,如果执行语句之间不存在数据依赖性,处理器可以改变语句的执行顺序

  • 内存重排序:由于处理器会使用读/写缓冲区,出于性能的原因,内存会对读/写进行重排序

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

volatile 的实现原理

=================================================================================

上面聊了这么多,你可能都要忘了这篇文章的故事主角了吧?主角永远存在于我们心中 …

其实上面聊的这些,都是在为 volatile 做铺垫。

在并发编程中,最需要处理的就是线程之间的通信和线程间的同步问题,上面的可见性、原子性、有序性也是这两个问题带来的。

可见性

而 volatile 就是为了解决这些问题而存在的。Java 语言规范对 volatile 下列定义:Java 语言为了确保能够安全的访问共享变量,提供了 volatile 这个关键字,volatile 是一种轻量级同步机制,它并不会对共享变量进行加锁,但在某些情况下要比加锁更加方便,如果一个字段被声明为 volatile,Java 线程内存模型能够确保所有线程访问这个变量的值都是一致的。

一旦共享变量被 volatile 修饰后,就具有了下面两种含义

  1. 保证了这个字段的可见性,也就是说所有线程都能够"看到"这个变量的值,如果某个 CPU 修改了这个变量的值之后,其他 CPU 也能够获得通知。

  2. 能够禁止指令的重排序

下面我们来看一段代码,这也是我们编写并发代码中经常会使用到的

boolean isStop = false;

while(!isStop){

}

isStop = true;

在这段代码中,如果线程一正在执行 while 循环,而线程二把 isStop 改为 true 之后,转而去做其他事情,因为线程一并不知道线程二把 isStop 改为 true ,所以线程一就会一直运行下去。

如果 isStop 用 volatile 修饰之后,那么事情就会变得不一样了。

使用 volatile 修饰了 isStop 之后,在线程二把 isStop 改为 true 之后,会强制将其写入内存,并且会把线程一中 isStop 的值置为无效(这个值实际上是在缓存在 CPU 中的缓存行里),当线程一继续执行代码的时候,会从内存中重新读取 isStop 的值,此时 isStop 的值就是正确的内存地址的值。

volatile 有下面两条实现原则,其实这两条原则我们在上面介绍的时候已经提过了,一种是总线锁的方式,我们后面说总线锁的方式开销比较大,所以后面设计人员做了优化,采用了锁缓存的方式。另外一种是 MESI 协议的方式。

  • 在 IA-32 架构软件开发者的手册中,有一种 Lock 前缀指令,这种指令能够声言 LOCK# 信号,在最近的处理器中,LOCK# 信号用于锁缓存,等到指令执行完毕后,会把缓存的内容写回内存,这种操作一般又被称为缓存锁定

  • 当缓存写回内存后,IA-32 和 IA-64 处理器会使用 MESI 协议控制内部缓存和其他处理器一致。IA-32 和 IA-64 处理器能够嗅探其他处理器访问系统内部缓存,当内存值修改后,处理器会从内存中重新读取内存值进行新的缓存行填充。

由此可见,volatile 能够保证线程的可见性。

那么 volatile 能够保证原子性吗?

原子性

我们还是以 i = i + 1 这个例子来说明一下,i = i + 1 分为三个操作

  • 读取 i 的值

  • 自增 i 的值

  • 把 i 的值写会内存

我们知道,volatile 能够保证修改 i 的值对其他线程可见,所以我们此时假设线程一执行 i 的读取操作,此时发生了线程切换,线程二读取到最新 i 的值是 0 然后线程再次发生切换,线程一把 i 的值改为 1,线程再次切换,因为此时 i 还没有应用到内存,所以线程 i 同样把 i 的值改为 1 后,线程再次发生切换,线程一把 i 的值写入内存后,再次发生切换,线程二再次把 i 的值写会内存,所以此时,虽然内存值改了两次,但是最后的结果却不是 2。

那么 volatile 不能保证原子性,那么该如何保证原子性呢?

在 JDK 5 的 java.util.concurrent.atomic 包下提供了一些原子操作类,例如 AtomicInteger、AtomicLong、AtomicBoolean,这些操作是原子性操作。它们是利用 CAS 来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的 CMPXCHG 指令实现的,而处理器执行 CMPXCHG 指令是一个原子性操作。

详情可以参考笔者的这篇文章 一场 Atomic XXX 的魔幻之旅。

那么 volatile 能不能保证有序性呢?

这里就需要和你聊一聊 volatile 对有序性的影响了

有序性

上面提到过,重排序分为编译器重排序、处理器重排序和内存重排序。我们说的 volatile 会禁用指令重排序,实际上 volatile 禁用的是编译器重排序和处理器重排序。

下面是 volatile 禁用重排序的规则

从这个表中可以看出来,读写操作有四种,即不加任何修饰的普通读写和使用 volatile 修饰的读写。

从这个表中,我们可以得出下面这些结论

  • 只要第二个操作(这个操作就指的是代码执行指令)是 volatile 修饰的写操作,那么无论第一个操作是什么,都不能被重排序。

  • 当第一个操作是 volatile 读时,不管第二个操作是什么,都不能进行重排序。

  • 当第一个操作是 volatile 写之后,第二个操作是 volatile 读/写都不能重排序。

为了实现这种有序性,编译器会在生成字节码中,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

这里我们先来了解一下内存屏障的概念。

内存屏障也叫做栅栏,它是一种底层原语。它使得 CPU 或编译器在对内存进行操作的时候, 要严格按照一定的顺序来执行, 也就是说在 memory barrier 之前的指令和 memory barrier 之后的指令不会由于系统优化等原因而导致乱序。

内存屏障提供了两个功能。首先,它们通过确保从另一个 CPU 来看屏障的两边的所有指令都是正确的程序顺序;其次它们可以实现内存数据可见性,确保内存数据会同步到 CPU 缓存子系统。

不同计算机体系结构下面的内存屏障也不一样,通常需要认真研读硬件手册来确定,所以我们的主要研究对象是基于 x86 的内存屏障,通常情况下,硬件为我们提供了四种类型的内存屏障。

  • LoadLoad 屏障

它的执行顺序是 Load1 ; LoadLoad ;Load2 ,其中的 Load1 和 Load2 都是加载指令。LoadLoad 指令能够确保执行顺序是在 Load1 之后,Load2 之前,LoadLoad 指令是一个比较有效的防止看到旧数据的指令。

  • StoreStore 屏障

它的执行顺序是 Store1 ;StoreStore ;Store2 ,和上面的 LoadLoad 屏障的执行顺序相似,它也能够确保执行顺序是在 Store1 之后,Store2 之前。

  • LoadStore 屏障

它的执行顺序是 Load1 ; StoreLoad ; Store2 ,保证 Load1 的数据被加载在与这数据相关的 Store2 和之后的 store 指令之前。

  • StoreLoad 屏障

它的执行顺序是 Store1 ; StoreLoad ; Load2 ,保证 Store1 的数据被其他 CPU 看到,在数据被 Load2 和之后的 load 指令加载之前。也就是说,它有效的防止所有 barrier 之前的 stores 与所有 barrier 之后的 load 乱序。

JMM 采取了保守策略来实现内存屏障,JMM 使用的内存屏障如下

下面是一个使用内存屏障的示例

class MemoryBarrierTest {

int a, b;

volatile int v, u;

void f() {

int i, j;

i = a;

j = b;

i = v;

j = u;

a = i;

b = j;

v = i;

u = j;

i = u;

j = b;

a = i;

}

}

这段代码虽然比较简单,但是使用了不少变量,看起来有些乱,我们反编译一下来分析一下内存屏障对这段代码的影响。

从反编译的代码我们是看不到内存屏障的,因为内存屏障是一种硬件层面的指令,单凭字节码是肯定无法看到的。虽然无法看到内存屏障的硬件指令,但是 JSR-133 为我们说明了哪些字节码会出现内存屏障。

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

Java核心架构进阶知识点

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的

image

image

image

内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补

a核心架构进阶知识点

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的

[外链图片转存中…(img-FWx7pchT-1710405949507)]

[外链图片转存中…(img-u5mr4rL2-1710405949508)]

[外链图片转存中…(img-WH9bCb8g-1710405949508)]

内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue中,在响应拦截器跳转路由可以通过使用Vue Router实现。要在响应拦截器跳转路由,首先需要在项目中引入Vue Router,并在main.js或者需要使用路由的文件中进行配置。 首先,需要在项目中安装Vue Router,可以使用npm或者yarn进行安装。然后,在main.js文件中引入Vue Router并配置: ```javascript import Vue from 'vue' import VueRouter from 'vue-router' import App from './App.vue' Vue.use(VueRouter) const router = new VueRouter({ routes: [ // 定义路由信息 { path: '/home', component: Home }, { path: '/about', component: About }, // ... ] }) new Vue({ router, render: h => h(App) }).$mount('#app') ``` 然后,在需要使用响应拦截器的地方进行配置: ```javascript import axios from 'axios' import router from './router' axios.interceptors.response.use(response => { // 在响应拦截器中对返回响应进行逻辑处理 if (response.status === 200) { // 根据逻辑判断,如果需要跳转路由,可以使用router.push方法实现跳转 router.push('/home') // 这里是示例,跳转到名为home的路由 } return response }, error => { return Promise.reject(error) }) ``` 在上述代码中,我们使用router.push方法在响应拦截器跳转到指定的路由。通过在Vue Router中定义路由规则,我们指定了需要跳转的目标路由。当满足某个条件时,我们调用router.push方法进行路由跳转。 需要注意的是,我们需要确保在使用router实例之前Vue Router已经完成了初始化和配置。在main.js文件中,我们在创建Vue实例之前初始化并传入了router实例。 这样,在响应拦截器中调用router.push方法就可以实现路由的跳转了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值