转载

Java多线程(二)volatile关键字

1 volatile的定义

Java语言规范第三版中对volatile定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致地更新,线程应该取保通过排它锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

2 volatile 的内存语义

一旦一个 共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:

2.1 保证了不同线程对这个变量进行 读取 时的可见性,即一个线程修改了某个变量的值 , 这新值对其他线程来说是立即可见的 。(volatile 解决了线程间 共享变量 的可见性问题)

2.1.1 使用 volatile 关键字会强制 将修改的值立即写入主存;

2.1.2 使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的量 工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是 CPU 的 L1或者 L2 缓存中对应的缓存行无效)

2.1.3 由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1再次读取变量 stop 的值时 会去主存读取。那么,在线程 2 修改 stop 值时(当然这里包括 2 个操作,修改线程 2 工作内存中的值,然后将修改后的值写入内存),会使得线程 1 的工作内存中缓存变量 stop 的缓存行无效,然后线程 1 读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程 1 读取到的就是最新的正确的值。

2.2 禁止进行指令重排序 ,阻止编译器对代码的优化 。

volatile 关键字禁止指令重排序有两层意思:

2.2.1 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且 结果已经对后面的操作 可见;在其后面的操作肯定还没有进行。

2.2.2 在进行指令优化时,不能把 volatile 变量前面的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

为了实现 volatile 的内存语义,加入 volatile 关键字时,编译器在生成字节码时,会在指令序列中插入内存屏障,会多出一个 lock 前缀指令。内存屏障是一组处理器指令,解决禁止指令重排序和内存可见性的问题。编译器和 CPU 可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。处理器在进行重排序时是会考虑指令之间的数据依赖性。

内存屏障 有 2 个作用:

1)先于这个 内存屏障 的 指令 必须先执行,后于这个 内存屏障的指令 必须后执行 。

2) 使得内存可见性。所以, 如果你的字段是 volatile ,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。

lock 前缀指令在多核处理器下会引发了两件事情:

1).将当前处理器中这个变量所在缓存行的数据会写回到系统内存。

2)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

内存屏障可以被分为以下几种类型:

LoadLoad 屏障:对于这样的语句 Load1; LoadLoad; Load2,在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。

StoreStore 屏障:对于这样的语句 Store1; StoreStore; Store2,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。

LoadStore 屏障:对于这样的语句 Load1; LoadStore; Store2,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。

StoreLoad 屏障:对于这样的语句 Store1; StoreLoad; Load2,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

3 volatile写-读的内存语义

3.1 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的值)消息。

3.2 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所修改的值)消息。

3.3 线程A写一个volatile变量,随后线程B读取这个volatile变量,这个过程实质上是线程A通过主存向线程B发送消息。

Java多线程(二)volatile关键字

4 线程之间的通信机制

4.1 通信方式的种类

线程之间的通信一共有两种方式:共享内存 和 消息传递。

共享内存 :指的是多条线程共享同一片内存,发送者将消息写入内存,接收者从内存中读取消息,从而实现了消息的传递。但这种方式有个弊端,即需要程序员来控制线程的同步,即线程的执行次序。这种方式并没有真正地实现消息传递,只是从结果上来看就像是将消息从一条线程传递到了另一条线程。

消息传递 : 顾名思义,消息传递指的是发送线程直接将消息传递给接收线程。由于执行次序由并发机制完成,因此不需要程序员添加额外的同步机制,但需要声明消息发送和接收的代码。

Java使用共享内存的方式实现多线程之间的消息传递。因此,程序员需要写额外的代码用于线程之间的同步。

5 Java内存模型

Java 内存模型规定所有的变量都是存在 主存当中,每个线程都有自己的 工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。

5.1 Java内存模型通信结构示意图如下

Java 内存模型规定所有的变量都是存在 主存当中,每个线程都有自己的 工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。

Java多线程(二)volatile关键字

从图来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。

1) 线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2) 线程B到主内存中去读取线程A之前已经更新过的共享变量。

6 volatile一定能保证原子性吗?

答案是否的,如果修改实例变量中的数据,比如i++;也就是i=i+1;则这样的操作其实并不是一个原子操作。也就是非线程安全的。表达式i++的操作步骤分解如下:

1)从内存中取出i的值;

2)计算i的值;

3)将i的值写到内存。

假如在第2步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏数据。解决的办法使用syschronized关键字。关于syschronized关键字明天继续更新。

7 volatile 和 和 synchronized 区别 。

1) volatile 是变量修饰符,而 synchronized 则作用于代码块或方法。

2) volatile 不会对变量加锁,不会造成线程的阻塞;synchronized 会对变量加锁,可能会造成线程的阻塞。

3) volatile 仅能实现变量的修改可见性,并不能保证原子性;而synchronized 则 可 以 保 证 变 量 的 修 改 可 见 性 和 原 子 性 。(synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥),而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性),在释放锁之前会将对变量的修改刷新到主存中)。

4) volatile 标记的变量不会被编译器优化,禁止指令重排序;synchronized 标记的变量可以被编译器优化。

以上内容皆整理自网上,以及《Java并发编程的艺术》,《Java多线程核心编程艺术》, 关于今天的更新就到这里啦, 明天继续更新多线程之synchronized关键字,喜欢的小伙伴可以点下关注~有什么问题欢迎随时讨论哈~

原文  https://juejin.im/post/5b5727d66fb9a04f834652d6
正文到此结束
Loading...