本文用到的一些关键词语以及常用术语,主要如下:
- 信号量(Semaphore): 是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用,也是作系统用来解决并发中的互斥和同步问题的一种方法。
- 信号量机制(Semaphores): 用来解决同步/互斥的问题的,它是1965年,荷兰学者 Dijkstra提出了一种卓有成效的实现进程互斥与同步的方法。
- 管程(Monitor) : 一般是指管理共享变量以及对共享变量的操作过程,让它们支持并发的一种机制。
基本概述
在Java领域中,我们可以将锁大致分为基于Java语法层面(关键词)实现的锁和基于JDK层面实现的锁。
在Java领域中, 尤其是在并发编程领域,对于多线程并发执行一直有两大核心问题:同步和互斥。其中:
- 互斥(Mutual Exclusion):一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。即就是同一时刻只允许一个线程访问共享资源的问题。
- 同步(Synchronization):两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。即就是线程之间如何通信、协作的问题。
针对对于这两大核心问题,利用管程是能够解决和实现的,因此可以说,管程是并发编程的万能钥匙。
虽然,Java在基于语法层面(synchronized 关键字)实现了对管程技术,但是从使用方式和性能上来说,内置锁(synchronized 关键字)的粒度相对过大,不支持超时和中断等问题。
为了弥补这些问题,从JDK层面对其“重复造轮子”,在JDK内部对其重新设计和定义,甚至实现了新的特性。
在Java领域中,从JDK源码分析来看,基于JDK层面实现的锁大致主要可以分为以下4种方式:
- 基于Lock接口实现的锁
- 基于ReadWriteLock接口实现的锁
- 基于AQS基础同步器实现的锁
- 基于自定义API操作实现的锁
从阅读源码不难发现,在Java SDK 并发包主要通过AbstractQueuedSynchronizer(AQS)实现多线程同步机制的封装与定义,而通过Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
一. 基本理论
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。
在操作系统中,一般有如果I/O操作时,对于阻塞和非阻塞是从函数调用角度来说的,其中:
- 阻塞:如果读写操作没有就绪或者完成,则函数一直等待。
- 非阻塞: 函数立即调用,然后让应用程序轮询循环。
而同步和异步则是从“读写是主要是由谁完成”的角度来说的,其中:
- 同步: 读写操作主要交给应用程序完成
- 异步: 读写操作主要由操作系统完成,一般完成之后,回调函数和事件通知应用程序。
其中,信号量机制(Semaphores)是用来解决同步/互斥的问题的,但是信号量(Semaphore)的操作分散在各个进程或线程中,不方便进行管理,因每次需调用P/V(来自荷兰语 proberen和 verhogen)操作,还可能导致死锁或破坏互斥请求的问题。
由于PV操作对于解决进程互斥/同步编程复杂,因而在此基础上提出了与信号量等价的——“管程技术”。
其中,管程(Monitor)当中定义了共享数据结构只能被管程内部定义的函数所修改,所以如果我们想修改管程内部的共享数据结构的话,只能调用管程内部提供的函数来间接的修改这些数据结构。
一般来说,管程(Monitor)和信号量(Semaphore)是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。
在管程的发展历程上,先后出现过Hasen模型、Hoare模型和MESA模型等三种不同的管程模型,现在正在广泛使用的是MESA模型。
在MESA模型中,管程中引入了条件变量(Conditional Variable)的概念,而且每个条件变量都对应有一个等待队列(Wait Queue)。其中,条件变量和等待队列的作用是解决线程之间的同步问题。
而对于解决线程之间的互斥问题,将共享变量(Shared Variable)及其对共享变量的操作统一封装起来,一般主要是实现一个线程安全的阻塞队列(Blocking Queue),将线程不安全的队列封装起来,对外提供线程安全的操作方法,例如入队操作(Enqueue)和出队操作(Dequeue)。
在Java领域中,对于Java语法层面实现的锁(synchronized 关键字), 其实就是参考了 MESA 模型,并且对 MESA 模型进行了精简,一般在MESA 模型中,条件变量可以有多个,Java 语言内置的管程(synchronized)里只有一个条件变量。
这就意味着,被synchronized 关键字修饰的代码块或者直接标记静态方法以及实例方法,在编译期会自动生成相关加锁(lock)和解锁(unlock)的代码,即就是monitorenter和monitorexit指令。
对于synchronized 关键字来说,主要是在Java HotSpot(TM) VM 虚拟机通过Monitor(监视器)来实现monitorenter和monitorexit指令的。
同时,在Java HotSpot(TM) VM 虚拟机中,每个对象都会有一个监视器,监视器和对象一起创建、销毁。
监视器相当于一个用来监视这些线程进入的特殊房间,其义务是保证(同一时间)只有一个线程可以访问被保护的临界区代码块。
本质上,监视器是一种同步工具,也可以说是JVM对管程的同步机制的封装实现,主要特点是:
- 同步:监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
- 协作:监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。
在Hotspot虚拟机中,监视器是由C++类ObjectMonitor实现的,ObjectMonitor类定义在ObjectMonitor.hpp文件中,其中:
- Owner: 指向的线程即为获得锁的线程
- Cxq:竞争队列(Contention Queue),所有请求锁的线程首先被放在这个竞争队列中
- EntryList:对象实体列表,表示Cxq中那些有资格成为候选资源的线程被移动到EntryList中。
- WaitSet:类似于等待队列,某个拥有ObjectMonitor的线程在调用Object.wait()方法之后将被阻塞,然后该线程将被放置在WaitSet链表中。
同时,管程与Java中面向对象原则(Object Oriented Principle)也是非常契合的,主要体现在 java.lang.Object类中wait()、notify()、notifyAll() 这三个方法,其中:
- wait()方法: 阻塞线程并且进入等待队列
- notify()方法:随机地通知等待队列中的一个线程
- notifyAll()方法: 通知等待队列中的所有线程
不难发现,在Java中synchronized 关键字及 java.lang.Object类中wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。<