Java同步相关基础概念

2019/03/17 JAVA

Java同步相关基础概念

1.JUC包结构

2019-03-17-JUC结构

2.Monitor(管程)

管程(Monitors,也叫监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。管程实现了在一个时间点最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现访问的并发程序设计相比,管程实现很大程度上简化了程序设计。管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。

2.1 特点

Monitor其实是一种同步工具,也可以说是一种同步机制,通常被描述为一个对象,主要的特点是:

对象的所有方法都被互斥的执行。好比一个Monitor只有一个运行许可,任一个线程进入任何一个方法都需要获得这个许可,离开时把许可归还;

通过提供singal机制:允许正持有许可的线程暂时放弃许可,等待某个条件变量成真,而条件成立后,当前进程可以通知正在等待这个条件变量的线程,让它可以重新去获取运行许可。

2.2 线程同步相关的Monitor

在多线程访问共享资源的时候,经常会带来可见性和原子性的安全问题。为了解决这类线程安全的问题,Java提供了同步机制,互斥锁机制,这些机制保证了在同一个时刻只有一个线程能访问共享资源。这些机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。

2.3 Monitor的实现

HotSpot中,Monitor由ObjectMonitor实现,基于C++。有几个关键属性:

_owner:指向持有ObjectMonitor对象的线程;

_WaitSet:存放处于wait状态的线程队列;

_EntryList:存放处于等待锁block状态的线程队列;

_recursions:锁的重入次数;

_count:用来记录该线程获取锁的次数。

当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的Monitor后进入_Owner区域并把Monitor中的_Owner变量设置为当前线程,同时Monitor中的计数器_count加1。即获得对象锁。

若持有Monitor的线程调用wait()方法,将释放当前持有的Monitor,_Owner变量恢复为null_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放Monitor并复位变量的值,以便其他线程进入获取Monitor。

3.AQS(同步器)

AQS(AbstractQueuedSynchronizer)是一个底层同步工具类,主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLockCountdownLatch就是基于AQS实现的,用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。

3.1 功能

具体一点的功能有:

内部同步状态的管理(例如表示一个锁的状态是获取还是释放);

同步状态的更新和检查操作,且至少有一个方法会导致调用线程在同步状态时被阻塞,以及在其他线程改变这个同步状态时解除线程的阻塞。

这些实际例子包括:互斥排它锁的不同形式,读写锁,信号量,屏障,Future,事件指示器,传送队列等。

3.2 主要操作

核心有两个步骤

acquire(获取)操作:

while(当前同步器的状态不允许获取操作){

​ 如果当前线程步骤队列中,则将其插入队列

​ 阻塞当前线程

}

如果线程位于队列中,则将其移出队列

release(释放)操作:

更新同步器状态

if(新的状态允许某个被阻塞的线程获取成功){

​ 解除队列中一个或多个线程的阻塞状态

}

这两个操作中三个关键的操作:同步器状态变更,线程阻塞和释放,插入和移出队列。

3.3 基本组件

为了实现上述两个操作,需要协调三个关键操作而引申出三个基本组件:

同步器状态的原子性管理;

线程阻塞和解除阻塞;

队列管理。

同步状态

AQS类使用volatile修饰的int变量state来保存同步状态,并且通过使用CAS指令来实现compareAndSetState,使得当且仅当同步状态拥有一个一致的期望值的时候,才会被原子的设置为新值,这样就确保了同步状态的原子性,可见性,有序性。

阻塞

之前的阻塞线程和解除阻塞都是基于Java的内置管程,没有其他非基于Java内置管程的API可以使用。

JUC包提供LockSupport类解决这个问题。LockSupport.park()方法阻塞当前线程直到LockSupport.unpark()方法被调用。unpark的调用是没有被计数的,因此在一个park调用前多次调用unpark方法只会解除一个park操作。park方法可以与JVM的Thread.interrupt结合,可通过中断来unpark一个线程。

队列

整个框架的核心是如何管理线程阻塞队列,该队列是FIFO的,因此不支持线程优先级的同步。同步队列的最佳选择的自身没有使用底层锁来构造的非阻塞数据结构

条件队列

队列的管理除了有同步队列,还有条件队列。AQS只有一个同步队列,但是可以有多个条件队列。AQS框架提供了一个ConditionObject类,给维护独占同步的类以及实现Lock接口的类使用。

ConditionObject类实现了Condition接口,Condition接口提供了类似Object管程式的方法,如await,signal和signalAll操作,还扩展了带有超时,检测和监控的方法。ConditionObject类有效的将条件与其他同步操作结合到一起。该类只支持Java风格的管程访问规则,当且仅当当前线程持有锁且要操作条件(Condition)属于该锁时,条件操作才是合法的。这样,一个ConditionObject关联到一个ReentrantLock上就表现的根内置的管程(通过Object.wait等)一样了。两者的不同仅仅在于方法的名称,额外的功能以及用户可以为每个锁声明多个条件。

ConditionObject类和AQS共用内部节点,有自己单独的条件队列。

signal操作是通过将节点从条件队列转移到同步队列中来实现的,没有必要在需要唤醒的线程重新获取到锁之前将其唤醒。

await操作就是当前线程节点从同步队列进入条件队列进行等待。

实现这些操作主要复杂在,因超时或Thread.interrupt导致取消了条件等待时,该如何处理。await和signal几乎同时发生会有竞态问题,最终结果遵照内置管程相关的规范。

3.4 应用

AQS被大量应用在同步工具类上。

ReentrantLock:使用AQS同步状态保存锁重复持有的次数。当锁被一个线程获取时,ReentrantLock也会记录下当前获取锁的线程标识,以便检查是否是重复获取,以及当错误的线程试图进行解锁操作时检测是否存在非法状态异常。ReentrantLock也使用了AQS提供的ConditionObject,还向外暴露了其他监控和检测相关的方法。

ReentrantReadWriteLock:使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位保存读锁的持有次数。WriteLock的构建方式同ReentrantLock;ReadLock则通过使用acquireShared方法来支持同时允许多个读线程。

Semaphore:使用AQS同步状态来保存信号量的当前计数。它里面定义的acquireShared方法会减少计数,或当计数为非正值时阻塞线程;tryRelease方法会增加计数,在计数为正值时还要解除线程的阻塞。

CountDownLatch:使用AQS同步状态来表示计数。当该计数为0时,所有的acquire操作(对应到CountDownLatch中就是await方法)才能通过。

FutureTask:使用AQS同步状态来表示某个异步计算任务的运行状态(初始化,运行中,被取消和完成)。设置(FutureTask中的set方法)或取消(FutureTask中的cancel方法)一个FutureTask时会调用AQS的release操作,等待计算结果的线程的阻塞解除是通过AQS的acquire操作实现的。

SynchronousQueues:使用AQS内部的等待节点,这些节点可以用于协调生产者和消费者。同时,使用AQS同步状态来控制当某个消费者消费当前一项时,允许一个生产者继续生产,反之亦然。

参考:

Moniter的实现原理

看懂AQS的前世今生

Search

    Table of Contents