Java中的锁
这里主要说一下Lock接口相关的实现部分
Lock接口与AbstractQueueSynchronizer(AQS)
Lock接口,该接口提供了编程式的获取/释放锁的能力,对于复杂的锁访问,通过Lock来进行锁控制非常好。
队列同步器:之前讲过,阻塞队列之后线程之间在竞争获取临界区资源时会进入同步队列,在Lock实现时,基本都可以借助于队列同步器(AbstractQueueSynchronizer,AQS)实现,它是面相锁的实现者的,它简化了锁的实现方式,屏蔽了同步状态管理,线程排队,等待与唤醒等底层操作,锁是面相使用者而言的,它提供了多线程之间对资源的访问,屏蔽了实现细节。锁和同步器很好的隔离了使用者和实现者所需关注的领域。
队列同步器采用模版方法模式定义了统一的访问流程,子类只需要根据自己的需求重写部分模版,如独占式/共享式的获取/释放锁,自己管理状态分配(实际上就是一个int类型的数据,我们把它当作临界资源分配标志,当我们修改时都要通过CAS保证线程安全),具体的同步交给同步队列器做就好了。
队列同步器大量采用CAS操作来完成安全的设置值操作,其队列部分采用虚拟的FIFO双向链表,同步队列器基本结构如图
队列中的节点信息如图
队列中的节点信息
可以看到节点信息持有了线程状态,前后节点,对应的线程等。这里基本上可以看出来就是通过同步队列器的同步队列来控制多线程访问临街资源了。
维护队列的原则,只有头节点拥有同步状态,当头节点释放时,唤醒后继节点,该节点线程检查前驱节点是否为头节点与尝试获取同步状态,如果成功将自己设置为头节点,然后方法返回。 对应的图如下:
独占式获取同步状态流程 节点自旋获取同步状态自旋获取同步状态时所使用的是虚线,实际上在每次被唤醒时若不是前驱节点为首节点或竞争获取同步状态失败,都会将自身挂起等待下次唤醒,所以这个自旋并不是真的完全自旋。
对于重入锁来说,也就是对同步状态第一次获取时绑定获取的线程,在线程重入时,状态累加,在线程释放时,状态累减。直到0那么清理绑定线程,允许其他线程进入。
对于读写锁来说,由于只有一个int类型来做同步状态,需要将int分解为高16位与低16位。
写锁是一个支持重入的排他锁,当有读锁被获取,或其他锁持有写锁时会等待。
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问 (或者写状态为0)时,读锁总会被成功地获取。
锁降级,当持有写锁时可以再持有读锁,然后释放写锁,那么就只持有读锁了,也就达到了锁降级。
可以看到基本的套路都是,为了实现不同的锁模式,继承AQS后只需要自己进行分配同步状态就可以很简单的实现锁。这样我们自己也可以继承AQS实现自定义的锁。
Condition实现等待/通知
Condition:任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
刚才说的是同步队列实现部分,现在这个是等待队列实现部分。
同步队列与等待队列理解图
一个锁对象可以产生多个Condition对象,每个Condition对象都持有有一个虚拟的单向队列,节点信息和AQS中的节点信息一致。当线程调用wait()方法时,会将当前线程加入到等待队列并释放锁(如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。),然后通过判断自己是否在同步队列中来进行自旋(会阻塞等通知),当其他线程调用该condition的signal()方法时,则去除队列头节点加入到同步队列中,然后唤醒该线程,那么刚才的自旋就会被打开,接着进行状态判断,进入获取同步状态的竞争中(有可能又进入同步队列的自旋,同步队列的自旋主要是获取锁,这两个自旋不要弄混了)。具体的await()代码流程如图
await()与signal()代码理解图这里很明显可以看出等待队列的自旋只是等待别人将自己移到同步队列,然后唤醒自己进入竞争同步资源,等待获取到同步资源(锁)线程也就真的被唤醒了
之前的Object对象的监视器方法wait(),notify()实际上也就一样的同步队列/等待队列,只不过那个对象就一个等待队列,通过Lock和Condition对象可以产生一个同步队列和多个等待队列。用这个方法实现读写分别阻塞的队列是非常经典的方式。
其他
之前写过的并发编程基础大体讲述了一下对象,监视器,同步队列,等待队列的关系,这里侧重讲了一下实现部分。实际上如果没有复杂的锁操作或者等待/通知的要求,更推荐使用synchronized进行代码块的锁,在JDK1.6之后synchronized在JVM层也实现了很多的优化,性能并不比AQS差,而且随着JDK的升级synchronized还能获得更多的优化。