背景(注释):
一个基于容量并且带有三种模式的锁,用于控制读取/写入访问。StampedLock的状态由版本和模式组成。锁获取操作返回一个用于展示和访问锁状态的邮编(stamp)变量:这些方法的"try"版本通过返回0代表获取锁失败。锁释放以及其他相关方法需要使用邮编(stamps)变量作为参数,如果他们和当前锁状态不符则失败,这三种模式为:
- 写入:方法writeLock可能为了获取独占访问而阻塞当前线程,返回一个邮编变量,能够在unlockWrite方法中使用从而释放锁。限时和立即版本的tryWriteLock也提供了支持。当锁被写模式所占有,没有读或者乐观的读操作能够成功。
- 读取:方法readLock可能为了获取非独占访问而阻塞当前线程,返回一个邮编变量,能够在unlockRead方法中用于释放锁。限时和立即版本的tryReadLock也提供了支持。
- 乐观读取:是方法tryOptimisticRead返回一个非0邮编变量,仅在当前锁没有以写入模式被持有。方法validate返回true如果在获得邮编变量(stamp)之后没有被写模式持有。这种模式可以被看做一种弱版本的读锁,可以被一个写入者在任何时间打断。乐观读取模式仅用于短时间读取操作时经常能够降低竞争和提高吞吐量。当然,它的使用在本质上是脆弱的。乐观读取的区域应该只包括字段,并且在validation之后用局部变量持有它们从而在后续使用。乐观模式下读取的字段值很可能是非常不一致的,所以它应该只用于那些你熟悉如何展示数据,从而你可以不断检查一致性和调用方法validate。比如,当我们第一次读取对象或者数组引用,然后检查它的字段、元素以及方法时。
这个类支持在模式之间转化的方法。比如,方法tryConvertToWriteLock尝试“升级”模式,返回一个合理的写入邮编,假如(1)已经是写模式(2)读模式,但是没有更多的读取者(3)处于乐观模式并且这个锁可能被获取。这些方法的形式被设计成有助于降低代码膨胀,特别是基于重试的设计。
StampedLocks被设计成在线程安全组件发展过程中的一个内部支持工具。他们的使用依赖于所要保护的数据、对象以及方法的内部性质。他们不是重入的,所以获取锁之后的操作里面不应该包括未知的会再次获取锁的操作(虽然你把stamp传递给其他方法能够使用或者转化它)。读模式锁的使用必须确保相关的代码块是无副作用的。未验证的乐观读区域不能调用那些无法容忍潜在不一致的方法。stamp变量使用有限的形式,并且没有安全加密(一个正确的stamp能够被猜出来)。邮编变量可能(不会更久)会在一年的连续操作之后回溯。一个stamp没有使用或者验证,那么在很长一段时间之后可能就会验证失败。StampedLocks是序列化的,但是通过反序列化为初始的非锁定状态,所以在远程锁定中是不安全的。
StampedLock的调度策略没有给读提供优先,写也一样。所有"try“的方法尽最大努力,但是不需要遵循任何调度或者公平策略。从"try“中返回0无法说明关于状态state的任何信息。一个接下来的调用可能会成功。
因为通过协作的方式支持多种模式,所以这个类没有直接实现Lock或者ReadWriteLock方法。当然,一个StampedLock可以通过asReadLock,asWriteLock,asReadWriteLock方法来得到全部功能的子集。
使用场景:
接下来这个类中的例子维护了一个二维空间的点。这个样例代码说明了一些try/catch使用管理,虽然他们不需要在此使用,因为没有异常会在代码体中出现:
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}
算法(注释):
从概念上说,这个锁的主要状态包含一个序列数,当处于写模式时第8位为1,读模式时前7为为1-126(附加的readerOverflow用于当读者超过126时)。当然,具体的读取个数是被忽略的当我们验证”乐观"序列锁-读者-方式 邮编时。因为我们必须使用一个足够小的数字(7位)来处理读者,所以一个提供补充的值被使用,在我们的读者数量超过了使用的位数。我们通过使用最大的读者个数(RBITS)作为一个自旋保护机制来做在溢出值上做更新。
等待者使用了一个CLH锁的变化形式,CLH在AbstractQueuedSynchronizer 中使用,每一个节点作为一个读者或者写者来等待。读者的集合通过一个公共节点被聚集在一起,所以通过单个节点来附加到CLH队列上。凭借队列的结构,等待者节点不能使用序列数。我们知道每一个是大于他们的前驱。这样可以简化FIFO队列的主要调度策略,通过引入了阶段-公平锁。特别的,我们使用阶段-公平(phase-fair)规则:如果一个读取到来时返现有一个写入等待,这个读取会入队。(这个规则使得acquireRead增加了一些复杂性,否则使得这个锁非常不公平),方法释放不会(不能)通过自身来唤醒等待者。这个责任是通过主线程,但是可以通过其他线程来协助,比如在方法acquireRead和acquireWirte中。
这些规则适用于已经入队的线程。所有的tryLock形式的方法试着获取锁而不管其他的优先规则,所以可能导致队列中的线程没有获取成功(因为闯入的线程获取了锁)。随机的自旋使用(增加开销)能够降低上下文切换,当我们为了避免线程间持续的内存颠簸。我们限制了自旋操作仅用于队列的头元素。一个线程会自旋等待SPINS次(每次都有50%的机会递减自旋值)在阻塞前。如果线程觉醒后还是没有获得锁,并且仍然是第一个等待线程(其他线程闯入并且获取了锁),那么它便会增加自己的自旋次数从而降低被闯入的线程获得锁的可能。
序列检查(主要是validate方法)需要严格的内存排序,在读取操作上(“state”)。我们通过使用Unsafe.loadFence来强制读取操作和验证操作在一些情况下的内存排序问题。
当前的内存布局保证了状态变量state和队列指针一起(在同一个缓存行上)。这样使得读取频繁的负载下工作良好。在大多数其他情况,CLH队列的自然属性以及它的自适应自旋,能够通过扩展竞争位置的方式从而降低内存竞争,在将来可能会作出改善。
实现:
首先我们来看读取锁的实现:
public long readLock() {
long s = state, next; // bypass acquireRead on common uncontended case
return ((whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
next : acquireRead(false, 0L));
}
private long acquireRead(boolean interruptible, long deadline) { WNode node = null, p; for (int spins = -1;;) { WNode h; if ((h = whead) == (p = wtail)) { for (long m, s, ns;;) { if ((m = (s = state) & ABITS) < RFULL ? U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) : (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) return ns; else if (m >= WBIT) { if (spins > 0) { if (LockSupport.nextSecondarySeed() >= 0) --spins; } else { if (spins == 0) { WNode nh = whead, np = wtail; if ((nh == h && np == p) || (h = nh) != (p = np)) break; } spins = SPINS; } } } } if (p == null) { // initialize queue WNode hd = new WNode(WMODE, null); if (U.compareAndSwapObject(this, WHEAD, null, hd)) wtail = hd; } else if (node == null) node = new WNode(RMODE, p); else if (h == p || p.mode != RMODE) { if (node.prev != p) node.prev = p; else if (U.compareAndSwapObject(this, WTAIL, p, node)) { p.next = node; break; } } else if (!U.compareAndSwapObject(p, WCOWAIT, node.cowait = p.cowait, node)) node.cowait = null; else { for (;;) { WNode pp, c; Thread w; if ((h = whead) != null && (c = h.cowait) != null && U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) && (w = c.thread) != null) // help release U.unpark(w); if (h == (pp = p.prev) || h == p || pp == null) { long m, s, ns; do { if ((m = (s = state) & ABITS) < RFULL ? U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) : (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) return ns; } while (m < WBIT); } if (whead == h && p.prev == pp) { long time; if (pp == null || h == p || p.status > 0) { node = null; // throw away break; } if (deadline == 0L) time = 0L; else if ((time = deadline - System.nanoTime()) <= 0L) return cancelWaiter(node, p, false); Thread wt = Thread.currentThread(); U.putObject(wt, PARKBLOCKER, this); node.thread = wt; if ((h != pp || (state & ABITS) == WBIT) && whead == h && p.prev == pp) U.park(false, time); node.thread = null; U.putObject(wt, PARKBLOCKER, null); if (interruptible && Thread.interrupted()) return cancelWaiter(node, p, true); } } } } for (int spins = -1;;) { WNode h, np, pp; int ps; if ((h = whead) == p) { if (spins < 0) spins = HEAD_SPINS; else if (spins < MAX_HEAD_SPINS) spins <<= 1; for (int k = spins;;) { // spin at head long m, s, ns; if ((m = (s = state) & ABITS) < RFULL ? U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) : (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) { WNode c; Thread w; whead = node; node.prev = null; while ((c = node.cowait) != null) { if (U.compareAndSwapObject(node, WCOWAIT, c, c.cowait) && (w = c.thread) != null) U.unpark(w); } return ns; } else if (m >= WBIT && LockSupport.nextSecondarySeed() >= 0 && --k <= 0) break; } } else if (h != null) { WNode c; Thread w; while ((c = h.cowait) != null) { if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) && (w = c.thread) != null) U.unpark(w); } } if (whead == h) { if ((np = node.prev) != p) { if (np != null) (p = np).next = node; // stale } else if ((ps = p.status) == 0) U.compareAndSwapInt(p, WSTATUS, 0, WAITING); else if (ps == CANCELLED) { if ((pp = p.prev) != null) { node.prev = pp; pp.next = node; } } else { long time; if (deadline == 0L) time = 0L; else if ((time = deadline - System.nanoTime()) <= 0L) return cancelWaiter(node, node, false); Thread wt = Thread.currentThread(); U.putObject(wt, PARKBLOCKER, this); node.thread = wt; if (p.status < 0 && (p != h || (state & ABITS) == WBIT) && whead == h && node.prev == p) U.park(false, time); node.thread = null; U.putObject(wt, PARKBLOCKER, null); if (interruptible && Thread.interrupted()) return cancelWaiter(node, node, true); } } } }这里的readLock会首先尝试直接CAS改变state(在whead==wtail和(s & ABITS) < RFULL的情况下),成功的话直接返回stamp(next)。
竞争失败的情况下进入acquireRead的逻辑,下面详细分析acquireRead的逻辑(至今为止最长的代码块):
- 传入的两参数:interruptible和deadline,前者标识是否允许中断,后者标识超时限时(0代表不限时),然后进入循环。
- 首先取得whead和wtail两个值,假如这两个值不等说明已经又入队者了,那么获取读锁没希望了。
- 否则再进入内层循环,会尽力尝试通过前7bit上递增state来获取锁 ,这里采取的方式极具特色,采用了另一个readerOverflow了计数另外增加的读者数量,state的前七位记录到126之后就会稳定在这个值,偶尔会到127,但是超出126的部分最终到了readerOverflow,加入获取了锁就返回stamp。
- 假如m >= WBIT,也就是说m(state前8位)值大于或等于128,那么说明当前锁已经被写者独占,那么我们尝试自旋+随机的方式来探测状态,并且在当前队列和进入循环前一样(说明还没有其他入队者)或者当前队列中已经有了入队者的情况下内层循环跳出,接着肯定会入队。
- 首先根据尾节点为null的情况探测是否初始化队列,使用一个WMODE模式的节点初始化whead和wtail。
- 然后假如当前节点为空则构建当前节点,模式为RMODE,前驱节点为p即尾节点。
- 接着查看,假如当前队列为空即只有一个节点(whead=wtail)或者当前尾节点的模式不是RMODE,那么我们会尝试在尾节点后面添加该节点作为尾节点,然后跳出外层循环;假如当前队列不为空并且当前尾节点模式就是RMODE,那么我们会尝试下一步:添加该节点到尾节点的cowait链(实际上构成一个stack)中。
我们先来分析处于同一循环中的后一种情况:尾节点模式为RMODE:
- 通过CAS方法将该节点node添加至尾节点的cowait链中,node成为cowait中的顶元素,cowait构成了一个LIFO队列。
- 成功后进入另一个循环,首先尝试unpark头元素(whead)的cowait中的第一个元素,这只是一种辅助作用,因为头元素whead所伴随的那个线程(假如存在)必定是已经获得锁了,假如是读锁那么也必定会释放cowait链。
- 假如当前节点node所在的根节点p的前驱就是whead或者p已经是whead或者p的前驱为null,那么我们会根据state再次积极的尝试获取锁(当m < WBIT)。
- 否则我们探测当前队列是否稳定:whead == h && p.prev == pp,在稳定的情况下,假如发现p成为过head或者p已经被取消(status>0),我们尝试node=null,并且跳出当前循环,回到一开的循环里面去尝试获取锁(这样做的原因是被其他线程闯入夺取了锁,或者p已经被取消)。
- 接着我们判断是否为限时版本,以及限时版本所需时间。
- 然后park当前线程以及可能出现的中断情况下取消当前节点的cancelWaiter操作。
我们再来分析跳出循环的另一种情况:队列中无节点或者尾节点模式为WMODE:这样我们的节点必须直接拼接到尾节点后面。
- 这样p作为当前节点的前驱节点,假如正好是whead的话,那么会尝试自旋+随机的方式在积极得探测state,从而能够取得锁。并且在获得锁,重置whead和node.prev=null之后释放当前cowait链中的节点。最后返回stamp。
- 否则只需h不为null时尝试释放当前头节点的cowait链,作为一种协作的积极行动。
- 然后在whead==h即队列稳定时,首先会CAS操作当前节点前驱的status,从0变为WAITING从而指示后面有等待的节点。假如发现p的状态已经为取消了,则重新选择node的前驱。
- 前面的这些都处理完成之后,使用类似的park以及cancelWaiter操作。区别在于这里的p.status<0必须保证(因为等待状态WAITING是-1)。
我们接着来看写入锁的实现:
public long writeLock() {
long s, next; // bypass acquireWrite in fully unlocked case only
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
private long acquireWrite(boolean interruptible, long deadline) { WNode node = null, p; for (int spins = -1;;) { // spin while enqueuing long m, s, ns; if ((m = (s = state) & ABITS) == 0L) { if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT)) return ns; } else if (spins < 0) spins = (m == WBIT && wtail == whead) ? SPINS : 0; else if (spins > 0) { if (LockSupport.nextSecondarySeed() >= 0) --spins; } else if ((p = wtail) == null) { // initialize queue WNode hd = new WNode(WMODE, null); if (U.compareAndSwapObject(this, WHEAD, null, hd)) wtail = hd; } else if (node == null) node = new WNode(WMODE, p); else if (node.prev != p) node.prev = p; else if (U.compareAndSwapObject(this, WTAIL, p, node)) { p.next = node; break; } } for (int spins = -1;;) { WNode h, np, pp; int ps; if ((h = whead) == p) { if (spins < 0) spins = HEAD_SPINS; else if (spins < MAX_HEAD_SPINS) spins <<= 1; for (int k = spins;;) { // spin at head long s, ns; if (((s = state) & ABITS) == 0L) { if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT)) { whead = node; node.prev = null; return ns; } } else if (LockSupport.nextSecondarySeed() >= 0 && --k <= 0) break; } } else if (h != null) { // help release stale waiters WNode c; Thread w; while ((c = h.cowait) != null) { if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) && (w = c.thread) != null) U.unpark(w); } } if (whead == h) { if ((np = node.prev) != p) { if (np != null) (p = np).next = node; // stale } else if ((ps = p.status) == 0) U.compareAndSwapInt(p, WSTATUS, 0, WAITING); else if (ps == CANCELLED) { if ((pp = p.prev) != null) { node.prev = pp; pp.next = node; } } else { long time; // 0 argument to park means no timeout if (deadline == 0L) time = 0L; else if ((time = deadline - System.nanoTime()) <= 0L) return cancelWaiter(node, node, false); Thread wt = Thread.currentThread(); U.putObject(wt, PARKBLOCKER, this); node.thread = wt; if (p.status < 0 && (p != h || (state & ABITS) != 0L) && whead == h && node.prev == p) U.park(false, time); // emulate LockSupport.park node.thread = null; U.putObject(wt, PARKBLOCKER, null); if (interruptible && Thread.interrupted()) return cancelWaiter(node, node, true); } } } }
可以看到获取写入锁时不需要顾及当前队列是否有元素,可以直接“闯入”从而获得锁。
- 这里同样是以自旋+随机的方式来探测锁,只不过探测值变为了m = (s = state) & ABITS) == 0L。
- 之后的过程同样是初始化队列,构造节点,设置前驱,拼接节点(对于写入锁只有在队列尾部拼接一种选择)。
- 这之后的操作事实上和读取锁中的根节点的操作类似,都是会先积极尝试自旋在whead节点上,然后清理头结点,以及前驱的被取消节点,最后才park住。
接着我们来看会在限时和中断的取消操作:
private long cancelWaiter(WNode node, WNode group, boolean interrupted) {
if (node != null && group != null) {
Thread w;
node.status = CANCELLED;
// unsplice cancelled nodes from group
for (WNode p = group, q; (q = p.cowait) != null;) {
if (q.status == CANCELLED) {
U.compareAndSwapObject(p, WCOWAIT, q, q.cowait);
p = group; // restart
}
else
p = q;
}
if (group == node) {
for (WNode r = group.cowait; r != null; r = r.cowait) {
if ((w = r.thread) != null)
U.unpark(w); // wake up uncancelled co-waiters
}
for (WNode pred = node.prev; pred != null; ) { // unsplice
WNode succ, pp; // find valid successor
while ((succ = node.next) == null ||
succ.status == CANCELLED) {
WNode q = null; // find successor the slow way
for (WNode t = wtail; t != null && t != node; t = t.prev)
if (t.status != CANCELLED)
q = t; // don't link if succ cancelled
if (succ == q || // ensure accurate successor
U.compareAndSwapObject(node, WNEXT,
succ, succ = q)) {
if (succ == null && node == wtail)
U.compareAndSwapObject(this, WTAIL, node, pred);
break;
}
}
if (pred.next == node) // unsplice pred link
U.compareAndSwapObject(pred, WNEXT, node, succ);
if (succ != null && (w = succ.thread) != null) {
succ.thread = null;
U.unpark(w); // wake up succ to observe new pred
}
if (pred.status != CANCELLED || (pp = pred.prev) == null)
break;
node.prev = pp; // repeat if new pred wrong/cancelled
U.compareAndSwapObject(pp, WNEXT, pred, succ);
pred = pp;
}
}
}
WNode h; // Possibly release first waiter
while ((h = whead) != null) {
long s; WNode q; // similar to release() but check eligibility
if ((q = h.next) == null || q.status == CANCELLED) {
for (WNode t = wtail; t != null && t != h; t = t.prev)
if (t.status <= 0)
q = t;
}
if (h == whead) {
if (q != null && h.status == 0 &&
((s = state) & ABITS) != WBIT && // waiter is eligible
(s == 0L || q.mode == RMODE))
release(h);
break;
}
}
return (interrupted || Thread.interrupted()) ? INTERRUPTED : 0L;
}
这里三个参数, node:需要删除的节点、group:可能的聚合节点、interrupted:是否被中断。
详情如下:
- 首先设置node的状态为CANCELLED,可以向其他线程传递这个节点是删除了的信息。
- 然后再聚合节点gruop上清理所有状态为CANCELLED的节点(即删除节点)
- 接下来假如当期node节点本身就是聚合节点,那么首先唤醒cowait链中的所有节点(读者),寻找到node后面的第一个非CANCELLED节点,直接拼接到pred上(从而删除当前节点),然后再检查前驱节点状态,假如为CANCELLED则需也需要重置前驱节点。
- 最后,在队列中不为空,并且头结点的状态为0即队列中的节点还未设置WAITING信号&当前没有持有写入锁模式&(当前没有锁或者只有乐观锁 | 队列中第一个等待者为读模式),那么就从队列头唤醒一次。
存在的问题:
- 不可中断的锁获取操作中没有保存中断状态,造成CPU占有。
- 由于新型锁使用了新的数据结构,所以当读锁的第一个节点被中断取消时,它的cowait链接上的节点会全部重新拼接到队列上,这样会造成一定程序的"优先级反转"。具体可用如下方法再现:
为了更好得看出这个问题,将源码中造成随机性的LockSupport.nextSecondarySeed取消掉。
然后协调取锁的限时,并使用类似如下代码:
public static void main(String[] args) throws InterruptedException{
// TODO Auto-generated method stub
StampedLock lock = new StampedLock();
new Thread(new WriteThread(lock)).start();
// new Thread(new ReadThreadTry(lock)).start();
Thread.sleep(200);
new Thread(new ReadThreadTry(lock)).start();
Thread.sleep(100);
// new Thread(new ReadThreadTry(lock)).start();
for( int i = 0; i < 6; ++i)
new Thread(new ReadThread(lock)).start();
Thread.sleep(300);
for( int i = 0; i < 6; ++i)
new Thread(new WriteThreadLast(lock)).start();
}
public static void main(String[] args) throws InterruptedException{ // TODO Auto-generated method stub StampedLock lock = new StampedLock(); new Thread(new WriteThread(lock)).start();// new Thread(new ReadThreadTry(lock)).start(); Thread.sleep(200); new Thread(new ReadThread(lock)).start(); Thread.sleep(100); new Thread(new ReadThreadTry(lock)).start(); for( int i = 0; i < 5; ++i) new Thread(new ReadThread(lock)).start(); Thread.sleep(300); for( int i = 0; i < 6; ++i) new Thread(new WriteThreadLast(lock)).start(); }
private static class WriteThread implements Runnable{
private StampedLock lock;
public WriteThread(StampedLock lock){
this.lock = lock;
}
public void run(){
long writeLong = lock.writeLock();
System.out.println(Thread.currentThread().getName() + " WRITE");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.unlockWrite(writeLong);
}
}
private static class ReadThreadTry implements Runnable{
private StampedLock lock;
public ReadThreadTry(StampedLock lock){
this.lock = lock;
}
public void run(){
long readLong = 0;
try {
readLong = lock.tryReadLock(600, TimeUnit.MILLISECONDS);
if(readLong > 0)
System.out.println(Thread.currentThread().getName() + " READ LOCK");
else
System.out.println(Thread.currentThread().getName() + " READ noLOCK");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
if(readLong>0)
lock.unlockRead(readLong);
}
}
}
private static class ReadThread implements Runnable{
private StampedLock lock;
public ReadThread(StampedLock lock){
this.lock = lock;
}
public void run(){
long readLong = lock.readLock();
System.out.println(Thread.currentThread().getName() + " READ");
lock.unlockRead(readLong);
}
}
private static class WriteThreadLast implements Runnable{
private StampedLock lock;
public WriteThreadLast(StampedLock lock){
this.lock = lock;
}
public void run(){
long writeLong = lock.writeLock();
System.out.println(Thread.currentThread().getName() + " WRITE");
lock.unlockWrite(writeLong);
}
}