ReentrantLock锁-CAS与阻塞
ReentrantLock锁
ReentrantLock通过原子操作和阻塞实现锁原理,一般使用lock获取锁,unlock释放锁
lock的时候可能被其他线程获得所,那么此线程会阻塞自己,关键原理底层用到Unsafe类的API: CAS和park
使用方式
lock unlock对应
lock
拿到锁,开始执行代码逻辑
unlock
执行完代码后,释放锁,让其他线程去获取
//创建锁对象
ReentrantLock lock = new ReentrantLock();
lock.lock(); //获取锁(锁定)
// 中间执行代码,保证同一时间只有一个线程能运行此处的代码
lock.unlock(); //锁释放
示例
为了体现锁的作用,这里sleep睡眠0.1秒,增加哪个线程获取锁的随机性
因为线程唤醒后,会开始尝试获取锁,多个线程下竞争一把锁是随机的
package javabasis.threads;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest implements Runnable {
public static ReentrantLock lock = new ReentrantLock();//锁
private int thold;
public LockTest(int h) {
this.thold = h;
}
public static void main(String[] args) {
for (int i = 10; i < 15; i++) {
new Thread(new LockTest(i),"name-" + i).start();
}
}
@Override
public void run() {
try {
Thread.sleep(100);
lock.lock(); //获取锁
System.out.println("lock threadName:" + Thread.currentThread().getName());
{
System.out.print(" writeStart ");
for (int i = 0; i < 15; i++) {
Thread.sleep(100);
System.out.print(thold+",");
}
System.out.println(" writeEnd");
}
System.out.println("unlock threadName:" + Thread.currentThread().getName() + "\r\n");
lock.unlock(); //锁释放
} catch (InterruptedException e) {
}
}
}
运行main方法输出结果:
lock threadName:name-10
writeStart 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, writeEnd
unlock threadName:name-10
lock threadName:name-14
writeStart 14,14,14,14,14,14,14,14,14,14,14,14,14,14,14, writeEnd
unlock threadName:name-14
lock threadName:name-13
writeStart 13,13,13,13,13,13,13,13,13,13,13,13,13,13,13, writeEnd
unlock threadName:name-13
lock threadName:name-11
writeStart 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11, writeEnd
unlock threadName:name-11
lock threadName:name-12
writeStart 12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, writeEnd
unlock threadName:name-12
这体现在多线程情况下,锁能做到让线程之间有序运行,
如果没有锁,情况可能是 12,13,13,10,10,10,12,没有锁其他线程可能插队执行System.out.print
原理
ReentrantLock主要用到unsafe的CAS和park两个功能实现锁(CAS + park )
Unsafe
sun.misc.Unsafe是关键类,提供大量偏底层的API 包括CAS park
sun.misc.Unsafe 此类在openjdk中可以查看
CAS 原子操作
compare and swapz(CAS)比较并交换,是原子性操作,
原理:当修改一个(内存中的)变量o的值N的时候,首先有个期望值expected,和一个更新值x,先比较N是否等于expected,等于,那么更新内存中的值为x值,否则不更新。
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
这里offset据了解,是对象的成员变量在内存中的偏移地址,
即底层一个对象object存放在内存中,读取的地址是0x2110,此对象的一个成员变量state的值也在内存中,但内存地址肯定不是0x2110
java中的CAS使用
java.util.concurrent.locks.AbstractQueuedSynchronizer 类
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
在Java中,这个操作如果更新成功,返回true,失败返回false,通过这个机制,可以定义锁(乐观锁)。
如三个线程A,B,C,在目标值为0的情况下,同时执行compareAndSetState(0,1) 去修改它
期望值是0,更新值是1,因为是原子操作,在第一个线程操作成功之后目标值变为1,返回true
所以另外两个线程就因为期望值为0不等于1,返回false。
我们可以理解为,返回true的线程拿到了锁。
最终调用的Java类是sun.misc.Unsafe
park 阻塞
Java中可以通过unsafe.park()去阻塞(停止)一个线程,也可以通过unsafe.unpark()让一个阻塞线程恢复继续执行
unsafe.park()
阻塞(停止)当前线程
public native void park(boolean isAbsolute, long time);
根据debug测试,此方法能停止线程自己,最后通过其他线程唤醒
unsafe.unpark()
取消阻塞(唤醒)线程
public native void unpark(Object thread);
根据debug测试,此方法可以唤醒其他被park调用阻塞的线程
park与interrupt的区别
interrupt是Thread类的的API,park是Unsafe类的API,两者是有区别的。
测试了解,Thread.currentThread().interrupt(),线程会继续运行,而Unsafe.park(Thread.currentThread())就是直接阻塞线程,不继续运行代码。
获取锁
线程cas操作失败,可以park阻塞自己,让其他拥有锁的线程在unlock的时候释放自己,达到锁的效果
java.util.concurrent.locks.ReentrantLock的lock方法是
public void lock() {
sync.lock();
}
而sync的实现类其中一个是java.util.concurrent.locks.ReentrantLock.NonfairSync 不公平锁,它的逻辑比较直接
/**
NonfairSync
*/
final void lock() {
if (compareAndSetState(0, 1))//cas操作,如果true 则表示操作成功,获取锁
setExclusiveOwnerThread(Thread.currentThread()); //设置获取锁拥有者为当前线程
else
acquire(1);//获取锁失败,锁住线程(自己)
}
获取失败后阻塞线程
如果获取锁失败,会再尝试一次,失败后,将线程(自己)阻塞
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//如果期望值为0,内存值也为0,再次尝试获取锁(此时其他线程也可能尝试获取锁)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); //第二次获取成功,放回true
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; //没有获取到锁,返回false,则 !tryAcquire(arg) 为true,执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
}
获取锁失败,线程会进入循环,acquireQueued 方法中for是个无限循环,除非获取锁成功后,才会return。
//获取锁失败后,准备阻塞线程(自己)
//阻塞之前,添加节点存放到链表,其他线程可以通过这个链表唤醒此线程
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {//cas操作
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// 在此方法直到获取锁成功才会跳出循环
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; //获取锁成功之后才会return跳出此方法
}
if (shouldParkAfterFailedAcquire(p, node) && //如果满足阻塞条件
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//停止线程(自己)
return Thread.interrupted();
}
释放锁
一个线程拿到锁之后,执行完关键代码,必须unlock释放锁的,否则其他线程永远拿不到锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//java.util.concurrent.locks.ReentrantLock.Sync 的tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //这里一般是 1 - 1 = 0
if (Thread.currentThread() != getExclusiveOwnerThread()) //只能是锁的拥有者释放锁
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); //设置state为0,相当于释放锁,让其他线程compareAndSetState(0, 1)可能成功
return free;
}
protected final void setState(int newState) {
state = newState; //没有cas操作
}
setState不做cas操作是因为,只有拥有锁的线程才调用unlock,不存才并发混乱问题
其他线程没拿到锁不会设值成功,其他线程在此线程设置state为0之前,compareAndSetState(0, 1)都会失败,拿不到锁,此线程设置state为0之后,其他线程compareAndSetState(0, 1)才有可能成功,返回true从而拿到锁
释放线程
线程在获取锁失败后,有可能阻塞线程(自己),在阻塞之前把阻塞线程信息放入链表的
释放锁之后,线程会尝试通过链表释放其他线程(一个),让一个阻塞线程恢复运行
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t; //循环,找到链表最前面需要被唤醒的线程
}
if (s != null)
LockSupport.unpark(s.thread); //唤醒(释放)被阻塞的线程
}
阻塞线程被取消阻塞后如何拿到锁(ReentrantLock中)
有时候线程被中断后,唤醒继续执行后面的代码,
线程没有拿到锁之后主动阻塞自己的,但所还没拿到,被唤醒之后怎么去尝试重新获取锁呢? 里面有一个for循环
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());//拿到锁
else
acquire(1); //没有拿到锁
}
// 上锁失败,会添加一个节点,节点包含线程信息,将此节点放入队列
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 存好节点后,将线程(自己)中断,等其他线程唤醒(自己)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//循环 被唤醒后线程还是在此处循环
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//尝试获取锁
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; //如果拿到锁了,才会return
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //没拿到锁时,主动中断Thread.currentThread()
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
被唤醒后继续执行compareAndSetState(0, 1)返回false没拿到锁,则继续循环或阻塞
compareAndSetState(0, 1) 这个操作是获取锁的关键
ReentrantLock锁-CAS与阻塞的更多相关文章
- volatile、synchronized、ReentrantLock与CAS
目录 一.JVM内存模型: 二.volatile关键字 1.volatile保证内存可见性. 2.能禁止指令重排序 3.不能保证原子性 三.synchronized关键字 1.内存可见性: 2.操作的 ...
- Java并发包源码学习系列:基于CAS非阻塞并发队列ConcurrentLinkedQueue源码解析
目录 非阻塞并发队列ConcurrentLinkedQueue概述 结构组成 基本不变式 head的不变式与可变式 tail的不变式与可变式 offer操作 源码解析 图解offer操作 JDK1.6 ...
- JUC原子操作类与乐观锁CAS
JUC原子操作类与乐观锁CAS 硬件中存在并发操作的原语,从而在硬件层面提升效率.在intel的CPU中,使用cmpxchg指令.在Java发展初期,java语言是不能够利用硬件提供的这些便利来提 ...
- Android进阶——多线程系列之wait、notify、sleep、join、yield、synchronized关键字、ReentrantLock锁
多线程一直是初学者最困惑的地方,每次看到一篇文章,觉得很有难度,就马上叉掉,不看了,我以前也是这样过来的.后来,我发现这样的态度不行,知难而退,永远进步不了.于是,我狠下心来看完别人的博客,尽管很难但 ...
- 并发系列2:Java并发的基石,volatile关键字、synchronized关键字、乐观锁CAS操作
由并发大师Doug Lea操刀的并发包Concurrent是并发编程的重要包,而并发包的基石又是volatile关键字.synchronized关键字.乐观锁CAS操作这些基础.因此了解他们的原理对我 ...
- [Python 多线程] Lock、阻塞锁、非阻塞锁 (八)
线程同步技术: 解决多个线程争抢同一个资源的情况,线程协作工作.一份数据同一时刻只能有一个线程处理. 解决线程同步的几种方法: Lock.RLock.Condition.Barrier.semapho ...
- ReentrantLock 锁释放源码分析
ReentrantLock 锁释放源码分析: 调用的是unlock 的方法: public void unlock() { sync.release(1); } 接下来分析release() 方法: ...
- AQS源码深入分析之独占模式-ReentrantLock锁特性详解
本文基于JDK-8u261源码分析 相信大部分人知道AQS是因为ReentrantLock,ReentrantLock的底层是使用AQS来实现的.还有一部分人知道共享锁(Semaphore/Count ...
- Java锁与非阻塞算法的性能比较与分析+原子变量类的应用
15.原子变量与非阻塞同步机制 在java.util.concurrent包中的许多类,比如Semaphore和ConcurrentLinkedQueue,都提供了比使用Synchronized更好的 ...
随机推荐
- Event Loop - 事件队列
Event Loop 定义: event - 事件 loop - 循环,既然叫事件循环,那么循环的点在哪? 循环的是一个又一个的任务队列,这些任务队列由宏任务和微任务构成 两条原则 一次处理一个任务 ...
- 软件安全----警惕缓冲区溢出(C中那些不安全的库函数)
原文链接:https://blog.csdn.net/yang_yulei/article/details/45314177 链接:http://www.360doc.com/content/11/0 ...
- django和DRF的不同点
django中封装了drf view 是 父类 class test1(View): def post(self,request): data=request.POST.get('name') ret ...
- Java 命名之道
为什么需要重视命名? 好的命名即是注释,别人一看到你的命名就知道你的变量.方法或者类是做什么的! 好的命名对于其他人(包括你自己)理解你的代码有着很大的帮助! 简单举个例子说明一下命名的重要性. &l ...
- 静湖ABC段扫描器(长更)
想学习python图形化的知识,从C段扫描器开始入手,很多的扫描器只能扫描C段,对于A,B段却没有设置这个功能,所以想将扫描A段和B段的功能跟C段一起做了. 暂时的想法是,先完成C段扫描器,扫描的同时 ...
- 堆叠注入tips
漏洞成因 使用mysqli_multi_query()这种支持多语句执行的函数 使用PDO的方式进行数据查询,创建PDO实例时PDO::MYSQL_ATTR_MULTI_STATEMENTS设置为tr ...
- 使用k8s部署springboot+redis简单应用
准备 本文将使用k8s部署一个springboot+redis应用,由于是示例,所以功能比较简单,只有设置值和获取值两个api. (1)设置值 (2)获取值 构建Web应用 (1)创建一个spring ...
- 1、tensorflow 框架理解
2020/10/31 参考:https://blog.csdn.net/mzpmzk/article/details/78636127 1. 两大步骤:定义图define the graph, 进行计 ...
- 老男孩培训作业day1
作业一:博客(开通博客园) 作业二:编写登录接口 输入用户名密码 认证成功后显示欢迎信息 输错三次后锁定 作业三:多级菜单 三级菜单 可依次选择进入各子菜单 所需新知识点:列表.字典(列表和字典的相互 ...
- javascript:void(0)用法和常见问题
javascript:void(0)的用法 下面的代码创建了一个超级链接,当用户以后不会发生任何事.当用户链接时,void(0) 计算为 0,但 Javascript 上没有任何效果. <a H ...