java 并发编程之共享变量的实现方法

可见性

如果一个线程对共享变量值的修改, 能够及时的被其他线程看到, 叫做共享变量的可见性.

Java 虚拟机规范试图定义一种 Java 内存模型 (JMM), 来屏蔽掉各种硬件和操作系统的内存访问差异, 让 Java 程序在各种平台上都能达到一致的内存访问效果.

简单来说, 由于 CPU 执行指令的速度是很快的, 但是内存访问的速度就慢了很多, 相差的不是一个数量级, 所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存.

在 Java 内存模型里, 对上述的优化又进行了一波抽象. JMM 规定所有变量都是存在主存中的, 类似于上面提到的普通内存, 每个线程又包含自己的工作内存, 方便理解就可以看成 CPU 上的寄存器或者高速缓存.

所以线程的操作都是以工作内存为主, 它们只能访问自己的工作内存, 且工作前后都要把值在同步回主内存.

简单点就是, 多线程中读取或修改共享变量时, 首先会读取这个变量到自己的工作内存中成为一个副本, 对这个副本进行改动后, 再更新回主内存中.

使用工作内存和主存, 虽然加快的速度, 但是也带来了一些问题. 比如看下面一个例子:

i = i + 1;

假设 i 初值为 0, 当只有一个线程执行它时, 结果肯定得到 1, 当两个线程执行时, 会得到结果 2 吗? 这倒不一定了. 可能存在这种情况:

线程1: load i from 主存  // i = 0
    i + 1 // i = 1
线程2: load i from主存 // 因为线程1还没将i的值写回主内存,所以i还是0
    i + 1 //i = 1
线程1: save i to 主存
线程2: save i to 主存

如果两个线程按照上面的执行流程, 那么 i 最后的值居然是 1 了. 如果最后的写回生效的慢, 你再读取 i 的值, 都可能是 0, 这就是缓存不一致问题.

这种情况一般称为 失效数据, 因为线程1 还没将 i 的值写回主内存, 所以 i 还是 0, 在线程2 中读到的就是 i 的失效值(旧值).

也可以理解成, 在操作完成之后将工作内存中的副本回写到主内存, 并且在其它线程从主内存将变量同步回自己的工作内存之前, 共享变量的改变对其是不可见的.

有序性

有序性: 即程序执行的顺序按照代码的先后顺序执行. 举个简单的例子, 看下面这段代码:

int i = 0;
boolean flag = false;
i = 1;        //语句1
flag = true;     //语句2

上面代码定义了一个 int 型变量, 定义了一个 boolean 类型变量, 然后分别对两个变量进行赋值操作.

从代码顺序上看, 语句1 是在语句2 前面的, 那么 JVM 在真正执行这段代码的时候会保证语句1 一定会在语句2 前面执行吗? 不一定, 为什么呢? 这里可能会发生指令重排序.

重排序

指令重排是指 JVM 在编译 Java 代码的时候, 或者 CPU 在执行 JVM 字节码的时候, 对现有的指令顺序进行重新排序.

它不保证程序中各个语句的执行先后顺序同代码中的顺序一致, 但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(指的是不改变单线程下的程序执行结果).

虽然处理器会对指令进行重排序, 但是它会保证程序最终结果会和代码顺序执行结果相同, 那么它靠什么保证的呢? 再看下面一个例子:

int a = 10;  //语句1
int r = 2;  //语句2
a = a + 3;  //语句3
r = a*a;   //语句4

这段代码有 4 个语句, 那么可能的一个执行顺序是:

那么可不可能是这个执行顺序呢?

语句2 语句1 语句4 语句3.

不可能, 因为处理器在进行重排序时是会考虑指令之间的数据依赖性, 如果一个指令 Instruction 2 必须用到 Instruction 1 的结果, 那么处理器会保证 Instruction 1 会在 Instruction 2 之前执行.

虽然重排序不会影响单个线程内程序执行的结果, 但是多线程呢? 下面看一个例子:

//线程1:
context = loadContext();  //语句1
inited = true;       //语句2

//线程2:
while(!inited ){
 sleep()
}
doSomethingwithconfig(context);

上面代码中, 由于语句1 和语句2 没有数据依赖性, 因此可能会被重排序.

假如发生了重排序, 在线程1 执行过程中先执行语句2, 而此时线程2 会以为初始化工作已经完成, 那么就会跳出 while 循环, 去执行 doSomethingwithconfig(context) 方法, 而此时 context 并没有被初始化, 就会导致程序出错.

从上面可以看出, 指令重排序不会影响单个线程的执行, 但是会影响到线程并发执行的正确性.

原子性

Java 中, 对基本数据类型的读取和赋值操作是原子性操作, 所谓原子性操作就是指这些操作是不可中断的, 要做一定做完, 要么就没有执行.

JMM 只实现了基本的原子性, 像 i++ 的操作, 必须借助于 synchronizedLock 来保证整块代码的原子性了. 线程在释放锁之前, 必然会把 i 的值刷回到主存的.

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

volatile 关键字

volatile 关键字的两层语义

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

1) 禁止进行指令重排序.

2) 读写一个变量时, 都是直接操作主内存.

在一个变量被 volatile 修饰后, JVM 会为我们做两件事:

1.在每个 volatile 写操作前插入 StoreStore 屏障, 在写操作后插入 StoreLoad 屏障.

2.在每个 volatile 读操作前插入 LoadLoad 屏障, 在读操作后插入 LoadStore 屏障.

或许这样说有些抽象, 我们看一看刚才线程A代码的例子:

boolean contextReady = false;

//在线程A中执行:
context = loadContext();
contextReady = true;

我们给 contextReady 增加 volatile 修饰符, 会带来什么效果呢?

由于加入了 StoreStore 屏障, 屏障上方的普通写入语句 context = loadContext() 和屏障下方的 volatile 写入语句 contextReady = true 无法交换顺序, 从而成功阻止了指令重排序.

也就是说, 当程序执行到 volatile 变量的读或写操作时, 在其前面的操作的更改肯定全部已经进行, 且结果已经对后面的操作可见.

volatile特性之一:
保证变量在线程之间的可见性. 可见性的保证是基于 CPU 的内存屏障指令, 被 JSR-133 抽象为 happens-before 原则.

volatile特性之二:
阻止编译时和运行时的指令重排. 编译时 JVM 编译器遵循内存屏障的约束, 运行时依靠 CPU 屏障指令来阻止重排.

volatile 除了保证可见性和有序性, 还解决了 long 类型和 double 类型数据的 8 字节赋值问题.
虚拟机规范中允许对 64 位数据类型, 分为 2 次 32 位的操作来处理, 当读取一个非 volatile 类型的 long 变量时, 如果对该变量的读操作和写操作不在同一个线程中执行, 那么很有可能会读取到某个值得高 32 位和另一个值得低 32 位.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java多线程编程之ThreadLocal线程范围内的共享变量

    模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程. package com.ljq.test.thread; import java.util.HashMap; import java.util.Map; import java.util.Random; /** * 线程范围内的共享变量 * * 三个模块共享数据,主线程模块和AB模块 * * @author Administrator * */ public class ThreadScopeS

  • Java线程重复执行以及操作共享变量的代码示例

    1.题目:主线程执行10次,子线程执行10次,此过程重复50次 代码: package com.Thread.test; /* * function:主线程执行10次,子线程执行10次, * 此过程重复50次 */ public class ThreadProblem { public ThreadProblem() { final Business bus = new Business(); new Thread(new Runnable() { public void run() { for

  • Java利用happen-before规则如何实现共享变量的同步操作详解

    前言 熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响. Java语言中有一个"先行发生"(happen-before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,"影响"包括修改了内存中共享变量的值.发

  • java通过共享变量结束run停止线程的方法示例

    stop()方法已经被弃用,原因是不太安全.API文档中给出了具体的详细解释.通过interrupted()方法打断线程.不推荐.通过共享变量结束run()方法,进而停止线程.如实例 复制代码 代码如下: public class ThreadInterrupt {    public static void main(String []args){        Runner run = new Runner();        run.start();        try {       

  • java 并发编程之共享变量的实现方法

    可见性 如果一个线程对共享变量值的修改, 能够及时的被其他线程看到, 叫做共享变量的可见性. Java 虚拟机规范试图定义一种 Java 内存模型 (JMM), 来屏蔽掉各种硬件和操作系统的内存访问差异, 让 Java 程序在各种平台上都能达到一致的内存访问效果. 简单来说, 由于 CPU 执行指令的速度是很快的, 但是内存访问的速度就慢了很多, 相差的不是一个数量级, 所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存. 在 Java 内存模型里, 对上述的优化又进行了一波抽象. JM

  • java并发编程_线程池的使用方法(详解)

    一.任务和执行策略之间的隐性耦合 Executor可以将任务的提交和任务的执行策略解耦 只有任务是同类型的且执行时间差别不大,才能发挥最大性能,否则,如将一些耗时长的任务和耗时短的任务放在一个线程池,除非线程池很大,否则会造成死锁等问题 1.线程饥饿死锁 类似于:将两个任务提交给一个单线程池,且两个任务之间相互依赖,一个任务等待另一个任务,则会发生死锁:表现为池不够 定义:某个任务必须等待池中其他任务的运行结果,有可能发生饥饿死锁 2.线程池大小 注意:线程池的大小还受其他的限制,如其他资源池:

  • Java 并发编程学习笔记之Synchronized简介

    一.Synchronized的基本使用 Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法.Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题.从语法上讲,Synchronized总共有三种用法: (1)修饰普通方法 (2)修饰静态方法 (3)修饰代码块 接下来我就通过几个例子程序来说明一下这三种使用方式(为了便于比较,三段代码除了Synchronized的使用方式不同以外,

  • java并发编程实例分析

    java并发编程是java程序设计语言的一块重点,在大部分的业务场景中都需要并发编程. 比如:并发的去处理http请求,这样就可以使得一台机器同时处理多个请求,大大提高业务的响应效率,从而使用用户体验更加流畅. java如何并发编程,要注意以下几个方面: 1.java语言中的多线程操作:创建和启动线程的几种方式. 2.共享变量的同步问题,要保证线程安全,辨别哪些变量是线程安全的.那些变量是线程不安全的,对于不安全的变量我们要想办法让其同步,一般也就是加锁. 3.线程锁:包括方法锁和synchro

  • java并发编程专题(三)----详解线程的同步

    有兴趣的朋友可以回顾一下前两篇 java并发编程专题(一)----线程基础知识 java并发编程专题(二)----如何创建并运行java线程 在现实开发中,我们或多或少的都经历过这样的情景:某一个变量被多个用户并发式的访问并修改,如何保证该变量在并发过程中对每一个用户的正确性呢?今天我们来聊聊线程同步的概念. 一般来说,程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价.如果程序并行化后, 连基本的执行结果的正确性都无法保证, 那么并行程序本身也就没有任何意义了.因此,

  • Java并发编程之线程之间的共享和协作

    一.线程间的共享 1.1 ynchronized内置锁 用处 Java支持多个线程同时访问一个对象或者对象的成员变量 关键字synchronized可以修饰方法或者以同步块的形式来进行使用 它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中 它保证了线程对变量访问的可见性和排他性(原子性.可见性.有序性),又称为内置锁机制. 对象锁和类锁 对象锁是用于对象实例方法,或者一个对象实例上的 类锁是用于类的静态方法或者一个类的class对象上的 类的对象实例可以有很多个,但是每个类只有

  • Java并发编程之关键字volatile知识总结

    一.作用 被 volatile 修饰的变量 1.保证了不同线程对该变量操作的内存可见性 2.禁止指令重排序 二.可见性 Java 内存模型(Java Memory Model) 是 Java 虚拟机定义的一种规范,即每个线程都有自己的工作空间,线程对变量的操作都在线程的工作内存中完成,再同步到主存中,这样可能会导致不同的线程对共享变量的操作,在各自线程工作空间内不一样的问题. 而用 volatile 修饰的变量,线程对该变量的修改,会立刻刷新到主存,其它线程读取该变量时,会重新去主存读取新值.

  • Java 并发编程的可见性、有序性和原子性

    并发编程无论在哪门语言里,都属于高级篇,面试中也尝尝会被问到.想要深入理解并发编程机制确实不是一件容易的事,因为它涉及到计算机底层和操作系统的相关知识,如果对这部分知识不是很清楚可能会导致理解困难. 在这个专栏里,王子会尽量以白话和图片的方式剖析并发编程本质,希望可以让大家更容易理解. 今天我们就来谈一谈可见性.有序性和原子性都是什么东西. 并发编程的幕后 进入主题之前,我们先来了解一下并发编程的幕后. 随着CPU.内存和I/O设备的不断升级,它们之间一直存在着一个矛盾,就是速度不一致问题.CP

  • 详解Java并发编程基础之volatile

    目录 一.volatile的定义和实现原理 1.Java并发模型采用的方式 2.volatile的定义 3.volatile的底层实现原理 二.volatile的内存语义 1.volatile的特性 2.volatile写-读建立的happens-before关系 3.volatile的写/读内存语义 三.volatile内存语义的实现 1.volatile重排序规则 2.内存屏障 3.内存屏障示例 四.volatile与死循环问题 五.volatile对于复合操作非原子性问题 一.volati

  • Java并发编程之内存模型

    目录 一.Java内存模型的基础 1.1 并发编程模型的两个关键问题 1.2 Java内存模型的抽象结构 1.3 从源代码到指令重排序 1.4 写缓冲区和内存屏障 1.4.1 写缓冲区 1.4.2 内存屏障 1.5 happens-before 简介 简介: Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,这一系列几篇文章将揭开Java内存模型的神秘面纱. 这一系列的文章大致分4个部分,分别是: Java内存模型基础,主要介绍内存模型相关基本概念 Java内存模型

随机推荐