详解-StampedLock原理-并发编程(Java)

1 简介

该类自JDK8加入,进一步优化了读性能,它的特点是使用读锁、写锁时都必须配合戳使用。

常见3种模式:读、写、乐观读。

  • 加解读锁典型代码:
long stamp 	= lock.readLock();
lock.unlockRead(stamp);
  • 加解写锁典型代码:
long stamp 	= lock.writeLock();
lock.unlockWrite(stamp);
  • 乐观读:StampedLock支持tryOptimisticRead()(乐观读),获取戳后需要做一次戳校验,如果校验通过,表示这期间没有写操作,数据安全可以使用;否则需要重新获取读锁,保证数据安全。

    long stamp = lock.tryOptimisticRead();
    if(!lock.validate(stamp)) {
    	// 锁升级
    }
    

2 简单应用

通过构建一个数据容器类,提供读取数据和写入数据的方法。代码如下:

@Slf4j(topic = "c.TestStampedLock")
public class TestStampedLock {
    public static void main(String[] args) {
        DataContainerStamped containerStamped = new DataContainerStamped(1);
        new Thread(() -> {
            containerStamped.read(1);
        }, "t1").start();

        new Thread(() -> {
            containerStamped.write(0);
        }, "t2").start();
    }
}

@Slf4j(topic = "c.DataContainerStamped")
class DataContainerStamped {
    private int data;
    private final StampedLock lock = new StampedLock();

    public DataContainerStamped(int data) {
        this.data = data;
    }

    public int read(int readTime) {
        long stamp = lock.tryOptimisticRead();
        log.debug("optimistic read lock {}", stamp);
        if (lock.validate(stamp)) {
            log.debug("read finish {}", stamp);
            return this.data;
        }
        log.debug("updating reading lock {}", stamp);
        try {
            stamp = lock.readLock();
            log.debug("read lock {}", stamp);
            TimeUnit.SECONDS.sleep(readTime);
            log.debug("read finish {}", stamp);
            return this.data;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return 0;
        } finally {
            log.debug("read unlock {}", stamp);
            lock.unlockRead(stamp);
        }

    }

    public void write(int newData) {
        long stamp = lock.writeLock();
        log.debug("write lock {}", stamp);
        try {
            TimeUnit.SECONDS.sleep(5);
            this.data = newData;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.debug("write unlock {}", stamp);
            lock.unlockWrite(stamp);
        }
    }
}

3 源码分析

StampedLock没有实现Lock或者ReadWriteLock接口,也没有内置AQS锁。

下面对一些属性做下简单说明:

private static final int NCPU = Runtime.getRuntime().availableProcessors();

/** Maximum number of retries before enqueuing on acquisition */
private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;

/** Maximum number of retries before blocking at head on acquisition */
private static final int HEAD_SPINS = (NCPU > 1) ? 1 << 10 : 0;

/** Maximum number of retries before re-blocking */
private static final int MAX_HEAD_SPINS = (NCPU > 1) ? 1 << 16 : 0;

/** The period for yielding when waiting for overflow spinlock */
private static final int OVERFLOW_YIELD_RATE = 7; // must be power 2 - 1

/** The number of bits to use for reader count before overflowing */
private static final int LG_READERS = 7;

// Values for lock state and stamp operations
private static final long RUNIT = 1L;
private static final long WBIT  = 1L << LG_READERS;
private static final long RBITS = WBIT - 1L;
private static final long RFULL = RBITS - 1L;
private static final long ABITS = RBITS | WBIT;
private static final long SBITS = ~RBITS; // note overlap with ABITS

// Initial value for lock state; avoid failure value zero
private static final long ORIGIN = WBIT << 1;

// 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;

结点模式:

  • RMODE:0,读结点
  • WMODE:1,写结点

结点状态:

  • 默认:0
  • WAITING:-1,阻塞状态
  • CANCELLED:1,取消状态

锁状态(计数):

  • LG_READERS:7,低7位用于读计数
  • RUNIT:1L,读计数单位
  • WBIT:1L << LG_READERS,即128,写计数单位
  • RBITS:WBIT - 1L,即127,读计数掩码(上限)
  • RFULL:RBITS - 1,即126,该类指定的读计数的最大容量
  • ABITS:RBITS | WBIT,即255
  • SBITS:~RBITS,低7位为0其他位为1的long型整数。

3.1 乐观读

3.1.1 tryOptimisticRead()

乐观读源代码如下:

public long tryOptimisticRead() {
	long s;
	return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
public StampedLock() {
	state = ORIGIN;
}
private static final long ORIGIN = WBIT << 1;
private static final long WBIT  = 1L << LG_READERS;
private static final int LG_READERS = 7;
  • state默认值:256(10),赋值给s
  • s&WBIT 结果为0,返回s&SBITS结果是s自己即256,即乐观读初始返回戳为256

tryOptimisticRead()要与validate()配合使用,即验戳。

3.1.2 validate()

验戳源代码如下:

public boolean validate(long stamp) {
	U.loadFence();
	return (stamp & SBITS) == (state & SBITS);
}
  • 戳stamp要与state除低7位之外全部相同,validate()才会通过,即返回true

3.2 加解写锁

3.2.1 writeLock()加写锁

独占式获取锁,必要时阻塞直到锁可用。返回一个戳,用于释放锁。源代码如下:

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));
}

执行流程如下:

  • 第一步判断((s = state) & ABITS) == 0L
    • 只有未加任何锁(或完全解锁)情况下,才相等
  • 通过cas方式加写锁,即改变锁state(计数)加1个写单位
    • cas加锁成功返回当前state
    • 失败执行acquireWrite()
3.2.2 acquireWrite()

获取写锁,源代码如下:

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);
			}
		}
	}
}

执行流程如下:

  • 有2个for循环,第一个for循环用于在添加锁阻塞队列结点之前各种操作
    • 第一步先判断m锁计数如果为0 ,表示没加锁
      • 执行cas加锁,返回新的锁状态
    • 第二步否则判断循环计数spins < 0,spins初始值-1
      • 如果此时加写锁且锁队列不为空,spins赋值为 2 6 2^6 26,否则赋值为0
    • 第三步判断spins > 0
      • 一旦开始执行这里,那么循环执行 2 6 2^6 26次,spins才会减到0
    • 第四步上述都不满足,判断队列是否为空
      • 执行队列初始化,新建结点,whead,wtail指向该结点
    • 第五部上述都不满足,判断node是否为空(node结点为当前线程阻塞准备的结点)
      • 新建结点,链入队列
    • 第六步确保结点前驱链接正常
    • 第七步确保前驱结点的后继链接正常,且break循环
  • 第一个循环只有2个出口,见加粗部分
  • 第二个for循环,执行当前线程阻塞和阻塞前的相关操作
    • 判断如果(h = whead) == p即之前链接的node结点为除哨兵结点之外的第一个结点
      • 执行和第一个for循环前面几步相似的操作
    • 否则判断h不为空,清理废弃(无用)的结点
    • 判断whead==h即这之间没有结点被清理或者唤醒
      • 判断如果node结点的前驱如果不等于p清理
      • 判断如果p节点的状态==0 ,默认是0
        • cas把p结点的状态设置为WAITING,-1
      • 判断如果p结点的状态为CANCELLED
        • 直接断开链接,等待GC
      • 上述都不符合,执行阻塞操作
        • 获取当前线程,把node结点的thead标志置为当前线程
        • 执行UNSAFE.park操作

注意事项:

  • 阻塞队列结点状态处最后加入的结点状态为0外,其他结点状态为WAITING,-1
    • CANCELLED除外
  • 在添加第一个阻塞结点之前会多次for循环,再次获取锁
    • 能不阻塞就不阻塞
  • 唤醒阻塞线程之后,从阻塞的地方继续执行
3.2.3 unlockWrite()写锁解锁

源代码如下:

public void unlockWrite(long stamp) {
	WNode h;
	if (state != stamp || (stamp & WBIT) == 0L)
		throw new IllegalMonitorStateException();
	state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
	if ((h = whead) != null && h.status != 0)
		release(h);
}

执行流程:

  • 如果戳和锁状态不相等或者写锁计数==0 抛异常
  • release()唤醒结点
3.2.4 release()

源代码如下:

private void release(WNode h) {
	if (h != null) {
		WNode q; Thread w;
		U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
		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 (q != null && (w = q.thread) != null)
			U.unpark(w);
	}
}

逻辑相对简单,如果队列不为空,唤醒阻塞结点上的线程。

3.3 加解读锁

3.3.1 readLock()加读锁

源代码如下:

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));
}

执行流程:

  • 判断阻塞队列为空,且读锁计数小于读锁计数容量,cas锁状态置为+1

    • 返回此时锁状态

    • 有一个判断条件失败执行acquireRead()

3.3.2 acquireRead()

源代码如下:

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);
			}
		}
	}
}

执行流程如下:和acquireWrite()有很多相似之处,这里主要讲解不同

  • 第一个for循环
    • 第一步判断阻塞队列是否为空
      • 添加第一个阻塞结点前执行多次尝试获取锁
    • 第二步判断p(wtail)为空,初始化阻塞队列
    • 第三步判断node(新阻塞结点)为空,初始化node
    • 第四步判断如果p==h(阻塞队列为空)或者p.mode != RMODE尾结点为写结点
      • cas方式把node结点置为尾结点,结束循环
    • 第五步上述条件不符合,cas方式把node结点链接到尾结点的WCOWAIT
      • cas方式失败,node.cowait = null;
    • 上述都不符合执行阻塞操作
  • 第二for同acquireWrite()

注:

  • 读读加锁不互斥
  • 如果阻塞队列要阻塞读线程尾结点为读结点,那么该结点会链接到该尾结点的WCOWAIT
  • WCOWAIT为链接读结点的单链表结构,一个释放,全部都会释放。

4 与ReentrantReadWriteLock比较

  • StampedLock配合戳使用,不支持锁重入,公平与非公平,不支持条件变量;进一步优化读性能,使用乐观读。

  • ReentrantReadWriteLock底层基于AQS同步锁实现,支持锁重入,有公平锁与费公平实现。写锁支持条件变量;

5 后记

如有问题,欢迎交流讨论。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaog2zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值