失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Lock接口和AQS原理与实现(Java并发编程的艺术整理)

Lock接口和AQS原理与实现(Java并发编程的艺术整理)

时间:2020-05-11 17:02:14

相关推荐

Lock接口和AQS原理与实现(Java并发编程的艺术整理)

Lock接口

锁是用来控制多个线程访问共享资源的方式,一个锁能够防止多个线程同时访问共享资源(但有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。

使用synchronized关键字会将隐式地获取锁,但是将锁的获取和释放固化了,也就是先获取在释放。缺点就是扩展行没有显示的锁获取和释放来的号。

Lock特性

Lock API

Lock是一个接口,定义了锁获取和释放的基本操作

队列同步器(AbstarctQueuedSynchronizer 简称AQS)

AQS是用来构建锁或者其他同步组件的基础框架,使用了int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

同步器的主要使用的方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程种要对同步状态进行更改。

同步器是实现锁的关键,在锁的实现种聚合同步器,利用同步器实现锁的语义。

锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层状态。锁和同步器隔离了使用者和实现者所需要关注的领域。

AQS的接口

AQS的设计是基于模板模式。使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组价的实现种,并调用提供的模板方法。

重写同步器指定方法是,需要使用同步器提供的3个方法来访问或修改同步状态。

同步器可重写的方法如下:

实现自定义同步组件时,将会调用同步器提供模板方法,这些方法如下:

AQS实现

队列同步器实现主要包括:同步队列独占式同步状态获取与释放共享式同步状态获取与释放以及超时获取同步状态等AQS核心参数数据结构与模板方法

同步队列

AQS依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败式,AQS会将当前线程以及等等待状态等信息构成一个节点(Node)并将节点加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把受节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列中的节点(Node)保存获取同步状态失败的线程引用,等待状态的节点信息如下:

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。

独占式同步状态获取与释放

通过调用同步器的acquire(int arg)方法可获取同步状态,该方法对中断不敏感,就是说线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。方法的主要逻辑是:

首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueuued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移除队列(或停止自旋)的条件时前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。

eg:如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以时共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况是不同的。

通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态。在acquireShared(int arg)方法中,同步器调用tryAcuireShared(int arg)方法尝试获取同步状态,tryAcuireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态。

超时获取同步状态

超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg, long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout -= now - lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时。

独占式超时获取同步状态doAcquireNanos(int arg, longnanosTimeout)和独占式获取同步状态acquire(int args)在流程上非常相似,其主要区别在于未获取到同步状态时的处理逻辑。acquire(int args)在未获取到同步状态时,将会使当前线程一直处于等待状态,而doAcquireNanos(int arg, long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。

基于AQS实现的锁

ReccntrantLock

重入锁ReentrantLock是支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。ReentrantLock还支持获取锁时的公平和非公平性选择。

ReentrantLock没有向synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

重进入

重进入时指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题

(1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果时,则再次成功获取。

(2)锁的最终释放。线程重复n次获取了锁,随后在第N次释放了该锁后,其他线程获取到该锁。锁的最终释放要求锁对获取进行技术自增,计数表示当前锁被重复获取的次数,而锁别释放时,计数自减,当前计数等于0时表示锁已经成功释放。

公平锁与非公平锁

公平性与否时针对获取锁而言的,如果一个锁时公平的,那么锁的获取顺序就应该时符合请求的绝对时间顺序,也就是FIFO。

公平锁与非公平锁区别:同步队列中当前节点是否有前驱节点的判断,线程比当前线程更早的请求获取锁,因此需要等待前驱线程获取并释放锁之后,才能继续获取锁。

公平锁保证了锁的获取按照FIFO原则,代价时进行大量的线程切换。非公平锁虽然可能造成线程“饥饿”,但是极少的线程切换,保证了更大的吞吐量。

读写锁(ReentrantReadWriteLock)

ReentrantReadWriteLock的特性

读写锁接口

ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,而实现-ReentrantReadWriteLock 。

ReentrantReadWriteLock展示内部工作状态的方法

读写锁的实现

ReentrantReadWriteLock的实现主要包括:读写状态的设计,写锁的获取与释放,读锁的获取与释放以及锁降级

读写状态的设计

读写锁依赖AQS来实现同步功能,所有读写状态就是其AQS的同步状态。而ReentranLock中定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整形变量)上维护多个线程和一个写线程的状态。

如果在一个整形变量上维护多种状态,就一定需要按位切割使用这个变量,读写锁将变量切分成了两个部分,高16位表示读低16位表示写

写锁的获取与释放

写锁是一个支持重进入的排他锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁是,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

该实现的tryAcquire除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功的获取,所作只是线程安全的增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。

读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值时(1<<16).

锁降级

锁降级指的时写锁降级为读锁,如果当前线程拥有写锁,然后将其释放,最后在获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,在获取到读锁,随后释放(先前拥有的)写锁的过程。

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程.

Condition 接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object),主要包括wait(),wait(long timeout),notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类型Object的监控方法,与Lock配合可以实现等待/通知模式。

Condition接口方法

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的。

一般Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

Condition的部分方法

Condition实现分析

ConditionObject是同步器AQS的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也比较合理。每个Condition对象都包含者一个队列(一下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

Condition的实现主要包括:等待队列,等待和通知。

等待队列

等待队列是一个FIFO队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁,构造成节点加入等待队列并进入等待状态。节点的定义服用了AQS的中节点的定义,也就是说,同步队列和等待队列中节点类型都是AbstractQueuedSynchronizer.Node

一个Condition包含一个等待对列,Condition拥有首节点和为节点。当前线程调用 Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待对列。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中Lock拥有一个同步队列和多个等得队列。

等待

调用Condition的await()方法,会使当前线程进入到等得队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关的锁。

调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造节点并加入到等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。

通知

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中。

调用该方法的前置条件时当前线程必须获取了锁,可以看待signal()方法进行了isHeldExclusively()检查,当前线成必须时获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。通过调用AQS中的enq(Node node)方法,等待队列中的头节点线程安全移动到同步队列,当前节点移动到同步队列后,当前线程在使用LockSupport唤醒该节点的线程。被唤醒后的线程,将从await()方法中的while()循环中退出,进而调用AQS的acquireQueued()方法加入发哦获取同步状态的竞争中。成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

参考书籍:

《Java并发编程的艺术》

如果觉得《Lock接口和AQS原理与实现(Java并发编程的艺术整理)》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。