1 ReentrantReadWriteLock回顾
- 读写锁适用于读多写少的场景,内部有写锁和读锁。
- 读锁是一把共享锁,当一个线程持有某一个数据的读锁时,其他线程也可以对这条数据进行读取,但是不能写。
- 写锁是一把独占锁,一个线程持有某一个数据的写锁时,其他线程是不可以获取到这条数据的写锁和读锁的。
- 对于锁升级来说,当一个线程在没有释放读锁的情况下,就去申请写锁,是不支持的。
- 对于锁降级来说,当一个线程在没有释放写锁的情况下,去申请读锁,是支持的。
2 StampedLock特点
- 获取锁的方法,会返回一个票据(stamp),当该值为0代表获取锁失败,其他值都代表成功。
- 释放锁的方法,都需要传递获取锁时返回的票据,从而控制是同一把锁。
- StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁就会造成死锁。
- StampedLock提供了三种模式控制读写操作:写锁、悲观读锁、乐观读锁。
写锁:
使用类似于ReentrantReadWriteLock,是一把独占锁,当一个线程获取该锁后,其他请求线程会阻塞等待。对于一条数据没有线程持有写锁或悲观读锁时,才可以获取到写锁,获取成功后会返回一个票据,当释放写锁时,需要传递获取锁时得到的票据。
悲观读锁:
使用类似于ReentrantReadWriteLock,是一把共享锁,多个线程可以同时持有该锁。当一个数据没有线程获取写锁的情况下,多个线程可以同时获取到悲观读锁,当获取到后会返回一个票据,并且阻塞线程获取写锁。当释放锁时,需要传递获取锁时得到的票据。
乐观读锁:
这把锁是StampedLock新增加的。可以把它理解为是一个悲观锁的弱化版。当没有线程持有写锁时,可以获取乐观读锁,并且返回一个票据。值得注意的是,它认为在获取到乐观读锁后,数据不会发生修改,获取到乐观读锁后,其并不会阻塞写入的操作。
在获取票据时,会将需要的数据拷贝一份,在真正读取数据时,会调用StampedLock中的API,验证票据是否有效。如果在获取到票据到使用数据这期间,有线程获取到了写锁并修改数据的话,则票据就会失效。 如果验证票据有效性时,当返回true,代表票据仍有效,数据没有被修改过,则直接读取原有数据。当返回flase,代表票据失效,数据被修改过,则重新拷贝最新数据使用。
乐观读锁适用于一些很短的只读代码,它可以降低线程之间的锁竞争,从而提高系统吞吐量。但对于读锁获取数据结果必须要进行校验。
3 源码解析
3.1 实现原理解析
3.1.1 实例化
1)StampedLock是基于CLH自旋锁实现,锁会维护一个等待线程链表队列,所有没有成功申请到锁的线程都以FIFO的策略记录到队列中,队列中每个节点代表一个线程,节点保存一个标记位,判断当前线程是否已经释放锁。
当一个线程试图获取锁时,首先取得当前队列的尾部节点作为其前序节点,并判断前序节点是否已经释放锁,如果前序节点没有释放锁,则当前线程还不能执行,进入自旋等待。如果前序节点已经释放锁,则当前线程执行。
2)了解一些StampedLock类的常量值
// Values for node status; order matters
private static final int WAITING = -1;
private static final int CANCELLED = 1;
// Modes for nodes (int not boolean to allow arithmetic)
private static final int RMODE = 0;
private static final int WMODE = 1;
/** Wait nodes */
static final class WNode {
volatile WNode prev;
volatile WNode next;
volatile WNode cowait; // list of linked readers
volatile Thread thread; // non-null while possibly parked
volatile int status; // 0, WAITING, or CANCELLED
final int mode; // RMODE or WMODE
WNode(int m, WNode p) { mode = m; prev = p; }
}
/** Head of CLH queue */
private transient volatile WNode whead;
/** Tail (last) of CLH queue */
private transient volatile WNode wtail;
// views
transient ReadLockView readLockView;
transient WriteLockView writeLockView;
transient ReadWriteLockView readWriteLockView;
/** Lock sequence/state */
private transient volatile long state;
/** extra reader count when state read count saturated */
private transient int readerOverflow;
state:
当前锁的状态,是由写锁占用还是由读锁占用。其中long的倒数第八位是1,则表示由写锁占用(00000001),前七位由读锁占用(1-126)。
readerOverFlow:
当读锁的数量超过了范围,通过该值进行记录。
3)当实例化StampedLock时,会设置节点状态值为ORIGIN(00000000)。
3.1.2 获取锁过程分析
假设现在有四个线程:ThreadA获取写锁、ThreadB获取读锁、ThreadC获取读锁、ThreadD获取写锁。
1)ThreadA获取写锁
该方法用于获取写锁,如果当前读锁和写锁都未被使用的话,则获取成功并更新state,返回一个long值,代表当前写锁的票据,如果获取失败,则调用acquireWrite()将写锁放入等待队列中。因为当前还没有任务线程获取到锁,所以ThreadA获取写锁成功。
2)ThreadB获取读锁
该方法用于获取读锁,如果写锁未被占用,则获取成功,返回一个long值,并更新state,如果有写锁存在,则调用acquireRead(),将当前线程包装成一个WNODE放入等待队列,线程会被阻塞。
因为现在ThreadA已经获取到了写锁并且没有释放,所以ThreadB在获取读锁时,一定会阻塞,被包装成WNode进入等待队列中。
在acquireRead()内部会有两个for循环进行自旋尝试获取锁,每个for循环次数由CPU核数决定,进入到该方法后,首先第一次自旋会尝试获取读锁,获取成功,则直接返回。否则,ThreadB会初始化等待队列,并创建一个WNode,作为队头放入等待队列,其内部模式为写模式,线程对象为null,status为0【初始化】。同时还会将当前线程ThreadB包装为WNode放入等待队列的队尾中,其内部模式为读模式,thread为当前ThreadB对象,status为0。
当进入到第二次自旋后,还是先尝试获取读锁,如果仍没有获取到,则将前驱节点的状态设置为-1【WAITING】,用于代表当前ThreadB已经进入等待阻塞。
3)ThreadC获取读锁
ThreadC在获取读锁时,其过程与ThreadB类似,因为ThreadA的写锁没有释放,ThreadC也会进入等待队列。但与ThreadB不同的是,ThreadC不会占用等待队列中的一个新节点,因为其前面的ThreadB也是一个读节点,它会赋值给用于表达ThreadB的WNode中的cowait属性,实际上构成一个栈。
4)ThreadD获取写锁
由于ThreadA的写锁仍然没有释放,当ThreadD调用writeLock()获取写锁时,内部会调用acquireWrite()
acquireWrite()内部的逻辑和acquireRead()类似,也会进行两次自旋。第一次自旋会先尝试获取写锁,获取成功则直接返回,获取失败,则会将当前线程TheadD包装成WNode放入等待队列并移动队尾指针,内部属性模式为写模式,thread为ThreadD对象,status=0【初始化】。
当进入到第二次自旋,仍然会尝试获取写锁,如果获取不到,会修改其前驱节点状态为-1【等待】,并阻塞当前线程。
3.1.3 释放锁过程分析
1)ThreadA释放写锁
当要释放写锁时,需要调用unlockWrite(),其内部首先会判断,传入的票据与获取锁时得到的票据是否相同,不同的话,则抛出异常。如果相同先修改state,接着调用release(),唤醒等待队列中的队首节点【即头结点whead的后继节点】
在release()中,它会先将头结点whead的状态修改从-1变为0,代表要唤醒其后继节点,接着会判断头结点whead的后继节点是否为null或者其后继节点的状态是否为1【取消】。 如果不是,则直接调用unpark()唤醒队首节点,如果是的话,再从队尾开始查找距离头结点最近的状态<=0【WAITING或初始化】的节点。
当ThreadB被唤醒后,它会从cowait中唤醒栈中的所有线程,因为读锁是一把共享锁,允许多线程同时占有。
当所有的读锁都被唤醒后,头结点指针会后移,指向ThreadB这个WNode,并将原有的头结点移出等待队列。
此时ThreadC已经成为了孤立节点,最终会被GC。
2)ThreadB和ThreadC释放读锁
读锁释放需要调用unlockRead(),其内部先判断票据是否正确,接着会对读锁数量进行扣减,当读锁数量为0,会调用release()唤醒队首节点。
其内部同样会先将头结点状态从-1该为0,标识要唤醒后继节点
当ThreadD被唤醒获取到写锁后,头结点指针会后移指向ThreadD,并原有头部节点移出队列。
3.2 乐观读锁解析
乐观读锁只需要获取,不需要释放。在获取时,只要没有线程获取写锁,则可以获取到乐观读锁,同时将共享数据储存到局部变量中。同时在获取到乐观读锁后,并不会阻塞其他线程对共享数据进行修改。因为就会造成当使用共享数据时,出现数据不一致的问题。因此在使用乐观读锁时,要反复的对数据进行校验。