Notes
  • Introduce
  • Go
    • Grammar
      • Basic
      • Goroutines & Channels
      • Test
    • System Library
      • Module
      • sync
      • context
      • net
    • Concurrency in Go
    • The Go Memory Model
    • Code Snippet
  • Rust
    • The Rust Programming Language
    • Rust by Example
  • JAVA
    • Preface
    • Grammar
      • Basic
      • Data Types
      • Operator
      • Exceptions
    • Class Libraries
      • Collection
      • Stream
      • IO
      • NIO
      • RMI
    • Concurrency
      • Preface
      • JMM
      • Synchronized & CAS
      • Deadlock
      • Thread
      • Lock & Condition
      • Utility Class
      • Thread-safe Collection
      • Atomic Class
      • Fork/Join
      • Concurrency Design Patterns
        • Immutable
        • Copy-on-Write
        • ThreadLocal
        • Multitheading If
        • Division
    • JVM
      • Class & Instance Initialization
      • Runtime Data Area
      • Garbage Collection
    • Web Container
      • Tomcat Architecture
      • Jetty Architecture
    • Spring
    • Tuning
      • Programming
  • Computer Science
    • Computer Organization
    • Algorithm
      • Complexity
      • Linear List
      • Sort
      • Binary Search
      • Skip List
      • Hash Table
      • Tree
      • Graph
      • String Matching
      • Bloom Filter
      • Greedy Algorithm
      • Divide and Conquer
      • Back Tracking
      • Dynamic Programming
    • Network Protocol
      • Pysical Layer
      • Data Link Layer
      • Network Layer
      • Transport Layer
      • Application layer
      • HTTP
      • HTTP/2 in Action
    • Operating System
      • Basic
      • System Initialization
      • Diagnostic Tools
      • CPU Diagnosis
      • Memory Diagnosis
      • Disk Diagnosis
      • Network Diagnosis
      • Monitor System
    • Design Patterns
      • UML
      • OOP
      • Principle
      • Refactoring & Specification
      • Creational
        • Singleton
        • Factory
        • Builder
        • Prototype
      • Structural
        • Proxy
        • Bridge
        • Decorator
        • Adapter
        • Facade
        • Composite
        • FlyWeight
      • Behavioral
        • Observer
        • Template Method
        • Strategy
        • State
        • Iterator
        • Chain of Responsibility
    • Distributed System
      • Protocol & Algorithm
      • Transcation
      • Theory
      • Resource Management
      • Scheduling
      • Computing
      • Message Queue
      • Cache
      • Consistent Hashing
  • database
    • InfluxDB
      • In-Memory Index
      • Meta
    • MySQL
      • SQL
      • Architecture
      • Log
      • Transaction
      • Indexing
      • Lock
      • Storage
    • Redis
    • Elasticsearch
      • Local Debug
    • HBase
    • Kafka
    • ZooKeeper
  • Reading
    • RocketMQ
    • 演说之禅
    • So Good They Can't Ignore You
    • 学会提问
    • Lecture
  • Other
    • v2ray
    • Kubernetes
    • Git
    • Maven
    • Anaconda And Conda
    • Fuck! Shit!
      • Remove Final by Reflection
      • Ingress Host
      • ExecuterService submit
  • Open source contribution
Powered by GitBook
On this page
  • 管程
  • MESA 模型
  • 三种模型对比
  • notify() 何时用
  • Java 与 管程
  • 信号量
  • 分工、同步、互斥
  • 分工
  • 同步
  • 互斥
  • 微观
  • 可见性
  • 原子性
  • 有序性
  • 宏观
  • 安全性问题
  • 活跃性问题
  • 性能问题

Was this helpful?

  1. JAVA
  2. Concurrency

Preface

PreviousConcurrencyNextJMM

Last updated 6 years ago

Was this helpful?

管程

管程(Monitor)作为一种解决并发问题的模型,是继信号量模型之后的一项重大创新,它与信号量在逻辑上是等价的(可以用管程实现信号量,也可以用信号量实现管程),但是相比之下管程更易用。

管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。

管程有三种模型:Hasen 模型、Hoare 模型、MESA 模型。

MESA 模型

应用最广泛的事 MESA 模型。

并发编程的两大核心问题:互斥、同步。

管程解决互斥问题

将共享变量及其对共享变量的操作统一封装起来。可以看出,管程和面向对象的思路类似,这可能也是 Java 选择管程模型的原因之一吧。

管程解决同步问题

  • 条件变量:

    • 进入条件变量等待队列:在 Java 里面采用 wait() 或 Condition.await() 实现。

    • 通知条件变量:在 Java 里面采用 notify()/notifyAll() 或 Condition.signal() 实现。

三种模型对比

管程的三种模型的核心区别在于如何通知相关线程:

  • Hasen 模型: 要求通知放在最后,线程 T2通知 T1后,T2就运行结束了,然后 T1执行。

  • Hoare 模型:T2 通知T1后,T2阻塞,T1执行;等 T1执行完,唤醒 T2。所以 T2多了一次阻塞唤醒操作。

  • MESA 模型:T2通知T1后,T2继续执行,T1从条件等待队列进入入口等待队列。

    • 优点:通知不用放在最后,T2也没有多余的阻塞唤醒操作。

    • 缺点:当 T1再次执行的时候,可能条件已经变化不满足了。所以 MESA 模型需要以循环的方式检验条件变量,有一个编程范式,见下面代码。

while(条件不满足) {
  wait();
}

notify() 何时用

一般来说,除非经过深思熟虑,否则尽量使用 notifyAll()。

当满足以下三个条件时,可以使用 notify():

  1. 所有等待线程拥有相同的等待条件;

  2. 所有等待线程被唤醒后,执行相同的操作;

  3. 只需要唤醒一个线程

Java 与 管程

管程有三种模型,Java 参考了 MESA 模型,并实现了它。

synchronized、wait()、notify() 不过是操作系统领域里管程模型的一种实现而已。Java SDK 并发包里的 Lock 和 Condition 是管程的另一种实现。

  • 内置的 synchronized 是对 MESA 模型的精简,MESA 模型可以有多个条件变量,synchronized 只有一个。但是使用简单,会自动加锁、解锁。

  • JDK 并发包的 Lock 和 Condition 是另一套实现,支持多个条件变量,但是需要手动加锁和解锁。

信号量

信号量(Semaphore) 在1965年提出,之后一直都是并发编程领域的王者,直到管程模型被提出。

信号量模型:一个计数器、一个等待队列、三个方法。

  • init():设置计数器初始值。

  • down():计数器减 1;如果此时计数器的值小于 0,则当前线程进入等待队列阻塞,否则当前线程可以继续执行。

  • up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

这三个方法都是原子性的,由实现方保证。

Java 的信号量由java.util.concurrent.Semaphore实现,up()对应acquire(),down()对应release()。

分工、同步、互斥

并发编程可以总结为三个核心问题:分工、同步、互斥。

为了性能,如 IO 等待的时候不能让 CPU 闲着,所以我们把任务拆分交替执行,有了分时操作系统,出现了并发,后来 CPU 多核了又有了并行计算,也就是分工。

线程之间需要通信,于是操作系统提供了一些让进程,线程之间通信的方式,也就是同步。

并发和通信带来了多线程并发操作共享资源的问题,又要将对共享资源的访问串行化,所以设计了了锁,信号量等,也就是互斥。

分工

分工指的是如何高效地拆解任务并分配给线程。

Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法。

并发编程领域的设计模式:生产者 - 消费者、Thread-Per-Message、Worker Thread 模式等都是用来指导你如何分工的。

同步

同步指的是线程之间如何协作。

Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是分工方法,但同时也能解决线程协作的问题。Java SDK 里提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用来解决线程协作问题的。

协作问题的本质:当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。

解决协作问题的核心技术是管程,上面提到的所有线程协作技术底层都是利用管程解决的。

互斥

互斥是保证同一时刻只允许一个线程访问共享资源。

分工、同步主要强调的是性能,但并发程序里还有一部分是关于正确性的,用专业术语叫“线程安全”。

不正确的主要源头是可见性问题、有序性问题和原子性问题。Java 引入内存模型可以避免可见性问题、有序性问题,但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥。

实现互斥的核心技术就是锁,Java 语言里 synchronized、SDK 里的各种 Lock 都能解决互斥问题。

CountDownLatch 就是一种典型的同步方式,而可重入锁则是一种互斥手段。

微观

CPU、内存、I/O 设备这三者的速度差别非常大,为了合理利用 CPU 的性能,平衡三者的速度,一般有如下措施:

  • 硬件:CPU 增加缓存,均衡 CPU 与内存的速度。会带来可见性问题。

  • 操作系统:增加进程、线程,分时复用 CPU,均衡 CPU 与 IO 设备的速度。会带来原子性问题。

  • 编译器:优化指令执行顺序,使缓存能更加合理利用。会带来有序性问题。

但是这三个优化又带来了问题。

可见性

CPU 缓存导致可见性问题。一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

单核 CPU 倒是没什么问题,但是多核 CPU 每核有自己的缓存,会导致可见性问题。

原子性

分时操作系统的线程切换带来原子性问题。一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符。

32位机器上对 long 进行操作不具有原子性。所以对 long 和 double 类型的变量,如有并发操作,最好加 volatile 或加锁。

有序性

编译优化带来有序性问题。

如下代码是双重检查机制创建单例对象,但是有问题。在 new 一个对象时,重排序后可能的执行方式为:

  1. 分配一块内存 M;

  2. 将 M 的地址赋值给 instance 变量;

  3. 最后在内存 M 上初始化 Singleton 对象。

若线程 A 拿到锁,执行了 new 方法,然后切换到线程 B,那么有可能线程 B 判断 instance 不为 null(锁外面的检查),但是 instance 的内容还没初始化。可以用 volatile 解决,禁止指令重排序。

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

宏观

并发编程中我们需要注意的问题有很多,主要是:安全性问题、活跃性问题、性能问题。

安全性问题

线程安全本质上就是正确性,而正确性的含义就是程序按照我们期望的执行。上文已经提到并发 Bug 的源头是原子性、可见性、有序性。但并不是所有的代码都需要分析上面三个问题,只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。

所以若能做到不共享数据或者数据状态不发生变化,也能够保证线程安全。这类技术方案有线程本地存储(Thread Local Storage,TLS)、不变模式等等。

当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,叫做数据竞争(Data Race)。

竞态条件(Race Condition):程序的执行结果依赖线程执行的顺序。也可以这样理解竞态条件,在并发场景中,程序的执行依赖于某个状态变量,当某个线程发现状态变量满足执行条件后,开始执行操作;可是就在这个线程执行操作的时候,其他线程同时修改了状态变量,导致状态变量不满足执行条件了。

面对数据竞争和竞态条件问题,都可以用互斥这个技术方案,逻辑上可以统一为:锁。

活跃性问题

活跃性问题:某个操作无法执行下去。有三种情况:

  • 死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

  • 活锁:线程虽然没有发生阻塞,但仍然会存在执行不下去。可以用一个随机等待时间解决。Raft 分布式一致性算法中也用到了。

  • 饥饿:线程因无法访问所需资源而无法执行下去。

    • 若线程优先级不均匀,低优先级线程执行机会很小,则可能发生饥饿;

    • 持有锁的线程,执行时间太长,也可能导致饥饿;

    • 解决方案:一保证资源充足,二公平分配资源,三避免持有锁的线程执行过长。一、三比较难满足,第二点可以用公平锁(先来后到的方案,线程的等待是有顺序的)来解决。

性能问题

锁的过度使用可能导致串行化的范围过大,这样就不能够发挥多线程的优势了。

  • 无锁的算法和数据结构:线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等;原子类是无锁的数据结构;Disruptor 是一个无锁的内存队列。

  • 减少锁持有的时间:使用细粒度的锁(如 ConcurrentHashMap 1.8之前),读写锁等。

性能指标:

  • 吞吐量:单位时间内能处理的请求数量。

  • 延迟:从发出请求到收到响应的时间。

  • 并发量:能同时处理的请求数量。一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。

入口等待队列:当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。可参考 Java synchronized 实现中 对象的 _EntryList 字段。

条件变量等待队列:每个条件变量都对应有一个等待队列。可参考 Java synchronized 实现中 对象的 _WaitSet 字段。

ObjectMonitor
ObjectMonitor
信号量模型