Day2并发

来源:互联网 发布:最靠谱网络兼职平台 编辑:程序博客网 时间:2024/06/16 10:04

记录

基于保守策略的JMM内存屏障插入策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
    • 保障所有的普通写在volatile写之前刷新到主内存。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
    • 避免volatile写与后面可能有的volatile读/写操作重排序
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
    • LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
  • 在每个volatile读操作的后面插入一个LoadStore屏障。
    • LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序
常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升
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写    }    …       // 其他方法}

执行顺序

锁的内存语义

锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

锁的释放-获取建立的happens-before关系

下面是锁释放-获取的示例代码。

class MonitorExample {    int a = 0;    public synchronized void writer() {     // 1            a++;           // 2    }             // 3    public synchronized void reader() {    // 4            int i = a;         // 5        ……    }             // 6}

假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类。

  • 根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happensbefore 6。
  • 根据监视器锁规则,3 happens-before 4。
  • 根据happens-before的传递性,2 happens-before 5。

happens-before关系图

在图3-24中,每一个箭头链接的两个节点,代表了一个happens-before关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的happensbefore保证。图3-24表示在线程A释放了锁之后,随后线程B获取同一个锁。在上图中,2 happens-before5。因此,线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见。

锁的释放和获取的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
class ReentrantLockExample {    int a = 0;    ReentrantLock lock = new ReentrantLock();    public void writer() {        lock.lock();     // 获取锁            try {                a++;            } finally {            lock.unlock();  // 释放锁            }        }    public void reader () {        lock.lock();     // 获取锁            try {                int i = a;        ……        } finally {        lock.unlock();  // 释放锁        }    }}

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,马上我们会看到,这个volatile变量是ReentrantLock内存语义实现的关键。

  • 公平锁
    • 公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。
  • 非公平锁
    • 以原子操作的方式更新state变量,本文把Java的compareAndSet()方法调用简称为CAS。此操作具有volatile读和写的内存语义。
      • 编译器
        • 编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。
      • 处理器

公平锁和非公平锁释放时,最后都要写一个volatile变量state。
公平锁获取时,首先会去读volatile变量。
非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

锁释放-获取的内存语义的实现至少有下面两种方式

  • 利用volatile变量的写-读所具有的内存语义。
  • 利用CAS所附带的volatile读和volatile写的内存语义

concurrent包的实现

由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面4种方式。

  • A线程写volatile变量,随后B线程读这个volatile变量。
  • A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  • A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  • A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

volatile变量的读/写和CAS可以实现线程之间的通信。

实现示意图

final域的内存语义

happens-before

JMM的设计

JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

happens-before的定义

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。
    • 从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
  • 如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
    • JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。
      • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
      • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
      • as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提
        下,尽可能地提高程序执行的并行度。

第四章

Java并发基础

线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。

设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。

线程状态变迁

Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
public class Daemon {    public static void main(String[] args) {        Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");        thread.setDaemon(true);        thread.start();    }    static class DaemonRunner implements Runnable {        @Override        public void run() {            try {                SleepUtils.second(10);            } finally {                System.out.println("DaemonThread finally run.");            }        }    }}

运行Daemon程序,可以看到在终端或者命令提示符上没有任何输出。main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机中已经没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonRunner立即终止,但是DaemonRunner中的finally块并没有执行。

启动和终止线程

启动

通过调用线程的start()方法进行启动,随着run()方法的执行完毕,线程也随之终止
线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。注意 启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。

中断

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。

  • 如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
  • 从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(longmillis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

其他

以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。