深刻分析 java 8 编程语言规范:Threads and Locks

前言

在 java 中,线程由 Thread 类表示,用户建立线程的惟一方式是建立 Thread 类的一个实例,每个线程都和这样的一个实例关联。在相应的 Thread 实例上调用 start() 方法将启动一个线程。html

若是没有正确使用同步,线程表现出来的现象将会是使人疑惑的、违反直觉的。这个章节将描述多线程编程的语义问题,包括一系列的规则,这些规则定义了在多线程环境中线程对共享内存中值的修改是否对其余线程当即可见。java编程语言内存模型定义了统一的内存模型用于屏蔽不一样的硬件架构,在没有歧义的状况下,下面将用内存模型表示这个概念。java

这些语义没有规定多线程的程序在 JVM 的实现上应该怎么执行,而是限定了一系列规则,由 JVM 厂商来知足这些规则,即无论 JVM 的执行策略是什么,表现出来的行为必须是可被接受的。程序员

操做系统有本身的内存模型,C/C++ 这些语言直接使用的就是操做系统的内存模型,而 Java 为了屏蔽各个系统的差别,定义了本身的统一的内存模型。面试

简单说,Java 开发者再也不关心每一个 CPU 核心有本身的内存,而后共享主内存。而是把关注点转移到:每一个线程都有本身的工做内存,全部线程共享主内存。编程

17.1 同步(synchronization)

Java 提供了多种线程之间通讯的机制,其中最基本的就是使用同步 (synchronization),其使用监视器 (monitor) 来实现。java中的每一个对象都关联了一个监视器,线程能够对其进行加锁和解锁操做。在同一时间,只有一个线程能够拿到对象上的监视器锁。若是其余线程在锁被占用期间试图去获取锁,那么将会被阻塞直到成功获取到锁。同时,监视器锁能够重入,也就是说若是线程 t 拿到了锁,那么线程 t 能够在解锁以前重复获取锁;每次解锁操做会反转一次加锁产生的效果。数组

synchronized 有如下两种使用方式:缓存

  1. synchronized 代码块。synchronized(object) 在对某个对象上执行加锁时,会尝试在该对象的监视器上进行加锁操做,只有成功获取锁以后,线程才会继续往下执行。线程获取到了监视器锁后,将继续执行 synchronized 代码块中的代码,若是代码块执行完成,或者抛出了异常,线程将会自动对该对象上的监视器执行解锁操做。安全

  2. synchronized 做用于方法,称为同步方法。同步方法被调用时,会自动执行加锁操做,只有加锁成功,方法体才会获得执行。若是被 synchronized 修饰的方法是实例方法,那么这个实例的监视器会被锁定。若是是 static 方法,线程会锁住相应的 Class 对象的监视器。方法体执行完成或者异常退出后,会自动执行解锁操做。bash

Java语言规范既不要求阻止死锁的发生,也不要求检测到死锁的发生。若是线程要在多个对象上执行加锁操做,那么就应该使用传统的方法来避免死锁的发生,若是有必要的话,须要建立更高层次的不会产生死锁的加锁原语。(原文:Programs where threads hold (directly or indirectly) locks on multiple objects should use conventional techniques for deadlock avoidance, creating higher-level locking primitives that do not deadlock, if necessary.)多线程

java 还提供了其余的一些同步机制,好比对 volatile 变量的读写、使用 java.util.concurrent 包中的同步工具类等。

同步这一节说了 Java 并发编程中最基础的 synchronized 这个关键字,你们必定要理解 synchronize 的锁是什么,它的锁是基于 Java 对象的监视器 monitor,因此任何对象均可以用来作锁。有兴趣的读者能够去了解相关知识,包括偏向锁、轻量级锁、重量级锁等。

小知识点:对 Class 对象加锁、对对象加锁,它们之间不构成同步。synchronized 做用于静态方法时是对 Class 对象加锁,做用于实例方法时是对实例加锁。

面试中常常会问到一个类中的两个 synchronized static 方法之间是否构成同步?构成同步。

17.2 等待集合 和 唤醒(Wait Sets and Notification)

每一个 java 对象,都关联了一个监视器,也关联了一个等待集合。等待集合是一个线程集合。

当对象被建立出来时,它的等待集合是空的,对于向等待集合中添加或者移除线程的操做都是原子的,如下几个操做能够操纵这个等待集合:Object.wait, Object.notify, Object.notifyAll。

等待集合也可能受到线程的中断状态的影响,也受到线程中处理中断的方法的影响。另外,sleep 方法和 join 方法能够感知到线程的 wait 和 notify。

这里归纳得比较简略,没看懂的读者不要紧,继续往下看就是了。

这节要讲Java线程的相关知识,主要包括:

  • Thread 中的 sleep、join、interrupt
  • 继承自 Object 的 wait、notify、notifyAll
  • 还有 Java 的中断,这个概念也很重要

17.2.1 等待 (Wait)

等待操做由如下几个方法引起:wait(),wait(long millisecs),wait(long millisecs, int nanosecs)。在后面两个重载方法中,若是参数为 0,即 wait(0)、wait(0, 0) 和 wait() 是等效的。

若是调用 wait 方法时没有抛出 InterruptedException 异常,则表示正常返回。

前方高能,请读者保持高度精神集中。

咱们在线程 t 中对对象 m 调用 m.wait() 方法,n 表明加锁编号,同时尚未相匹配的解锁操做,则下面的其中之一会发生:

  • 若是 n 等于 0(如线程 t 没有持有对象 m 的锁),那么会抛出 IllegalMonitorStateException 异常。

    注意,若是没有获取到监视器锁,wait 方法是会抛异常的,并且注意这个异常是IllegalMonitorStateException 异常。这是重要知识点,要考。

  • 若是线程 t 调用的是 m.wait(millisecs) 或m.wait(millisecs, nanosecs),形参 millisecs 不能为负数,nanosecs 取值应为 [0, 999999],不然会抛出 IllegalArgumentException 异常。

  • 若是线程 t 被中断,此时中断状态为 true,则 wait 方法将抛出 InterruptedException 异常,并将中断状态设置为 false。

    中断,若是读者不了解这个概念,

  • 不然,下面的操做会顺序发生:

    注意:到这里的时候,wait 参数是正常的,同时 t 没有被中断,而且线程 t 已经拿到了 m 的监视器锁。

    1.线程 t 会加入到对象 m 的等待集合中,执行 加锁编号 n 对应的解锁操做

    这里也很是关键,前面说了,wait 方法的调用必须是线程获取到了对象的监视器锁,而到这里会进行解锁操做。切记切记。。。

    public Object object = new Object();
     void thread1() {
         synchronized (object) { // 获取监视器锁
             try {
                 object.wait(); // 这里会解锁,这里会解锁,这里会解锁
                 // 顺便提一下,只是解了object上的监视器锁,若是这个线程还持有其余对象的监视器锁,这个时候是不会释放的。
             } catch (InterruptedException e) {
                 // do somethings
             }
         }
     }
    复制代码

    2.线程 t 不会执行任何进一步的指令,直到它从 m 的等待集合中移出(也就是等待唤醒)。在发生如下操做的时候,线程 t 会从 m 的等待集合中移出,而后在以后的某个时间点恢复,并继续执行以后的指令。

    并非说线程移出等待队列就立刻往下执行,这个线程还须要从新获取锁才行,这里也很关键,请日后看17.2.4中我写的两个简单的例子。

    • 在 m上执行了 notify 操做,并且线程 t 被选中从等待集合中移除。

    • 在 m 上执行了 notifyAll 操做,那么线程 t 会从等待集合中移除。

    • 线程 t 发生了 interrupt 操做。

    • 若是线程 t 是调用 wait(millisecs) 或者 wait(millisecs, nanosecs) 方法进入等待集合的,那么过了millisecs 毫秒或者 (millisecs*1000000+nanosecs) 纳秒后,线程 t 也会从等待集合中移出。

    • JVM 的“假唤醒”,虽然这是不鼓励的,可是这种操做是被容许的,这样 JVM 能实现将线程从等待集合中移出,而没必要等待具体的移出指令。

      注意,良好的 Java 编码习惯是,只在循环中使用 wait 方法,这个循环等待某些条件来退出循环。

      我的理解wait方法是这么用的:

      synchronized(m) {
           while(!canExit) {
             m.wait(10); // 等待10ms; 固然中断也是经常使用的
             canExit = something();  // 判断是否能够退出循环
           }
       }
       // 2 个知识点:
       // 1. 必须先获取到对象上的监视器锁
       // 2. wait 有可能被假唤醒
      复制代码

      每一个线程在一系列 可能致使它从等待集合中移出的事件 中必须决定一个顺序。这个顺序没必要要和其余顺序一致,可是线程必须表现为它是按照那个顺序发生的。

      例如,线程 t 如今在 m 的等待集合中,无论是线程 t 中断仍是 m 的 notify 方法被调用,这些操做事件确定存在一个顺序。若是线程 t 的中断先发生,那么 t 会由于 InterruptedException 异常而从 wait 方法中返回,同时 m 的等待集合中的其余线程(若是有的话)会收到这个通知。若是 m 的 notify 先发生,那么 t 会正常从 wait 方法返回,且不会改变中断状态。

      咱们考虑这个场景:

      线程 1 和线程 2 此时都 wait 了,线程 3 调用了 :

      synchronized (object) {
          thread1.interrupt(); //1
          object.notify();  //2
      }
      复制代码

      原本我觉得上面的状况 线程1 必定是抛出 InterruptedException,线程2 是正常返回的。

      感谢评论留言的 xupeng.zhang,个人这个想法是错误的,彻底有可能线程1正常返回(即便其中断状态是true),线程2 一直 wait。

3.线程 t 执行编号为 n 的加锁操做

回去看 2 说了什么,线程刚刚从等待集合中移出,而后这里须要从新获取监视器锁才能继续往下执行。

4.若是线程 t 在 2 的时候因为中断而从 m 的等待集合中移出,那么它的中断状态会重置为 false,同时 wait 方法会抛出 InterruptedException 异常。

这一节主要在讲线程进出等待集合的各类状况,同时,最好要知道中断是怎么用的,中断的状态重置发生于何时。

这里的 1,2,3,4 的发生顺序很是关键,你们能够仔细再看看是否是彻底理解了,以后的几个小节还会更具体地阐述这个,参考代码请看 17.2.4 小节我写的简单的例子。

17.2.2 通知(Notification)

通知操做发生于调用 notify 和 notifyAll 方法。

咱们在线程 t 中对对象 m 调用 m.notify() 或 m.notifyAll() 方法,n 表明加锁编号,同时对应的解锁操做没有执行,则下面的其中之一会发生:

  • 若是 n 等于 0,抛出 IllegalMonitorStateException 异常,由于线程 t 尚未获取到对象 m 上的锁。

    这一点很关键,只有获取到了对象上的监视器锁的线程才能够正常调用 notify,前面咱们也说过,调用 wait 方法的时候也要先获取锁

  • 若是 n 大于 0,并且这是一个 notify 操做,若是 m 的等待集合不为空,那么等待集合中的线程 u 被选中从等待集合中移出。

    对于哪一个线程会被选中而被移出,虚拟机没有提供任何保证,从等待集合中将线程 u 移出,可让线程 u 得以恢复。注意,恢复以后的线程 u 若是对 m 进行加锁操做将不会成功,直到线程 t 彻底释放锁以后。

    由于线程 t 这个时候还持有 m 的锁。这个知识点在 17.2.4 节我还会重点说。这里记住,被 notify 的线程在唤醒后是须要从新获取监视器锁的。

  • 若是 n 大于 0,并且这是一个 notifyAll 操做,那么等待集合中的全部线程都将从等待集合中移出,而后恢复。

    注意,这些线程恢复后,只有一个线程能够锁住监视器。

本小节结束,通知操做相对来讲仍是很简单的吧。

17.2.3 中断(Interruptions)

中断发生于 Thread.interrupt 方法的调用。

令线程 t 调用线程 u 上的方法 u.interrupt(),其中 t 和 u 能够是同一个线程,这个操做会将 u 的中断状态设置为 true。

顺便说说中断状态吧,初学者确定觉得 thread.interrupt() 方法是用来暂停线程的,主要是和它对应中文翻译的“中断”有关。中断在并发中是经常使用的手段,请你们必定好好掌握。能够将中断理解为线程的状态,它的特殊之处在于设置了中断状态为 true 后,这几个方法会感知到:

  1. wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)

    这些方法都有一个共同之处,方法签名上都有throws InterruptedException,这个就是用来响应中断状态修改的。

  2. 若是线程阻塞在 InterruptibleChannel 类的 IO 操做中,那么这个 channel 会被关闭。

  3. 若是线程阻塞在一个 Selector 中,那么 select 方法会当即返回。

若是线程阻塞在以上3种状况中,那么当线程感知到中断状态后(此线程的 interrupt() 方法被调用),会将中断状态从新设置为 false,而后执行相应的操做(一般就是跳到 catch 异常处)。

若是不是以上3种状况,那么,线程的 interrupt() 方法被调用,会将线程的中断状态设置为 true。

固然,除了这几个方法,我知道的是 LockSupport 中的 park 方法也能自动感知到线程被中断,固然,它不会重置中断状态为 false。咱们说了,只有上面的几种状况会在感知到中断后先重置中断状态为 false,而后再继续执行。

另外,若是有一个对象 m,并且线程 u 此时在 m 的等待集合中,那么 u 将会从 m 的等待集合中移出。这会让 u 从 wait 操做中恢复过来,u 此时须要获取 m 的监视器锁,获取完锁之后,发现线程 u 处于中断状态,此时会抛出 InterruptedException 异常。

这里的流程:t 设置 u 的中断状态 => u 线程恢复 => u 获取 m 的监视器锁 => 获取锁之后,抛出 InterruptedException 异常。

这个流程在前面 wait 的小节已经讲过了,这也是不少人都不了解的知识点。若是还不懂,能够看下一小节的结束,个人两个简单的例子。

一个小细节:u 被中断,wait 方法返回,并不会当即抛出 InterruptedException 异常,而是在从新获取监视器锁以后才会抛出异常。

实例方法 thread.isInterrupted() 能够知道线程的中断状态。

调用静态方法 Thread.interrupted() 能够返回当前线程的中断状态,同时将中断状态设置为false。

因此说,若是是这个方法调用两次,那么第二次必定会返回 false,由于第一次会重置状态。固然了,前提是两次调用的中间没有发生设置线程中断状态的其余语句。

17.2.4 等待、通知和中断 的交互(Interactions of Waits, Notification, and Interruption)

以上的一系列规范能让咱们肯定 在等待、通知、中断的交互中 有关的几个属性。

若是一个线程在等待期间,同时发生了通知和中断,它将发生:

  • 从 wait 方法中正常返回,同时不改变中断状态(也就是说,调用 Thread.interrupted 方法将会返回 true)

  • 因为抛出了 InterruptedException 异常而从 wait 方法中返回,中断状态设置为 false

线程可能没有重置它的中断状态,同时从 wait 方法中正常返回,即第一种状况。

也就是说,线程是从 notify 被唤醒的,因为发生了中断,因此中断状态为 true

一样的,通知也不能因为中断而丢失。

这个要说的是,线程实际上是从中断唤醒的,那么线程醒过来,同时中断状态会被重置为 false。

假设 m 的等待集合为 线程集合 s,而且在另外一个线程中调用了 m.notify(), 那么将发生:

  • 至少有集合 s 中的一个线程正常从 wait 方法返回,或者
  • 集合 s 中的全部线程由抛出 InterruptedException 异常而返回。

考虑是否有这个场景:x 被设置了中断状态,notify 选中了集合中的线程 x,那么此次 notify 将唤醒线程 x,其余线程(咱们假设还有其余线程在等待)不会有变化。

答案:存在这种场景。由于这种场景是知足上述条件的,并且此时 x 的中断状态是 true。

注意,若是一个线程同时被中断和通知唤醒,同时这个线程经过抛出 InterruptedException 异常从 wait 中返回,那么等待集合中的某个其余线程必定会被通知。

下面咱们经过 3 个例子简单分析下 wait、notify、中断 它们的组合使用。

第一个例子展现了 wait 和 notify 操做过程当中的监视器锁的 持有、释放 的问题。考虑如下操做:

public class WaitNotify {

    public static void main(String[] args) {

        Object object = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {

                synchronized (object) {
                    System.out.println("线程1 获取到监视器锁");
                    try {
                        object.wait();
                        System.out.println("线程1 恢复啦。我为何这么久才恢复,由于notify方法虽然早就发生了,但是我还要获取锁才能继续执行。");
                    } catch (InterruptedException e) {
                        System.out.println("线程1 wait方法抛出了InterruptedException异常");
                    }
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("线程2 拿到了监视器锁。为何呢,由于线程1 在 wait 方法的时候会自动释放锁");
                    System.out.println("线程2 执行 notify 操做");
                    object.notify();
                    System.out.println("线程2 执行完了 notify,先休息3秒再说。");
                    try {
                        Thread.sleep(3000);
                        System.out.println("线程2 休息完啦。注意了,调sleep方法和wait方法不同,不会释放监视器锁");
                    } catch (InterruptedException e) {

                    }
                    System.out.println("线程2 休息够了,结束操做");
                }
            }
        }, "线程2").start();
    }
}

output:
线程1 获取到监视器锁
线程2 拿到了监视器锁。为何呢,由于线程1 在 wait 方法的时候会自动释放锁
线程2 执行 notify 操做
线程2 执行完了 notify,先休息3秒再说。
线程2 休息完啦。注意了,调sleep方法和wait方法不同,不会释放监视器锁
线程2 休息够了,结束操做
线程1 恢复啦。我为何这么久才恢复,由于notify方法虽然早就发生了,但是我还要获取锁才能继续执行。
复制代码

上面的例子展现了,wait 方法返回后,须要从新获取监视器锁,才能够继续往下执行。

同理,咱们稍微修改下以上的程序,看下中断和 wait 之间的交互:

public class WaitNotify {

    public static void main(String[] args) {

        Object object = new Object();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {

                synchronized (object) {
                    System.out.println("线程1 获取到监视器锁");
                    try {
                        object.wait();
                        System.out.println("线程1 恢复啦。我为何这么久才恢复,由于notify方法虽然早就发生了,但是我还要获取锁才能继续执行。");
                    } catch (InterruptedException e) {
                        System.out.println("线程1 wait方法抛出了InterruptedException异常,即便是异常,我也是要获取到监视器锁了才会抛出");
                    }
                }
            }
        }, "线程1");
        thread1.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("线程2 拿到了监视器锁。为何呢,由于线程1 在 wait 方法的时候会自动释放锁");
                    System.out.println("线程2 设置线程1 中断");
                    thread1.interrupt();
                    System.out.println("线程2 执行完了 中断,先休息3秒再说。");
                    try {
                        Thread.sleep(3000);
                        System.out.println("线程2 休息完啦。注意了,调sleep方法和wait方法不同,不会释放监视器锁");
                    } catch (InterruptedException e) {

                    }
                    System.out.println("线程2 休息够了,结束操做");
                }
            }
        }, "线程2").start();
    }
}
output:
线程1 获取到监视器锁
线程2 拿到了监视器锁。为何呢,由于线程1 在 wait 方法的时候会自动释放锁
线程2 设置线程1 中断
线程2 执行完了 中断,先休息3秒再说。
线程2 休息完啦。注意了,调sleep方法和wait方法不同,不会释放监视器锁
线程2 休息够了,结束操做
线程1 wait方法抛出了InterruptedException异常,即便是异常,我也是要获取到监视器锁了才会抛出
复制代码

上面的这个例子也很清楚,若是线程调用 wait 方法,当此线程被中断的时候,wait 方法会返回,而后从新获取监视器锁,而后抛出 InterruptedException 异常。

咱们再来考虑下,以前说的 notify 和中断:

package com.javadoop.learning;

/**
 * Created by hongjie on 2017/7/7.
 */
public class WaitNotify {

    volatile int a = 0;

    public static void main(String[] args) {

        Object object = new Object();

        WaitNotify waitNotify = new WaitNotify();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {

                synchronized (object) {
                    System.out.println("线程1 获取到监视器锁");
                    try {
                        object.wait();
                        System.out.println("线程1 正常恢复啦。");
                    } catch (InterruptedException e) {
                        System.out.println("线程1 wait方法抛出了InterruptedException异常");
                    }
                }
            }
        }, "线程1");
        thread1.start();

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {

                synchronized (object) {
                    System.out.println("线程2 获取到监视器锁");
                    try {
                        object.wait();
                        System.out.println("线程2 正常恢复啦。");
                    } catch (InterruptedException e) {
                        System.out.println("线程2 wait方法抛出了InterruptedException异常");
                    }
                }
            }
        }, "线程2");
        thread2.start();

         // 这里让 thread1 和 thread2 先起来,而后再起后面的 thread3
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("线程3 拿到了监视器锁。");
                    System.out.println("线程3 设置线程1中断");
                    thread1.interrupt(); // 1
                    waitNotify.a = 1; // 这行是为了禁止上下的两行中断和notify代码重排序
                    System.out.println("线程3 调用notify");
                    object.notify(); //2
                    System.out.println("线程3 调用完notify后,休息一会");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("线程3 休息够了,结束同步代码块");
                }
            }
        }, "线程3").start();
    }
}

// 最多见的output:
线程1 获取到监视器锁
线程2 获取到监视器锁
线程3 拿到了监视器锁。
线程3 设置线程1中断
线程3 调用notify
线程3 调用完notify后,休息一会
线程3 休息够了,结束同步代码块
线程2 正常恢复啦。
线程1 wait方法抛出了InterruptedException异常
复制代码

上述输出不是绝对的,再次感谢 xupeng.zhang

有可能发生 线程1 是正常恢复的,虽然发生了中断,它的中断状态也确实是 true,可是它没有抛出 InterruptedException,而是正常返回。此时,thread2 将得不到唤醒,一直 wait。

17.3. 休眠和礼让(Sleep and Yield)

Thread.sleep(millisecs) 使当前正在执行的线程休眠指定的一段时间(暂时中止执行任何指令),时间取决于参数值,精度受制于系统的定时器。休眠期间,线程不会释听任何的监视器锁。线程的恢复取决于定时器和处理器的可用性,即有可用的处理器来唤醒线程。

须要注意的是,Thread.sleep 和 Thread.yield 都不具备同步的语义。在 Thread.sleep 和 Thread.yield 方法调用以前,不要求虚拟机将寄存器中的缓存刷出到共享内存中,同时也不要求虚拟机在这两个方法调用以后,从新从共享内存中读取数据到缓存。

例如,咱们有以下代码块,this.done 定义为一个 non-volatile 的属性,初始值为 false。

while (!this.done)
    Thread.sleep(1000);
复制代码

编译器能够只读取一次 this.done 到缓存中,而后一直使用缓存中的值,也就是说,这个循环可能永远不会结束,即便是有其余线程将 this.done 的值修改成 true。

yield 是告诉操做系统的调度器:个人cpu能够先让给其余线程。注意,调度器能够不理会这个信息。

这个方法太鸡肋,几乎没用。

17.4 内存模型(Memory Model)

内存模型这一节比较长,请耐心阅读

内存模型描述的是程序在 JVM 的执行过程当中对数据的读写是不是按照程序的规则正确执行的。Java 内存模型定义了一系列规则,这些规则定义了对共享内存的写操做对于读操做的可见性。

简单地说,定义内存模型,主要就是为了规范多线程程序中修改或者访问同一个值的时候的行为。对于那些自己就是线程安全的问题,这里不作讨论。

内存模型描述了程序执行时的可能的表现行为。只要执行的结果是知足 java 内存模型的全部规则,那么虚拟机对于具体的实现能够自由发挥。

从侧面说,无论虚拟机的实现是怎么样的,多线程程序的执行结果都应该是可预测的

虚拟机实现者能够自由地执行大量的代码转换,包括重排序操做和删除一些没必要要的同步。

这里我画了一条线,从这条线到下一条线之间是两个重排序的例子,若是你没接触过,能够看一下,若是你已经熟悉了或者在其余地方看过了,请直接往下滑。

示例 17.4-1 不正确的同步可能致使奇怪的结果

java语言容许 compilers 和 CPU 对执行指令进行重排序,致使咱们会常常看到似是而非的现象。

这里没有翻译 compiler 为编译器,由于它不只仅表明编译器,后续它会表明全部会致使指令重排序的机制。

如表 17.4-A 中所示,A 和 B 是共享属性,r1 和 r2 是局部变量。初始时,令 A == B == 0。

表17.4-A. 重排序致使奇怪的结果 - 原始代码

Thread 1 Thread 2
1: r2 = A; 3: r1 = B;
2: B = 1; 4: A = 2;

按照咱们的直觉来讲,r2 == 2 同时 r1 == 1 应该是不可能的。直观地说,指令 1 和 3 应该是最早执行的。若是指令 1 最早执行,那么它应该不会看到指令 4 对 A 的写入操做。若是指令 3 最早执行,那么它应该不会看到执行 2 对 B 的写入操做。

若是真的表现出了 r2==2 和 r1==1,那么咱们应该知道,指令 4 先于指令 1 执行了。

若是在执行过程出表现出这种行为( r2==2 和r1==1),那么咱们能够推断出如下指令依次执行:指令 4 => 指令 1=> 指令 2 => 指令 3。看上去,这种顺序是荒谬的。

可是,Java 是容许 compilers 对指令进行重排序的,只要保证在单线程的状况下,能保证程序是按照咱们想要的结果进行执行,即 compilers 能够对单线程内不产生数据依赖的语句之间进行重排序。若是指令 1 和指令 2 发生了重排序,如按照表17.4-B 所示的顺序进行执行,那么咱们就很容易看到,r2==2 和 r1==1 是可能发生的。

表 17.4-B. 重排序致使奇怪的结果 - 容许的编译器转换

Thread 1 Thread 2
B = 1; r1 = B;
r2 = A; A = 2;

B = 1; => r1 = B; => A = 2; => r2 = A;

对于不少程序员来讲,这个结果看上去是 broken 的,可是这段代码是没有正确的同步致使的:

  • 其中有一个线程执行了写操做
  • 另外一个线程对同一个属性执行了读操做
  • 同时,读操做和写操做没有使用同步来肯定它们之间的执行顺序

简单地说,以后要讲的一大堆东西主要就是为了肯定共享内存读写的执行顺序,不正确或者说非法的代码就是由于读写同一内存地址没有使用同步(这里不只仅只是说synchronized),从而致使执行的结果具备不肯定性。

这个是数据竞争(data race)的一个例子。当代码包含数据竞争时,常常会发生违反咱们直觉的结果。

有几个机制会致使表 17.4-B 中的指令重排序。java 的 JIT 编译器实现可能会重排序代码,或者处理器也会作重排序操做。此外,java 虚拟机实现中的内存层次结构也会使代码像重排序同样。在本章中,咱们将全部这些会致使代码重排序的东西统称为 compiler。

因此,后续咱们不要再简单地将 compiler 翻译为编译器,不要狭隘地理解为 Java 编译器。而是表明了全部可能会制造重排序的机制,包括 JVM 优化、CPU 优化等。

另外一个可能产生奇怪的结果的示例如表 17.4-C,初始时 p == q 同时 p.x == 0。这个代码也是没有正确使用同步的;在这些写入共享内存的写操做中,没有进行强制的前后排序。

Table 17.4-C

Thread 1 Thread 2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r1.x;

一个简单的编译器优化操做是会复用 r2 的结果给 r5,由于它们都是读取 r1.x,并且在单线程语义中,r2 到 r5之间没有其余的相关的写入操做,这种状况如表 17.4-D 所示。

Table 17.4-D

Thread 1 Thread 2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r2;

如今,咱们来考虑一种状况,在线程1第一次读取 r1.x 和 r3.x 之间,线程 2 执行 r6=p; r6.x=3; 编译器进行了 r5复用 r2 结果的优化操做,那么 r2==r5==0,r4 == 3,从程序员的角度来看,p.x 的值由 0 变为 3,而后又变为 0。

我简单整理了一下:

Thread 1 Thread 2 结果
r1 = p;
r2 = r1.x; r2 == 0
r6 = p;
r6.x = 3;
r3 = q;
r4 = r3.x; r4 == 3
r5 = r2; r5 == r2 == 0

例子结束,回到正题

Java 内存模型定义了在程序的每一步,哪些值是内存可见的。对于隔离的每一个线程来讲,其操做是由咱们线程中的语义来决定的,可是线程中读取到的值是由内存模型来控制的。当咱们提到这点时,咱们说程序遵照线程内语义,线程内语义说的是单线程内的语义,它容许咱们基于线程内读操做看到的值彻底预测线程的行为。若是咱们要肯定线程 t 中的操做是不是合法的,咱们只要评估当线程 t 在单线程环境中运行时是不是合法的就能够,该规范的其他部分也在定义这个问题。

这段话不太好理解,首先记住“线程内语义”这个概念,以后还会用到。我对这段话的理解是,在单线程中,咱们是能够经过一行一行看代码来预测执行结果的,只不过,代码中使用到的读取内存的值咱们是不能肯定的,这取决于在内存模型这个大框架下,咱们的程序会读到的值。也许是最新的值,也许是过期的值。

此节描述除了 final 关键字外的java内存模型的规范,final将在以后的17.5节介绍。

这里描述的内存模型并非基于 Java 编程语言的面向对象。为了简洁起见,咱们常常展现没有类或方法定义的代码片断。 大多数示例包含两个或多个线程,其中包含局部变量,共享全局变量或对象的实例字段的语句。 咱们一般使用诸如 r1 或 r2 之类的变量名来表示方法或线程本地的变量。 其余线程没法访问此类变量。

17.4.1. 共享变量(Shared Variables)

全部线程均可以访问到的内存称为共享内存堆内存

全部的实例属性,静态属性,还有数组的元素都存储在堆内存中。在本章中,咱们用术语变量来表示这些元素。

局部变量、方法参数、异常对象,它们不会在线程间共享,也不会受到内存模型定义的任何影响。

两个线程对同一个变量同时进行读-写操做写-写操做,咱们称之为“冲突”。

好,这一节都是废话,愉快地进入到下一节

17.4.2. 操做(Actions)

这一节主要是讲解理论,主要就是严谨地定义操做

线程间操做是指由一个线程执行的动做,能够被另外一个线程检测到或直接影响到。如下是几种可能发生的线程间操做

  • 读 (普通变量,非 volatile)。读一个变量。

  • 写 (普通变量,非 volatile)。写一个变量。

  • 同步操做,以下:

    • volatile 读。读一个 volatile 变量

    • volatile 写。写入一个 volatile 变量

    • 加锁。对一个对象的监视器加锁。

    • 解锁。解除对某个对象的监视器锁。

    • 线程的第一个和最后一个操做。

    • 开启线程操做,或检测一个线程是否已经结束。

  • 外部操做。一个外部操做指的是可能被观察到的在外部执行的操做,同时它的执行结果受外部环境控制。

    简单说,外部操做的外部指的是在 JVM 以外,如 native 操做。

  • 线程分歧操做(§17.4.9)。此操做只由处于无限循环的线程执行,在该循环中不执行任何内存操做、同步操做、或外部操做。若是一个线程执行了分歧操做,那么其后将跟着无数的线程分歧操做。

    分歧操做的引入是为了用来讲明,线程可能会致使其余全部线程停顿而不能继续执行。

此规范仅关心线程间操做,咱们不关心线程内部的操做(好比将两个局部变量的值相加存到第三个局部变量中)。如前文所说,全部的线程都须要遵照线程内语义。对于线程间操做,咱们常常会简单地称为操做

咱们用元祖<t,k,v,u>来描述一个操做:

  • t - 执行操做的线程

  • k - 操做的类型。

  • v - 操做涉及的变量或监视器

    对于加锁操做,v 是被锁住的监视器;对于解锁操做,v 是被解锁的监视器。

    若是是一个读操做( volatile 读或非 volatile 读),v 是读操做对应的变量

    若是是一个写操做( volatile 写或非 volatile 写),v 是写操做对应的变量

  • u - 惟一的标识符标识此操做

外部动做元组还包含一个附加组件,其中包含由执行操做的线程感知的外部操做的结果。 这多是关于操做的成败的信息,以及操做中所读的任何值。

外部操做的参数(如哪些字节写入哪一个 socket)不是外部操做元祖的一部分。这些参数是经过线程中的其余操做进行设置的,并能够经过检查线程内语义进行肯定。它们在内存模型中没有被明确讨论。

在非终结执行中,不是全部的外部操做都是可观察的。17.4.9小节讨论非终结执行和可观察操做。

你们看完这节最懵逼的应该是外部操做线程分歧操做,我简单解释下。

外部操做你们能够理解为 Java 调用了一个 native 的方法,Java 能够获得这个 native 方法的返回值,可是对于具体的执行其实不感知的,意味着 Java 其实不能对这种语句进行重排序,由于 Java 没法知道方法体会执行哪些指令。

引用 stackoverflow 中的一个例子:

// method()方法中jni()是外部操做,不会和 "foo = 42;" 这条语句进行重排序。
class Externalization {
  int foo = 0;
  void method() {
    jni(); // 外部操做
    foo = 42;
  }
  native void jni(); /* {
    assert foo == 0; //咱们假设外部操做执行的是这个。
  } */
}
复制代码

在上面这个例子中,显然,jni()foo = 42 之间不能进行重排序。

再来个线程分歧操做的例子:

// 线程分歧操做阻止了重排序,因此 "foo = 42;" 这条语句不会先执行
class ThreadDivergence {
  int foo = 0;
  void thread1() {
    while (true){} // 线程分歧操做
    foo = 42;
  }

  void thread2() {
    assert foo == 0; // 这里永远不会失败
  }
}
复制代码

17.4.3. 程序和程序顺序(Programs and Program Order)

在每一个线程 t 执行的全部线程间动做中,t 的程序顺序是反映 根据 t 的线程内语义执行这些动做的顺序 的总顺序。

若是全部操做的执行顺序 和 代码中的顺序一致,那么一组操做就是连续一致,而且,对变量 v 的每一个读操做 r 会看到写操做 w 写入的值,也就是:

  • 写操做 w 先于 读操做 r 完成,而且

  • 没有其余的写操做 w' 使得 w' 在 w 以后 r 以前发生。

连续一致性对于可见性和程序执行顺序是一个很是强的保证。在这种场景下,全部的单个操做(好比读和写)构成一个统一的执行顺序,这个执行顺序和代码出现的顺序是一致的,同时每一个单个操做都是原子的,且对全部线程来讲当即可见。

若是程序没有任何的数据竞争,那么程序的全部执行操做将表现为连续一致。

Sequential consistency and/or freedom from data races still allows errors arising from groups of operations that need to be perceived atomically and are not.

连续一致性 和/或 数据竞争的自由仍然容许错误从一组操做中产生。

彻底不知道这句话是什么意思

若是咱们用连续一致性做为咱们的内存模型,那咱们讨论的许多关于编译器优化和处理器优化就是非法的。好比在17.4-C中,一旦执行 p.x=3,那么后续对于该位置的读操做应该是当即能够读到最新值的。

连续一致性的核心在于每一步的操做都是原子的,同时对于全部线程都是可见的,并且不存在重排序。因此,Java 语言定义的内存模型确定不会采用这种策略,由于它直接限制了编译器和 JVM 的各类优化措施。

注意:不少地方所说的顺序一致性就是这里的连续一致性,英文是 Sequential consistency

17.4.4. 同步顺序(Synchronization Order)

每一个执行都有一个同步顺序。同步顺序是由执行过程当中的每一个同步操做组成的顺序。对于每一个线程 t,同步操做组成的同步顺序是和线程 t 中的代码顺序一致的。

虽然拗口,但毕竟说的是同步,咱们都不陌生。

同步操做包括了以下同步关系:

  • 对于监视器 m 的解锁与全部后续操做对于 m 的加锁同步

  • 对 volatile 变量 v 的写入,与全部其余线程后续对 v 的读同步

  • 启动线程的操做与线程中的第一个操做同步。

  • 对于每一个属性写入默认值(0, false,null)与每一个线程对其进行的操做同步。

    尽管在建立对象完成以前对对象属性写入默认值有点奇怪,但从概念上来讲,每一个对象都是在程序启动时用默认值初始化来建立的。

  • 线程 T1 的最后操做与线程 T2 发现线程 T1 已经结束同步。

    线程 T2 能够经过 T1.isAlive() 或 T1.join() 方法来判断 T1 是否已经终结。

  • 若是线程 T1 中断了 T2,那么线程 T1 的中断操做与其余全部线程发现 T2 被中断了同步(经过抛出 InterruptedException 异常,或者调用 Thread.interrupted 或 Thread.isInterrupted )

以上同步顺序能够理解为对于某资源的释放先于其余操做对同一资源的获取。

好,这节相对 easy,说的就是关于 A synchronizes-with B 的一系列规则。

17.4.5. Happens-before顺序(Happens-before Order)

Happens-before 是很是重要的知识,有些地方我没有很理解,我尽可能将原文直译过来。想要了解更深的东西,你可能还须要查询更多的其余资料。

两个操做能够用 happens-before 来肯定它们的执行顺序,若是一个操做 happens-before 于另外一个操做,那么咱们说第一个操做对于第二个操做是可见的。

注意:happens-before 强调的是可见性问题

若是咱们分别有操做 x 和操做 y,咱们写成 hb(x, y) 来表示 x happens-before y

  • 若是操做 x 和操做 y 是同一个线程的两个操做,而且在代码上操做 x 先于操做 y 出现,那么有 hb(x, y)

    请注意,这里不表明不能够重排序,只要没有数据依赖关系,重排序就是可能的。

  • 对象构造方法的最后一行指令 happens-before 于 finalize() 方法的第一行指令。

  • 若是操做 x 与随后的操做 y 构成同步,那么 hb(x, y)。

    这里说的就是上一小节的同步顺序

  • hb(x, y) 和 hb(y, z),那么能够推断出 hb(x, z)

对象的 wait 方法关联了加锁和解锁的操做,它们的 happens-before 关系便是加锁 happens-before 解锁。

咱们应该注意到,两个操做之间的 happens-before 的关系并不必定表示它们在 JVM 的具体实现上必须是这个顺序,若是重排序后的操做结果和合法的执行结果是一致的,那么这种实现就不是非法的。

好比说,在线程中对对象的每一个属性写入初始默认值并不须要先于线程的开始,只要这个事实没有被读到就能够了。

咱们能够发现,happens-before 规则主要仍是上一节 同步顺序 中的规则,加上额外的几条

更具体地说,若是两个操做是 happens-before 的关系,可是在代码中它们并无这种顺序,那么就没有必要表现出 happens-before 关系。如线程 1 对变量进行写入,线程 2 随后对变量进行读操做,那么这两个操做是没有 happens-before 关系的。

happens-before 关系用于定义当发生数据竞争的时候。

将上面全部的规则简化成如下列表:

  • 对一个监视器的解锁操做 happens-before 于后续的对这个监视器的加锁操做。

  • 对 volatile 属性的写操做先于后续对这个属性的读操做。

    也就是一旦写操做完成,那么后续的读操做必定能读到最新的值

  • 线程的 start() 先于任何在线程中定义的语句。

  • 若是 A 线程中调用了 B.join(),那么 B 线程中的操做先于 A 线程 join() 返回以后的任何语句。

    由于 join() 自己就是让其余线程先执行完的意思。

  • 对象的默认初始值 happens-before 于程序中对它的其余操做。

    也就是说无论咱们要对这个对象干什么,这个对象即便没有建立完成,它的各个属性也必定有初始零值。

当程序出现两个没有 happens-before 关系的操做对同一数据进行访问时,咱们称之为程序中有数据竞争。

除了线程间操做,数据竞争不直接影响其余操做的语义,如读取数组的长度、检查转换的执行、虚拟方法的调用。

所以,数据竞争不会致使错误的行为,例如为数组返回错误的长度。

当且仅当全部连续一致的操做都没有数据争用时,程序就是正确同步的。

若是一个程序是正确同步的,那么程序中的全部操做就会表现出连续一致性。

这是一个对于程序员来讲强有力的保证,程序员不须要知道重排序的缘由,就能够肯定他们的代码是否包含数据争用。所以,他们不须要知道重排序的缘由,来肯定他们的代码是不是正确同步的。一旦肯定了代码是正确同步的,程序员也就不须要担忧重排序对于代码的影响。

其实就是正确同步的代码不存在数据竞争问题,这个时候程序员不须要关心重排序是否会影响咱们的代码,咱们的代码执行必定会表现出连续一致。

程序必须正确同步,以免当出现重排序时,会出现一系列的奇怪的行为。正确同步的使用,不能保证程序的所有行为都是正确的。可是,它的使用可让程序员以很简单的方式就能知道可能发生的行为。正确同步的程序表现出来的行为更不会依赖于可能的重排序。没有使用正确同步,很是奇怪、使人疑惑、违反直觉的任何行为都是可能的。

咱们说,对变量 v 的读操做 r 能看到对 v 的写操做 w,若是:

  • 读操做 r 不是先于 w 发生(好比不是 hb(r, w) ),同时
  • 没有写操做 w' 穿插在 w 和 r 中间(如不存在 hb(w, w') 和 hb(w', r))。

非正式地,若是没有 happens-before 关系阻止读操做 r,那么读操做 r 就能看到写操做 w 的结果。

后面的部分是关于

happens-before consistency
的,我也不是很理解,感兴趣的读者请自行参阅其余资料。

A set of actions Ais happens-before consistentif for all reads rin A, whereW(r)is the write action seen by r, it is not the case that eitherhb(r, W(r))or that there exists a write win Asuch that w.v=r.vand hb(W(r), w) andhb(w, r).

In a happens-before consistent set of actions, each read sees a write that it is allowed to see by the happens-before ordering.

Example 17.4.5-1. Happens-before Consistency

For the trace in Table 17.4.5-A, initially A == B == 0. The trace can observe r2 == 0 and r1 == 0 and still be

happens-before consistent, since there are execution orders that allow each read to see the appropriate write.

Table 17.4.5-A. Behavior allowed by happens-before consistency, but not sequential consistency.

Thread 1 Thread 2
B = 1; A = 2;
r2 = A; r1 = B;

Since there is no synchronization, each read can see either the write of the initial value or the write by the other thread. An execution order that displays this behavior is:

1: B = 1;
3: A = 2;
2: r2 = A;  // sees initial write of 0
4: r1 = B;  // sees initial write of 0
复制代码

Another execution order that is happens-before consistent is:

1: r2 = A;  // sees write of A = 2
3: r1 = B;  // sees write of B = 1
2: B = 1;
4: A = 2;
复制代码

In this execution, the reads see writes that occur later in the execution order. This may seem counterintuitive, but is allowed by happens-before consistency. Allowing reads to see later writes can sometimes produce unacceptable behaviors.

关于后面的几个小节,我本身对其理解不够,也不但愿误导你们,若是你们感兴趣的话,请参阅其余资料。

17.4.6. Executions

未完成

17.4.7. Well-Formed Executions

未完成

17.4.8. Executions and Causality Requirements

未完成

17.4.9. Observable Behavior and Nonterminating Executions

未完成

17.5. final 属性的语义(final Field Semantics)

咱们常用 final,关于它最基础的知识是:用 final 修饰的类不能够被继承,用 final 修饰的方法不能够被覆写,用 final 修饰的属性一旦初始化之后不能够被修改。

固然,这节说的不是这些,这里将阐述 final 关键字的深层次含义。

用 final 声明的属性正常状况下初始化一次后,就不会被改变。final 属性的语义与普通属性的语义有一些不同。尤为是,对于 final 属性的读操做,compilers 能够自由地去除没必要要的同步。相应地,compilers 能够将 final 属性的值缓存在寄存器中,而不用像普通属性同样从内存中从新读取。

final 属性同时也容许程序员不须要使用同步就能够实现线程安全不可变对象。一个线程安全的不可变对象对于全部线程来讲都是不可变的,即便传递这个对象的引用存在数据竞争。这能够提供安全的保证,即便是错误的或者恶意的对于这个不可变对象的使用。若是须要保证对象不可变,须要正确地使用 final 属性域。

对象只有在构造方法结束了才被认为彻底初始化了。若是一个对象彻底初始化之后,一个线程持有该对象的引用,那么这个线程必定能够看到正确初始化的 final 属性的值。

这个隐含了,若是属性值不是 final 的,那就不能保证必定能够看到正确初始化的值,可能看到初始零值。

final 属性的使用是很是简单的:在对象的构造方法中设置 final 属性;同时在对象初始化完成前,不要将此对象的引用写入到其余线程能够访问到的地方。若是这个条件知足,当其余线程看到这个对象的时候,那个线程始终能够看到正确初始化后的对象的 final 属性。It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

这里面说到了一个正确初始化的问题,看过《Java并发编程实战》的可能对这个会有印象,不要在构造方法中将 this 发布出去。

Example 17.5-1. final Fields In The Java Memory Model

这段代码把final属性和普通属性进行对比。

class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample f;

    public FinalFieldExample() {
        x = 3;
        y = 4;
    }

    static void writer() {
        f = new FinalFieldExample();
    }

    static void reader() {
        if (f != null) {
            int i = f.x;  // 程序必定能获得 3
            int j = f.y;  // 也许会看到 0
        }
    }
}
复制代码

这个类FinalFieldExample有一个 final 属性 x 和一个普通属性 y。咱们假定有一个线程执行 writer() 方法,另外一个线程再执行 reader() 方法。

由于 writer() 方法在对象彻底构造后将引用写入 f,那么 reader() 方法将必定能够看到初始化后的 f.x : 将读到一个 int 值 3。然而, f.y 不是 final 的,因此程序不能保证能够看到 4,可能会获得 0。

Example 17.5-2. final Fields For Security

final 属性被设计成用来保障不少操做的安全性。

考虑如下代码,线程 1 执行:

Global.s = "/tmp/usr".substring(4);
复制代码

同时,线程 2 执行:

String myS = Global.s;
if (myS.equals("/tmp")) System.out.println(myS);
复制代码

String 对象是不可变对象,同时 String 操做不须要使用同步。虽然 String 的实现没有任何的数据竞争,可是其余使用到 String 对象的代码多是存在数据竞争的,内存模型没有对存在数据竞争的代码提供安全性保证。特别是,若是 String 类中的属性不是 final 的,那么有可能(虽然不太可能)线程 2 会看到这个 string 对象的 offset 为初始值 0,那么就会出现 myS.equals("/tmp")。以后的一个操做可能会看到这个 String 对象的正确的 offset 值 4,那么会获得 “/usr”。Java 中的许多安全特性都依赖于 String 对象的不可变性,即便是恶意代码在数据竞争的环境中在线程之间传递 String 对象的引用。

你们看这段的时候,若是要看代码,请注意,这里说的是 JDK6 及之前的 String 类:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;

    /** Cache the hash code for the string */
    private int hash; // Default to 0
复制代码

由于到 JDK7 和 JDK8 的时候,代码已经变为:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
复制代码

17.5.1. final属性的语义(Semantics of final Fields)

令 o 为一个对象,c 为 o 的构造方法,构造方法中对 final 的属性 f 进行写入值。当构造方法 c 退出的时候,会在final 属性 f 上执行一个 freeze 操做。

注意,若是一个构造方法调用了另外一个构造方法,在被调用的构造方法中设置 final 属性,那么对于 final 属性的 freeze 操做发生于被调用的构造方法结束的时候。

我没懂这边的 freeze 操做是什么。

对于每个执行,读操做的行为被其余的两个偏序影响,解引用链

dereferences()
和内存链
mc()
,它们被认为是执行的一部分。这些偏序必须知足下面的约束:

我对于解引用链和内存链彻底不熟悉,因此下面这段我就不翻译了。

  • Dereference Chain: If an action a is a read or write of a field or element of an object o by a thread t that did not initialize o , then there must exist some read r by thread t that sees the address of o such that r dereferences(r, a).

  • Memory Chain: There are several constraints on the memory chain ordering:

    • If r is a read that sees a write w , then it must be the case that mc(w, r).
    • If r and a are actions such that dereferences(r, a) , then it must be the case that  mc(r, a) .
    • If w is a write of the address of an object o by a thread t that did not initialize o , then there must exist some read r by thread t that sees the address of o such that mc(r, w) .

Given a write w , a freeze f , an action a (that is not a read of a final field), a read r1 of the final field frozen by f , and a read r2 such that hb(w, f) , hb(f, a) , mc(a, r1) , and dereferences(r1, r2) , then when determining which values can be seen by r2 , we consider hb(w, r2) . (This happens-before ordering does not transitively close with other happens-before orderings.)

Note that the dereferences order is reflexive, and r1 can be the same as r2.

For reads of final fields, the only writes that are deemed to come before the read of the final field are the ones derived through the final field semantics.

17.5.2. 在构造期间读 final 属性(Reading final Fields During Construction)

在构造对象的线程中,对该对象的 final 属性的读操做,遵照正常的 happens-before 规则。若是在构造方法内,读某个 final 属性晚于对这个属性的写操做,那么这个读操做能够看到这个 final 属性已经被定义的值,不然就会看到默认值。

17.5.3. final 属性的修改(Subsequent Modification of final Fields)

在许多场景下,如反序列化,系统须要在对象构造以后改变 final 属性的值。final 属性能够经过反射和其余方法来改变。惟一的具备合理语义的模式是:对象被构造出来,而后对象中的 final 属性被更新。在这个对象的全部 final 属性更新操做完成以前,此对象不该该对其余线程可见,也不该该对 final 属性进行读操做。对于 final 属性的 freeze 操做发生于构造方法的结束,这个时候 final 属性已经被设值,还有经过反射或其余方式对于 final 属性的更新以后

即便是这样,依然存在几个难点。若是一个 final 属性在属性声明的时候初始化为一个常量表达式,对于这个 final 属性值的变化过程也许是不可见的,由于对于这个 final 属性的使用是在编译时用常量表达式来替换的。

另外一个问题是,该规范容许 JVM 实现对 final 属性进行强制优化。在一个线程内,容许对于 final 属性的读操做构造方法以外的对于这个 final 属性的修改进行重排序。

Example 17.5.3-1. 对于 final 属性的强制优化(Aggressive Optimization of final Fields

class A {
    final int x;
    A() {
        x = 1;
    }

    int f() {
        return d(this,this);
    }

    int d(A a1, A a2) {
        int i = a1.x;
        g(a1);
        int j = a2.x;
        return j - i;
    }

    static void g(A a) {
        // 利用反射将 a.x 的值修改成 2
        // uses reflection to change a.x to 2
    }
}
复制代码

在方法 d 中,编译器容许对 x 的读操做和方法 g 进行重排序,这样的话,new A().f()可能会返回 -1, 0, 或 1。

我在个人 MBP 上试了好多办法,真的无法重现出来,不过并发问题就是这样,咱们不能重现不表明不存在。StackOverflow 上有网友说在 Sparc 上运行,惋惜我没有 Sparc 机器。

下文将说到一个比较少见的 final-field-safe context

JVM 实现能够提供一种方式在 final 属性安全上下文(final-field-safe context)中执行代码块。若是一个对象是在

final 属性安全上下文
中构造出来的,那么在这个
final 属性安全上下文
中对于 final 属性的读操做不会和相应的对于 final 属性的修改进行重排序。

final 属性安全上下文
还提供了额外的保障。若是一个线程已经看到一个不正确发布的一个对象的引用,那么此线程能够看到了 final 属性的默认值,而后,在
final 属性安全上下文
中读取该对象的正确发布的引用,这能够保证看到正确的 final 属性的值。在形式上,在
final 属性安全上下文
中执行的代码被认为是一个独立的线程(仅用于知足 final 属性的语义)。

在实现中,compiler 不该该将对 final 属性的访问移入或移出

final 属性安全上下文
(尽管它能够在这个执行上下文的周边移动,只要这个对象没有在这个上下文中进行构造)。

对于

final 属性安全上下文
的使用,一个恰当的地方是执行器或者线程池。在每一个独立的
final 属性安全上下文
中执行每个 Runnable,执行器能够保证在一个 Runnable 中对对象 o 的不正确的访问不会影响同一执行器内的其余 Runnable 中的 final 带来的安全保障。

17.5.4. 写保护属性(Write-Protected Fields)

一般,若是一个属性是 final 的和 static 的,那么这个属性是不会被改变的。可是, System.in, System.out, 和 System.errstatic final 的,出于遗留的历史缘由,它们必须容许被 System.setIn, System.setOut, 和 System.setErr 这几个方法改变。咱们称这些属性是写保护的,用以区分普通的 final 属性。

public final static InputStream in = null;
    public final static PrintStream out = null;
    public final static PrintStream err = null;
复制代码

编译器须要将这些属性与 final 属性区别对待。例如,普通 final 属性的读操做对于同步是“免疫的”:锁或 volatile 读操做中的内存屏障并不会影响到对于 final 属性的读操做读到的值。由于写保护属性的值是能够被改变的,因此同步事件应该对它们有影响。所以,语义规定这些属性被当作普通属性,不能被用户的代码改变,除非是 System类中的代码。

17.6. 字分裂(Word Tearing)

实现 Java 虚拟机须要考虑的一件事情是,每一个对象属性以及数组元素之间是独立的,更新一个属性或元素不能影响其余属性或元素的读取与更新。尤为是,两个线程在分别更新 byte 数组相邻的元素时,不能互相影响与干扰,且不须要同步来保证连续一致性。

一些处理器不提供写入单个字节的能力。 经过简单地读取整个字,更新相应的字节,而后将整个字写入内存,用这种方式在这种处理器上实现字节数组更新是非法的。 这个问题有时被称为字分裂(word tearing),在这种不能单独更新单个字节的处理器上,将须要寻求其余的方法。

请注意,对于大部分处理器来讲,都没有这个问题

Example 17.6-1. Detection of Word Tearing

如下程序用于测试是否存在字分裂:

public class WordTearing extends Thread {
    static final int LENGTH = 8;
    static final int ITERS = 1000000;
    static byte[] counts = new byte[LENGTH];
    static Thread[] threads = new Thread[LENGTH];

    final int id;

    WordTearing(int i) {
        id = i;
    }

    public void run() {
        byte v = 0;
        for (int i = 0; i < ITERS; i++) {
            byte v2 = counts[id];
            if (v != v2) {
                System.err.println("Word-Tearing found: " +
                        "counts[" + id + "] = " + v2 +
                        ", should be " + v);
                return;
            }
            v++;
            counts[id] = v;
        }
        System.out.println("done");
    }

    public static void main(String[] args) {
        for (int i = 0; i < LENGTH; ++i)
            (threads[i] = new WordTearing(i)).start();
    }
}
复制代码

这代表写入字节时不得覆写相邻的字节。

17.7. double 和 long 的非原子处理 (Non-Atomic Treatment of double and long)

在Java内存模型中,对于 non-volatile 的 long 或 double 值的写入是经过两个单独的写操做完成的:long 和 double 是 64 位的,被分为两个 32 位来进行写入。那么可能就会致使一个线程看到了某个操做的低 32 位的写入和另外一个操做的高 32 位的写入。

写入或者读取 volatile 的 long 和 double 值是原子的。

写入和读取对象引用必定是原子的,无论具体实现是32位仍是64位。

将一个 64 位的 long 或 double 值的写入分为相邻的两个 32 位的写入对于 JVM 的实现来讲是很方便的。为了性能上的考虑,JVM 的实现是能够决定采用原子写入仍是分为两个部分写入的。

若是可能的话,咱们鼓励 JVM 的实现避开将 64 位值的写入分拆成两个操做。咱们也但愿程序员将共享的 64 位值操做设置为 volatile 或者使用正确的同步,这样能够提供更好的兼容性。

目前来看,64 位虚拟机对于 long 和 double 的写入都是原子的,不必加 volatile 来保证原子性。

相关文章
相关标签/搜索