JAVA 并发之路 (三)对象的共享 (1)

栏目: Java · 发布时间: 6年前

内容简介:JAVA 并发之路 (三)对象的共享 (1)

再次重复:要编写正确的并发程序,关键问题在于:在访问 共享的可变的 状态时,需要进行正确的管理。

如在(二)中所述,同步可以确保以原子的方式执行操作,比如关键字synchronized可用于实现原子性或者确定临界区。实际上,同步还有另一个重要的方面: 内存可见性 。我们不仅仅是希望防止在某个线程使用对象状态的同时,有其他线程在修改该状态。而且希望确保当一个线程修改了对象状态后,其他线程 能够看到发生的状态变化 。如果没有同步,则无法实现。

可见性

在多线程环境下,当读操作和写操作在不同的线程中执行时,通常无法确保读操作能够适时地看到其他线程写入的值,有时候甚至是根本不可能的事。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

重排序

先看一个现象, 重排序 :在没有同步的情况下,编译器,处理器以及运行时等都可能 对操作的执行顺序进行一些意想不到的调整 。在缺少同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,还允许CPU对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中。允许重 排序 是因为可以让JVM充分利用现代多核处理器的强大性能。

正是因为重排序的原因,在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

如下面的程序中,没有使用同步,有可能读线程永远都看不到ready的值;也有可能读线程看到了写入ready的值,但是没有看到number的值;还有可能得到失效的值等。

public class NoVisibility {
    //主线程和读线程共享这两个变量
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    //主线程
    public static void main(String[] args) {
        //启动读线程
        new ReaderThread().start();
        //写入number值
        number = 42;
        //写入ready值
        ready = true;
    }
}

幸运的是,有一种简单的方法能够避免这些复杂问题: 只要有数据在多个线程之间共享,就使用正确的同步。

非原子的64位操作

Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但是对于 非volatile类型的long和double变量 (8个字节),JVM允许将64位的读操作或写操作分解为两个32位的操作。(这是因为在编写 Java 虚拟机规范时,许多主流处理器架构还不能有效地提供64位数值的原子操作)

所以当读取一个非volatile类型的long或double变量时,如果对该变量的读操作和写操作在不同的线程中执行时,那么很可能会读取到某个值的高32位和另一个值的低32位。

因此即便不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型变量也是不安全的,除非用关键字volatile声明它们或者用锁保护起来。

加锁与可见性

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,在这种情况下可以保证,在锁被释放之前,A看到的变量值在B获得锁后同样可以由B看到。也就是说,当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果。

所以说为什么在访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。否则,如果一个线程在未持有正确的锁的情况下读取某个变量,可能会读到一个失效值。 加锁不仅仅局限于互斥行为,还包括内存可见性为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。

volatile变量

上一节提到volatile类型变量也是一种同步机制,不过稍弱。它主要用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。并且volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,所以在读取volatile类型的变量时, 总是返回最新写入的值

可按如下理解volatile变量:将它的读操作和写操作分别看成get方法和set方法。但是 在访问volatile变量时不会执行加锁的操作,所以不会使执行线程阻塞 ,因此volatile变量时一种比synchronized关键字更轻量级的同步机制。

public class SynchronizedInteger() {
    private int value;
    public synchronized int get() {
        return value;
    }
    public synchronized void set(int value) {
        this.value = value
    }
}

volatile变量 对可见性的影响 比volatile变量本身更为重要。从内存可见性角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量则相当于进入同步代码块。

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。

volatile变量通常用做某个操作完成、发生中断或者状态的标志。使用时要非常小心,比如volatile的语义不足以确保递增操作的原子性,除非能确保只有一个线程对变量执行写操作。(比起volatile,原子变量提供了“读-改-写”的原子操作,常常作为一种“更好的volatile变量”)

所以:加锁机制既能确保可见性又能确保原子性,而volatile变量只能确保可见性。

volatile变量的正确使用方式包括:确保它们自身状态的可见性;确保它们所引用对象的状态的可见性;以及标识一些重要的程序生命周期事件的发生(比如初始化,关闭)。

当且仅当满足以下条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者可以确保只有单个线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变性条件中。
  • 在访问变量时不需要加锁。

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

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

极简算法史:从数学到机器的故事

极简算法史:从数学到机器的故事

[法] 吕克•德•布拉班迪尔 / 任轶 / 人民邮电出版社 / 2019-1 / 39.00元

数学、逻辑学、计算机科学三大领域实属一家,彼此成就,彼此影响。从古希腊哲学到“无所不能”的计算机,数字、计算、推理这些貌似简单的概念在三千年里融汇、碰撞。如何将逻辑赋予数学意义?如何从简单运算走向复杂智慧?这背后充满了人类智慧的闪光:从柏拉图、莱布尼茨、罗素、香农到图灵都试图从数学公式中证明推理的合理性,缔造完美的思维体系。他们是凭天赋制胜,还是鲁莽地大胆一搏?本书描绘了一场人类探索数学、算法与逻......一起来看看 《极简算法史:从数学到机器的故事》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具