并发学习之内存模型

整装待发

注:文章总结自深入理解Java内存模型-系列-程晓明

并发编程的三个特性

  • 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行—— 处理器优化(处理器对代码进行内存和指令并行的乱序处理)
  • 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值—— 缓存一致性
  • 有序性即程序执行的顺序按照代码的先后顺序执行——指令重排(JIT对代码的乱序处理)

JMM产生原因

并发编程的抽象

并发学习之内存模型_第1张图片

JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证

注:每个处理器上的写缓冲区,仅仅对它所在的处理器可见

此处说明了缓存一致性的重要

重排序

分类

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

解决重排序的办法

  • 1 属于编译器重排序——JMM进制特定类型编译器的重排序
  • 2,3属于处理器重排序——JMM会要求Java编译器在生成字节码时插入特定类型的**内存屏障**,禁止特定类型处理器的重排序

重排序

顺序一致性

  • 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而 JMM 不保证单线程内的操作会按程序的顺序执行(比如下图正确同步的多线程程序在临界区内的重排序)

    对于下面代码:

    // 线程A执行writer
    // 线程B执行reader
    class SynchronizedExample {
    int a = 0;
    boolean flag = false;
    
    public synchronized void writer() {
        a = 1;
        flag = true;
    }
    
    public synchronized void reader() {
        if (flag) {
            int i = a;
            ……
        }
    }
    }
    

    并发学习之内存模型_第2张图片

  • 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序

    并发学习之内存模型_第3张图片

    并发学习之内存模型_第4张图片

  • JMM 不保证对 64 位的 long 型和 double 型变量的读 / 写操作具有原子性,而顺序一致性模型保证对所有的内存读 / 写操作都具有原子性

    当 JVM 在32bit处理器上运行时,会把一个 64 位 long/ double 型变量的读 / 写操作拆分为两个 32 位的读 / 写操作来执行

    并发学习之内存模型_第5张图片

volatile

特性

先看一段volatile程序

  • class VolatileFeaturesExample {
        volatile long vl = 0L;  // 使用 volatile 声明 64 位的 long 型变量 
    
        public void set(long l) {
            vl = l;   // 单个 volatile 变量的写 
        }
        public void getAndIncrement () {
            vl++;    // 复合(多个)volatile 变量的读 / 写 
        }
        public long get() {
            return vl;   // 单个 volatile 变量的读 
        }
    }
    

此程序相当于(即对每一个调用该变量的方法都加锁同步):

  • class VolatileFeaturesExample {
        long vl = 0L;               // 64 位的 long 型普通变量 
    
        public synchronized void set(long l) {     // 对单个的普通 变量的写用同一个监视器同步 
            vl = l;
        }
    
        public void getAndIncrement () { // 普通方法调用 
            long temp = get();           // 调用已同步的读方法 
            temp += 1L;                  // 普通写操作 
            set(temp);                   // 调用已同步的写方法 
        }
        public synchronized long get() { 
        // 对单个的普通变量的读用同一个监视器同步 
            return vl;
        }
    }
    
  • 原子性:监视器锁的语义决定了临界区代码的执行具有原子性。这意味着即使是 64 位的 long 型和 double 型变量,只要它是 volatile 变量,对该变量的读写就将具有原子性。但如果是多个volatile或者类似于volatile++这种复合操作,这些操作整体上不具有原子性

  • 可见性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入

内存语义

  • volatile读操作:JMM 会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
  • volatile写操作:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存
  • 如果我们把 volatile 写和 volatile 读这两个步骤综合起来看的话,在读线程 B 读一个 volatile 变量后,写线程A 在写这个 volatile 变量之前所有可见的共享变量的值都将立即变得对读线程 B 可见

线程通信:

  • 线程 A 写一个 volatile 变量,实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(其对共享变量所在修改的)消息。
  • 线程 B 读一个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个 volatile 变量之前对共享变量所做修改的)消息。
  • 线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程实质上是线程 A 通过主内存向线程 B 发送消息

volatile内存语义的实现

对于编译器重排序
是否能重排序 第二个操作
第一个操作 普通读 / 写 volatile 读 volatile 写
普通读 / 写 NO
volatile 读 NO NO NO
volatile 写 NO NO
  • 第二个操作是 volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。
  • 第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。
  • 第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序
对于指令重排序

在生成字节码的时候会插入指令屏障

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           // 第一个 volatile 读 
        int j = v2;           // 第二个 volatile 读 
        a = i + j;            // 普通写 
        v1 = i + 1;          // 第一个 volatile 写 
        v2 = j * 2;          // 第二个 volatile 写 
    }// 其他方法 
}

并发学习之内存模型_第6张图片

和监视器锁的区别

  • volatile 仅仅保证对单个 volatile 变量的读 / 写具有原子性
  • 监视器锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性
  • 在功能上,监视器锁比 volatile 更强大
  • 在可伸缩性和执行性能上,volatile 更有优势
  • 如果读者想在程序中用 volatile 代替监视器锁,请一定谨慎

synchronized

锁是 java 并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息

特性

看一段代码

class MonitorExample {
    int a = 0;

    //线程A调用writer
    public synchronized void writer() {  //1
        a++;                             //2
    }                                    //3

    //线程B调用reader
    public synchronized void reader() {  //4
        int i = a;                       //5
        ……
    }                                    //6
}

假设A先于B执行,则A拿到锁之后执行1、2、3,然后释放锁,B拿到相同的锁,执行4、5、6

线程B获得同一个锁之后,A中的共享变量,即a,相对于B可见

内存语义

  • 当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中
  • 当线程拿到锁时,JMM会把该线程会把对应的本地内存置为无效,然后从主内存中读取内容

线程通信

  • 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。
  • 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  • 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息

synchronized的实现

对于类ReentrantLock来说

并发学习之内存模型_第7张图片

PS: 其中红线代表内部类

ReenTrantLock分为FairSync和NonfairSync

public ReentrantLock() {
        sync = new NonfairSync();
    }

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  • 对于公平锁的获取来说,实现顺序为:

    1. ReentrantLock : lock()

      public void lock() {
              sync.acquire(1);
          }
      
    2. AbstractQueuedSynchronizer : acquire(int arg) (该类为Sync的基类)

      public final void acquire(int arg) {
              if (!tryAcquire(arg) &&
                  acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                  selfInterrupt();
          }
      
    3. ReentrantLock.FairSync : tryAcquire(int acquires)

       @ReservedStackAccess
              protected final boolean tryAcquire(int acquires) {
                  final Thread current = Thread.currentThread();
                  // 获取锁的开始,首先读 volatile 变量 state
                  int c = getState();
                  if (c == 0) {
                      if (!hasQueuedPredecessors() &&
                          compareAndSetState(0, acquires)) {
                          setExclusiveOwnerThread(current);
                          return true;
                      }
                  }
                  else if (current == getExclusiveOwnerThread()) {
                      int nextc = c + acquires;
                      if (nextc < 0)
                          throw new Error("Maximum lock count exceeded");
                      setState(nextc);
                      return true;
                  }
                  return false;
              }
      

    可以看出,加锁的时候要先读出volatile的变量state

  • 对于非公平锁的加锁来说,实现顺序为:

    1. ReentrantLock : lock()

      public void lock() {
              sync.acquire(1);
          }
      
    2. AbstractQueuedSynchronizer : acquire(int arg) (该类为Sync的基类)

      public final void acquire(int arg) {
              if (!tryAcquire(arg) &&
                  acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                  selfInterrupt();
          }
      
    3. ReentrantLock.NofairSync : tryAcquire(int acquires)

      protected final boolean tryAcquire(int acquires) {
                  return nonfairTryAcquire(acquires);
              }
      
    4. ReentrantLock.Sync : nonfairTryAcquire(acquires)

      @ReservedStackAccess
              final boolean nonfairTryAcquire(int acquires) {
                  final Thread current = Thread.currentThread();
                  int c = getState();
                  if (c == 0) {
                      if (compareAndSetState(0, acquires)) {
                          setExclusiveOwnerThread(current);
                          return true;
                      }
                  }
                  else if (current == getExclusiveOwnerThread()) {
                      int nextc = c + acquires;
                      if (nextc < 0) // overflow
                          throw new Error("Maximum lock count exceeded");
                      setState(nextc);
                      return true;
                  }
                  return false;
              }
      
  • 对于公平锁或者非公平锁的释放来说,实现顺序为:

    1. ReentrantLock : unlock()

      public void unlock() {
              sync.release(1);
          }
      
    2. Sync : release(int arg) (继承了它的基类AbstractQueuedSynchronizer : release(int arg))

      public final boolean release(int arg) {
              if (tryRelease(arg)) {
                  Node h = head;
                  if (h != null && h.waitStatus != 0)
                      unparkSuccessor(h);
                  return true;
              }
              return false;
          }
      
    3. Sync : tryRelease(int releases)

      @ReservedStackAccess
              protected final boolean tryRelease(int releases) {
                  int c = getState() - releases;
                  if (Thread.currentThread() != getExclusiveOwnerThread())
                      throw new IllegalMonitorStateException();
                  boolean free = false;
                  if (c == 0) {
                      free = true;
                      setExclusiveOwnerThread(null);
                  }
                   // 释放锁的最后,写 volatile 变量 state
                  setState(c);
                  return free;
              }
      

    可以看出释放锁后会写入state变量

final

特性

final使得其所修饰的值不被改变

如下例子

public class FinalExample {
    int i;                            // 普通变量 
    final int j;                      //final 变量 
    static FinalExample obj;

    public void FinalExample () {     // 构造函数 
        i = 1;                        // 写普通域 
        j = 2;                        // 写 final 域 
    }

    public static void writer () {    // 写线程 A 执行 
        obj = new FinalExample ();
    }

    public static void reader () {       // 读线程 B 执行 
        FinalExample object = obj;       // 读对象引用 
        int a = object.i;                // 读普通域 
        int b = object.j;                // 读 final 域 
    }
}

当A执行writer之后,B执行reader时

  • 可能会发生(final会保证构造函数和final域不会重排序),此为写final的重排序规则

并发学习之内存模型_第8张图片

  • 可能会发生(final会保证引用变量和final域不会重排序),此为读final域的重排序规则

    并发学习之内存模型_第9张图片

内存语义

  • 在构造函数内对一个 final 域的写入,随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

  • 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序

  • 如果final是引用类型,那么在构造函数内对一个final应用的对象的成员域的写入,与随后在构造函数外把这个被构造函数对象的引用赋值给另一个应用变量,这两个操作之间不能重排序

你可能感兴趣的:(Java成神之路)