锁的独占与共享
java并发包提供的加锁模式分为独占锁和共享锁,独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。共享锁,则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别标识 AQS队列中等待线程的锁获取模式。
很显然,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 java的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。
锁的公平与非公平
锁的公平与非公平,是指线程请求获取锁的过程中,是否允许插队。在公平锁上,线程将按他们发出请求的顺序来获得锁;而非公平锁则允许在线程发出请求后立即尝试获取锁,如果可用则可直接获取锁,尝试失败才进行排队等待。ReentrantLock提供了两种锁获取方式,FairSyn和NofairSync。结论:ReentrantLock是以独占锁的加锁策略实现的互斥锁,同时它提供了公平和非公平两种锁获取方式。
AQS提供锁的模板方法
AQS提供了独占锁和共享锁必须实现的方法,具有独占锁功能的子类,它必须实现tryAcquire、tryRelease、isHeldExclusively等;共享锁功能的子类,必须实现tryAcquireShared和tryReleaseShared等方法,带有Shared后缀的方法都是支持共享锁加锁的语义。Semaphore是一种共享锁,ReentrantLock是一种独占锁。
AbstractQueuedSynchronizer:抽象队列同步器,简称AQS。AQS 采用双向队列的方式,提供基于FIFO方式操作的队列。常用于构建锁或者其它相关同步装置的基础框架。
数据结构如图:
prev:指向队列的前驱结点。
next:指向队列的后继结点。
thread:指向当前线程。
nextWaiter:条件等待队列,是以单向队列。
waitStatus:
1(CANCELLED):代表当前线程由于超时或者中断而取消。
-1(SIGNAL):代表后继结点由于调用park方法导致线程堵塞,当前线程如果要唤醒或者取消后继结点,必须调用unpark方法。
-2(CONDITION):代表当前结点因条件而等待与条件队列。当该condition对象的signal()/signalAll()方法被调用时,该结点会再次有条件队列进入等待队列
-3(PROPAGATE):代表当前结点的事务将被共享到其他结点。
既然AQS是队列,那么重点了解的就是如队列和出队列操作。
AQS封装了队列的复杂操作,扩展实现自定义锁(比如分布式锁),自需要继承AQS,自己实现tryAcquire方法即可。下面我们分别了解公平锁和非公平锁的获取锁和释放锁的过程,然后对比出公平锁和非公平锁的异同点。
非公平锁的获取锁流程
1:在调用锁方法的lock获取锁时,需要看当前锁被多少线程持有,如果没有被线程持有,直接CAS设置锁的线程 持有计数器为1,然后设置当前线程持有锁
2:如果锁已经被持有,在调用lock方法时,需要看是否是当前线程持有锁,如果是则持锁计数器累加一,否则获取锁的线程需要被加入到等待队列
3:在加入等待队列时候,需要初始化双向队列,初始双向队列时候,队列头后队列尾指向同一个虚拟node(node 的thread等于null,prev等于null)
4:自旋方式插入队列尾部
5:自旋操作锁的等待双向队列。当前竞争锁的节点有个特征,prev指向的节点一定是head,否则不予处理。
6:不予处理或者竞争锁再次失败的节点,根据状态施加不同处理方式。ws等于1的为已经取消节点,处理方式是把与之直接相连的取消节点全部删除。前驱ws等于-1的节点,则当前节点调用park方法,表明获取锁阻塞。前驱ws为0、-2、-3的节点,设置ws为-1,下一个自旋操作时会设置阻塞。
公平锁的获取锁流程
7: 红色部分即是公平锁和非公平锁的区别。在没有线程持有锁的情况,公平锁还需要再去确认是否有前驱结(FIFO方式,前驱结点就是等待锁最久的线程)。
作者其实在这里也有疑问: state等于0,表示没有线程持有锁,那为啥还要去看有没有前驱结点存在呢?还是想再次确认
公平锁和非公平锁释放锁流程
公平锁和非公平锁释放锁的流程是一致的,具体如下: