Java多线程之volatile详解,夯实你的开发基础

private static volatile int num=0;

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

Thread A=new Thread(new Runnable() {

@Override

public void run() {

while (true){

if (num==0){ //读取num过程记作1

System.out.println(“A”);

num=1; //写入num记位2

}

}

}

},“A”);

Thread B=new Thread(new Runnable() {

@Override

public void run() {

while (true){

if (num==1){ //读取num过程记作3

System.out.println(“B”);

num=0; 写入num记位4

}

}

}

},“B”);

A.start();

B.start();

}

}

Lock可以通过阻止同时访问来完成对共享变量的同时访问和修改,必要的时候阻塞其他尝试获取锁的线程,那么volatile关键字又是如何工作,在这个例子中,是否效果会优于Lock呢。

Java 内存模型中的可见性、原子性和有序性

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

  • **可见性:**值线程之间的可见性,一个线程对于状态的修改对另一个线程是可见的,也就是说一个线程修改的结果对于其他线程是实时可见的。

可见性是一个复杂的属性,因为可见性中的错误总是会违背我们的直觉(JMM决定),通常情况下,我们无法保证执行读操作的线程能实时的看到其他线程的写入的值。为了保证线程的可见性必须使用同步机制。退一步说,最少应该保证当一个线程修改某个状态时,而这个修改时程序员希望能被其他线程实时可见的,那么应该保证这个状态实时可见,而不需要保证所有状态的可见。在 Java 中 volatile、synchronized 和 final 实现可见性。

  • **原子性:**如果一个操作是不可以再被分割的,那么我们说这个操作是一个原子操作,即具有原子性。但是例如i++实际上是i=i+1这个操作是可分割的,他不是一个原子操作。

非原子操作在多线程的情况下会存在线程安全性问题,需要是我们使用同步技术将其变为一个原子操作。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性

  • **有序性:**一系列操作是按照规定的顺序发生的。如果在本线程之内观察,所有的操作都是有序的,如果在其他线程观察,所有的操作都是无序的;前半句指“线程内表现为串行语义”后半句指“指令重排序”和“工作内存和主存同步延迟”

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性。volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

Volatile原理

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

volatile定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该通过获取排他锁单独获取这个变量;

java提供了volatile关键字在某些情况下比锁更好用。

  • Java语言提供了volatile了关键字来提供一种稍弱的同步机制,他能保证操作的可见性和有序性。当把变量声明为volatile类型后,

编译器与运行时都会注意到这个变量是一个共享变量,并且这个变量的操作禁止与其他的变量的操作重排序。

  • 访问volatile变量时不会执行加锁操作。因此也不会存在阻塞竞争的线程,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

volatile的特性

===========

volatile具有以下特性:

  • 可见性:对于一个volatile的读总能看到最后一次对于这个volatile变量的写

  • 原子性:对任意单个volatile变量的读/写具有原子性,但对于类似于i++这种复合操作不具有原子性。

  • 有序性:

volatile happens-before规则

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

根据JMM要求,共享变量存储在共享内存当中,工作内存存储一个共享变量的副本,

线程对于共享变量的修改其实是对于工作内存中变量的修改,如下图所示:

Java多线程之volatile详解,夯实你的开发基础

从多线程交替打印A和B开始章节中使用volatile关键字的实现为例来研究volatile关键字实现了什么:

假设线程A在执行num=1之后B线程读取num指,则存在以下happens-before关系

  1. 1 happens-before 2,3 happens-before 4

  2. 根据volatile规则有:2 happens-before 3

  3. 根据heppens-before传递规则有: 1 happens-before 4

至此线程的执行顺序是符合我们的期望的,那么volatile是如何保证一个线程对于共享变量的修改对于其他线程可见的呢?

volatile 内存语义

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

根据JMM要求,对于一个变量的读写存在8个原子操作。对于一个共享变量的读写过程如下图所示:

Java多线程之volatile详解,夯实你的开发基础

对于一个没有进行同步的共享变量,对其的使用过程分为read、load、use、assign以及不确定的store、write过程。

整个过程的语言描述如下:

  • 第一步:从共享内存中读取变量放入工作内存中(readload)

  • 第二步:当执行引擎需要使用这个共享变量时从本地内存中加载至CPU中(use)

  • 第三步:值被更改后使用(assign)写回工作内存。

  • 第四步:若之后执行引擎还需要这个值,那么就会直接从工作内存中读取这个值,不会再去共享内存读取,除非工作内存中的值出于某些原因丢失。- 第五步:在不确定的某个时间使用storewrite将工作内存中的值回写至共享内存。

由于没有使用锁操作,两个线程可能同时读取或者向共享内存中写入同一个变量。或者在一个线程使用这个变量的过程中另一个线程读取或者写入变量。

上图中1和6两个操作可能会同时执行,或者在线程1使用num过程中6过程执行,那么就会有很严重的线程安全问题,

一个线程可能会读取到一个并不是我们期望的值。

**那么如果希望一个线程的修改对后续线程的读立刻可见,那么只需要将修改后存储在本地内存中的值回写到共享内存

并且在另一个线程读的时候从共享内存重新读取而不是从本地内存中直接读取即可;事实上

当写一个volatile变量时,JMM会把该线程对应的本地内存中共享变量值刷新会共享内存;

而当读取一个volatile变量时,JMM会从主存中读取共享变量**,这也就是volatile的写-读内存语义。

volatile的写-读内存语义:

volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中共享变量值刷新会共享内存volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

如果将这两个步骤综合起来,那么线程3读取一个volatile变量后,写线程1在写这个volatile变量之前所有可见的共享变量的值都将乐客变得对线程3可见。

volatile变量的读写过程如下图:

Java多线程之volatile详解,夯实你的开发基础

需要注意的是:在各个线程的工作内存中是存在volatile变量的值不一致的情况的,只是每次使用都会从共享内存读取并刷新,执行引擎看不到不一致的情况,

所以认为volatile变量在本地内存中不存在不一致问题。

volatile 内存语义的实现

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

在前文Java内存模型中有提到重排序。为了实现volatile的内存语义,JMM会限制重排序的行为,具体限制如下表:

是否可以重排序第二个操作第二个操作第二个操作第一个操作普通读/写volatile读volatile写普通读/写NOvolatile读NONONOvolatile写NONO

说明:

  • 若第一个操作时普通变量的读写,第二个操作时volatile变量的写操作,则编译器不能重排序这两个操作

  • 若第一个操作是volatile变量的读操作,不论第二个变量是什么操作不饿能重排序这两个操作

  • 若第一个操作时volatile变量的写操作,除非第二个操作是普通变量的独写,否则不能重排序这两个操作

为了实现volatile变量的内存语义,编译器生成字节码文件时会在指令序列中插入内存屏障来禁止特定类型的处理器排序。

为了实现volatile变量的内存语义,插入了以下内存屏障,并且在实际执行过程中,只要不改变volatile的内存语义,

编译器可以根据实际情况省略部分不必要的内存屏障

  • 在每个volatile写操作前面插入StoreStore屏障

  • 在每个volatile写操作后面插入StoreLoad屏障

  • 在每个volatile读操作后面插入LoadLoad屏障

  • 在每个volatile读操作后面插入LoadStore屏障

插入内存屏障后volatile写操作过程如下图:

Java多线程之volatile详解,夯实你的开发基础

插入内存屏障后volatile读操作过程如下图:

Java多线程之volatile详解,夯实你的开发基础

至此在共享内存和工作内存中的volatile的写-读的工作过程全部完成

但是现在的CPU中存在一个缓存,CPU读取或者修改数据的时候是从缓存中获取并修改数据,那么如何保证CPU缓存中的数据与共享内存中的一致,并且修改后写回共享内存呢?

CPU对于Volatile的支持

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

缓存行:cpu缓存存储数据的基本单位,cpu不能使数据失效,但是可以使缓存行失效。

对于CPU来说,CPU直接操作的内存时高速缓存,而每一个CPU都有自己L1、L2以及共享的L3级缓存,如下图:

Java多线程之volatile详解,夯实你的开发基础

那么当CPU修改自身缓存中的被volatile修饰的共享变量时,如何保证对其他CPU的可见性。

缓存一致性协议

===========

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值