原文链接:https://blog.csdn.net/sunhaoning/article/details/68924625
StamppedLock是Java 8中引入的一种新的锁机制。读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发。但是,读和写之间依然是冲突的。读锁会完全阻塞写锁,它使用的依然是悲观锁的策略,如果有大量的读线程,它也有可能引起写线程的“饥饿”。
而StampedLock提供了一种乐观的读策略。这种乐观策略的锁非常类似无锁的操作,使得乐观锁完全不会阻塞写线程。

1)StampedLock使用示例

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

}
 
上述代码出自JDK的官方文档。它定义了一个Point类,内部有两个元素x和y,表示点的坐标。第3行定义了StampedLock锁。第15行定义的distanceFromOrigin()方法是一个只读方法,它只会读取Point的x和y坐标。在读取时,首先使用了StampedLock.tryOptimisticRead()方法。这个方法表示试图尝试一次乐观读。它会返回一个类似于时间的邮戳整数stamp。这个stamp就可以作为这一次锁获取的凭证。
接着,在第17行,读取x和y的值。当然,这时并不确定这个x和y是否是一致的(在读取x的时候,可能其他线程改写了y的值,使得currentX和currentY处于不一致的状态)。因此,我们必须在18行,使用validate()方法,判断这个stamp是否在读过程发生期间被修改过。如果stamp没有被修改过,则认为这次读取的过程中,可能被其他线程改写了数据,因此,有可能出现了脏读。如果出现这种情况,我们可以像处理CAS操作那样在一个死循环中一直使用乐观读,知道成功为止。
也可以升级锁的级别。在本例中,我们升级乐观锁的级别,将乐观锁变为悲观锁。在第19行,当判断乐观读失败后,使用readLock()获得悲观的读锁,并进一步读取数据。如果当前对象正在被修改,则读锁的申请可能导致线程挂起。
写入的情况可以参考第5行定义的move()函数。使用writeLock()函数可以申请写锁。这里的含义和读写锁是类似的。
在退出临界区时,不要忘记释放写锁(第11行)或者读锁(第24行)。

2)StampedLock的小陷阱

StampedLock内部实现时,使用类似于CAS操作的死循环反复尝试的策略。在它挂起线程时,使用的是Unsafe.park()函数,而park()函数在遇到线程中断时,会直接返回(不同于Thread.sleep(),它不会抛出异常)。而在StampedLock的死循环逻辑中,没有处理有关中断的逻辑。因此,这就会导致阻塞在park()上的线程被中断后,会再次进入循环。而当退出条件得不到满足时,就会发生疯狂占用CPU的情况。下面演示了这个问题:
public class StampedLockCUPDemo {

static Thread[] holdCpuThreads = new Thread[3];

static final StampedLock lock = new StampedLock();

public static void main(String[] args) throws InterruptedException {

new Thread() {

public void run(){

long readLong = lock.writeLock();

LockSupport.parkNanos(6100000000L);

lock.unlockWrite(readLong);

}

}.start();

Thread.sleep(100);

for( int i = 0; i < 3; ++i) {

holdCpuThreads [i] = new Thread(new HoldCPUReadThread());

holdCpuThreads [i].start();

}

Thread.sleep(10000);

for(int i=0; i<3; i++) {

holdCpuThreads [i].interrupt();

}

}

private static class HoldCPUReadThread implements Runnable {

public void run() {

long lockr = lock.readLock();

System.out.println(Thread.currentThread().getName() + " get read lock");

lock.unlockRead(lockr);

}

}

}
 
在上述代码中,首先开启线程占用写锁(第7行),为了演示效果,这里使用写线程不释放锁而一直等待。接着,开启3个读线程,让它们请求读锁。此时,由于写锁的存在,所有读线程都会被最终挂起。
读线程因为park()的操作进入了等待状态,这种情况是正常的。
而在10秒钟以后(代码在17行执行了10秒等待),系统中断了这3个读线程,之后,就会发现,CPU占用率极有可能会飙升。这是因为中断导致park()函数返回,使线程再次进入运行状态。
此时,这个线程的状态是RUNNABLE,这是我们不愿意看到的,它会一直存在并耗尽CPU资源,直到自己抢占到了锁。
 

3)有关StampedLock的实现思想

StampedLock的内部实现是基于CLH锁的。CLH锁是一种自旋锁,它保证没有饥饿发生,并且可以保证FIFO的服务顺序。
CLH锁的基本思想如下:锁维护一个等待线程队列,所有申请锁,但是没有成功的线程都记录在这个队列中。每一个节点(一个节点代表一个线程),保存一个标记位(locked),用于判断当前线程是否已经释放锁。
当一个线程试图获得锁时,取得当前等待队列的尾部节点作为其前序节点,并使用类似如下代码判断前序节点是否已经成功释放:
while(pred.locked) {
}
只要前序节点(pred)没有释放锁,则表示当前线程还不能继续执行,因此会自旋等待(很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。)。
反之,如果前序线程已经释放锁,则当前线程可以继续执行。
释放锁时,也遵循这个逻辑,线程会将自身节点的locked位置标记为false,那么后续等待的线程就能继续执行了。
在StampedLock内部,为维护一个等待链表队列:

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;
上述代码中,WNode为链表的基本元素,每一个WNode表示一个等待线程。字段whead和wtail分别指向等待链表的头部和尾部。
另外一个重要的字段为state:
private transient volatile long state;
字段state表示当前锁的状态。它是一个long型,有64位,其中,倒数第8位表示写锁状态,如果该位为1,表示当前由写锁占用。
对于一次乐观读的操作,它会执行如下操作:
public long tryOptimisticRead() {

long s;

return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;

}
一次成功的乐观读必须保证当前锁没有写锁占用。其中WBIT用来获取写锁状态位,值为0x80。如果成功,则返回当前state的值(末尾7位清零,末尾7位表示当前正在读取的线程数量)。
如果在乐观读后,有线程申请了写锁,那么state的状态就会改变:
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));

}
上述代码中第4行,设置写锁位为1(通过加上WBIT(0x80))。这样,就会改变state的取值。那么在乐观锁确认(validate)时,就会发现这个改动,而导致乐观锁失败。
public boolean validate(long stamp) {

// See above about current use of getLongVolatile here

return (stamp & SBITS) == (U.getLongVolatile(this, STATE) & SBITS);

}
 
上述validate()函数比较当前stamp和发生乐观锁时取得的stamp,如果不一致,则宣告乐观锁失败。
乐观锁失败后,则可以提升锁级别,使用悲观读锁。
public long readLock() {

long s, next; // bypass acquireRead on fully unlocked case only

return ((((s = state) & ABITS) == 0L &&

U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?

next : acquireRead(false, 0L));

}
悲观读会尝试设置state状态(第4行),它会将state加1(前提是读线程数量没有溢出,对于读线程数量溢出的情况,会使用辅助的readerOverflow进行统计),用于统计读线程的数量。如果失败,则进入acquireRead()二次尝试锁获取。
在acquireRead()中,线程会在不同条件下进行若干次自旋,试图通过CAS操作获得锁。如果自旋宣告失败,则会启用CLH队列,将自己加到队列中。之后再进行自旋,如果发现自己成功获得了读锁,则会进一步把自己cowait队列中的读线程全部激活(使用Usafe.unpark()方法)。如果最终依然无法成功获得读锁,则会使用Unsafe.park()方法挂起当前线程。
方法acquireWrite()和acquireRead()也非常类似,也是通过自旋尝试、加入等待队列、直至最终Unsafe.park()挂起线程的逻辑进行的。释放锁时与加锁动作相反,以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);

}
上述代码第5行,将写标记位清零,如果state发生溢出,则退回到初始值。
接着,如果等待队列不为空,则从等待队列中激活一个线程(绝大部分情况下是第1个等待线程)继续执行(第7行)。

StampedLock原理的更多相关文章

  1. Java 8 StampedLock解决同步问题

    Java 8新特性探究(十)StampedLock将是解决同步问题的新宠 JDK8中StampedLock原理探究 深入理解StampedLock及其实现原理 JDK1.8 StampedLock源码 ...

  2. Java并发编程原理与实战三十九:JDK8新增锁StampedLock详解

    1.StampedLock是做什么的? ----->它是ReentrantReadWriteLock 的增强版,是为了解决ReentrantReadWriteLock的一些不足.   2.Ree ...

  3. 同步中的四种锁synchronized、ReentrantLock、ReadWriteLock、StampedLock

    目录 1.synchronized同步锁 2.ReentrantLock重入锁 3.ReadWriteLock读写锁 4.StampedLock戳锁(目前没找到合适的名字,先这么叫吧...) 5.总结 ...

  4. Java并发编程笔记之StampedLock锁源码探究

    StampedLock是JUC并发包里面JDK1.8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数的时候,会返回一个long 型的变量,该变量被称为戳记(stamp),这个戳记 ...

  5. Lock、synchronized和ReadWriteLock,StampedLock戳锁的区别和联系以及Condition

    https://www.cnblogs.com/RunForLove/p/5543545.html 先来看一段代码,实现如下打印效果: 1 2 A 3 4 B 5 6 C 7 8 D 9 10 E 1 ...

  6. 同步中的四种锁synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock

    为了更好的支持并发程序,JDK内部提供了多种锁.本文总结4种锁. 1.synchronized同步锁 使用: synchronized本质上就2种锁: 1.锁同步代码块 2.锁方法 可用object. ...

  7. Java并发(8)- 读写锁中的性能之王:StampedLock

    在上一篇<你真的懂ReentrantReadWriteLock吗?>中我给大家留了一个引子,一个更高效同时可以避免写饥饿的读写锁---StampedLock.StampedLock实现了不 ...

  8. Java 并发编程-不懂原理多吃亏(送书福利)

    作者 | 加多 关注阿里巴巴云原生公众号,后台回复关键字"并发",即可参与送书抽奖!** 导读:并发编程与 Java 中其他知识点相比较而言学习门槛较高,从而导致很多人望而却步.但 ...

  9. AQS底层原理分析

    J.U.C 简介 Java.util.concurrent 是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件.比如线程池.阻塞队列.计时器.同步器.并发集合等等.并发包的作者是大 ...

随机推荐

  1. Lib之过?Java反序列化漏洞通用利用分析

    转http://blog.chaitin.com/ 1 背景 2 Java反序列化漏洞简介 3 利用Apache Commons Collections实现远程代码执行 4 漏洞利用实例 4.1 利用 ...

  2. Servlet3.0之八:基于Servlet3.0的文件上传@MultipartConfig

    在Servlet2.5中,我们要实现文件上传功能时,一般都需要借助第三方开源组件,例如Apache的commons-fileupload组件,在Servlet3.0中提供了对文件上传的原生支持,我们不 ...

  3. eclipse插件安装(个人版)

    1.Eclipse 安装反编译插件jadclipse http://jingyan.baidu.com/article/3f16e003c857082590c1036f.html 2.MyEclips ...

  4. Git学习笔记(三)远程库(GitHub)协同开发,fork和忽略特殊文件

    远程库 远程库,通俗的讲就是不再本地的git仓库!他的工作方式和我们本地的一样,但是要使用他就需要先建立连接! 远程库有两种,一个是自己搭建的git服务器:另一种就是使用GitHub,这个网站就是提供 ...

  5. Celery-4.1 用户指南: Calling Tasks(调用任务)

    基础 本文档描述 Celery 中任务实例和 Canvas 使用的统一 “Calling API”. API 中定义了一个执行选项的标准集,以及三个方法: - apply_async(args[, k ...

  6. 2015.5.11 string与byte[]相互转换

    string类型转成byte[]: byte[] byteArray = System.Text.Encoding.Default.GetBytes ( str ); 反过来,byte[]转成stri ...

  7. Anaconda 安装教程(Win10环境) Tensorflow安装

    序 Python易用,但用好却不易,其中比较头疼的就是包管理和Python不同版本的问题,特别是当你使用Windows的时候.为了解决这些问题,有不少发行版的Python,比如WinPython.An ...

  8. PHP同时连接多个数据库

    PHP同时连接多个mysql数据库的具体实现 方法一: <?php $conn1 = mysql_connect("127.0.0.1", "root", ...

  9. Python 网络爬虫 008 (编程) 通过ID索引号遍历目标网页里链接的所有网页

    通过 ID索引号 遍历目标网页里链接的所有网页 使用的系统:Windows 10 64位 Python 语言版本:Python 2.7.10 V 使用的编程 Python 的集成开发环境:PyChar ...

  10. Luogu 2831 [NOIP2016] 愤怒的小鸟

    第一眼看成爆搜的状压dp,膜Chester大神犇. 考虑到三个不在同一直线上的点可以确定一条抛物线,而固定点$(0, 0)$和不在同一直线上这两个条件是题目中给定的,所以我们只要枚举两个点然后暴力算抛 ...