并发编程基础

什么是共享资源

堆是被所有线程共享的一块区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,Java中几乎所有的对象实例都在这里分配内存。

方法区与堆区一样,也是每个线程共享的一块内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

栈中数据是私有的,是线程隔离的。
alt text

难点

  1. 原子性问题
  • 操作系统做任务切换,可以发生在任何一条CPU指令执行完成之后。
  • CPU能保证的原子操作是指令级别的,而不是高级语言的操作符。
    alt text
  1. 可见性问题
  • 可见性是指一个线程对共享变量的修改,另外一个线程能够立刻看到。
  • 可见性问题是由CPU的缓存导致的,多核CPU均有各自的缓存,这些缓存均要与内存同步。
    alt text
  1. 有序性
  • 在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。
  • 重排序不会影响单线程的执行结果,但在并发的情况下,可能会出现诡异的bug。
    alt text

    Java Memory Model

  1. 并发编程的关键目标

并发编程需要解决两个问题:线程之间如何通信和同步

  • 通信:线程之间以何种机制交换信息
  • 同步:程序中用于控制不同线程之间的操作发生的相对顺序机制
  1. 并发编程的内存模型

共有两种并发编程模型:共享内存模型、信息传递型,Java采用的是前者。

  • 在共享内存模型下,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
  • 在共享内存模型下,同步是显式进行的,程序员必须显式指定某段代码需要在线程之间互斥执行。
  1. JMM
    JMM是Java Memory Model的缩写,Java线程之间的通信是由JMM控制,即JMM决定一个线程对共享变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系,通过控制主内存与每个本地内存(抽象概念)之间的交互,JMM为Java程序员提供了内存可见性的保证。
    alt text
    4.源代码与指令间的重排序

为了提高性能,编译器和处理器常常会对指令做重排序。重排序有3种类型,其中后2种都是处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。

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

alt text
5.重排序对可见性的影响
参考下表,虽然处理器执行的顺序是A1->A2,但是从内存角度来看,实际发生的顺序是A2->A1。这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此它们都会允许对写-读操作执行重排序。
alt text
6.如何解决重排序带来的问题
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barries/Memory Fence)指令,通过内存屏障指令来禁止某特定类型的处理器重排序。
由于常见的处理器内存模型比JMM要弱,Java编译器在生成字节码的时候,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱不同,为了在不同的处理器平台 向程序员展示一个一致性的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不同。

  • CPU内存屏障
    1.LoadLoad: 禁止读和读的重排序
    2.StoreStore: 禁止写和写的重排序
    3.LoadStore: 禁止写和读的重排序
    4.StoreLoad: 禁止读和写的重排序
  • Java内存屏障
    public final class Unsafe{
    public native void loadFence(); // LoadLoad + LoadStore
    public native void storeFence();// StoreStore + LoadStore
    public native void fullFence(); // LoadFence() + storeFence() + StoreLoad
    }

7.happens-before
JMM使用happens-before规则来阐述操作之间的内存可见性,以及什么时候不能重排序。在JMM中如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系换个角度来说,如果A happens-before B,则意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。其中,前4条规则与程序员密切相关。
1.程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作;

  1. volatile变量规则: 对一个volatile域的写,happens-before于任意后续对这个volatile域的读;
  2. synchronized规则: 对一个锁的解锁,happens-before于随后对这个锁的加锁;
  3. 传递性: 若Ahappens-before B,且B happens-before C,则Ahappens-before C
  4. start()规则: 若线程A执行Thread.start(),则线程A的start()操作happens-before于线程B中的任意操作
  5. join0规则: 若线程A执行ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()的成功返回。
    alt txt

    volatile

    1.volatile的基本特性
  • 可见性:对一个volatile变量的读,总是能看到对中国volatile变量的最后写入
  • 原子性:对任意单个volatile变量的读写具有原子性,但类似volatile++这种复合操作不具有原子性

2.volatile的内存语义

  • 写内存语义:当写一个volatile变量时,JMM会把该线程本地内存中的共享变量的值刷新到主内存读- 内存语义:当读一个volatile变量时,JMM会把该线程本地内存置为无效,使其从主内存中读取共享变量。

3.volatile实现机制
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • 在每个volatile读操作的后面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障

4.volatile与锁的对比
volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上锁比volatile更强大,在可伸缩性和执行性能上volatile更有优势

1.内存语义:

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

2.实现机制

  • synchronized:采用“CAS + Mark Word”实现,存在锁升级的情况;
  • Lock: 采用“CAS + volatile”实现,存在锁降级的情况,核心是AOS;

为什么需要Lock
1.支持响应中断
2.支持超时机制
3.支持以非阻塞的方式获取锁
4.支持多个条件变量(阻塞队列)

原子类

线程池

并发工具