前言

AQS一共花了5篇文章,对里面实现的核心源码都做了注解 也和大家详细描述了下,后面的几篇文字我将和大家聊聊一下AQS的实际使用,主要会聊几张锁,第一篇我会和大家聊下ReentrantLock 重入锁,虽然在讲AQS中也穿插了讲了一下,但是我还是深入的聊下

PS:前面5篇写在了CSDN里面 懒得转过来来了,有兴趣的移步去看下吧

  1. Java-AQS同步器 源码解读1-独占锁加锁
  2. Java-AQS同步器 源码解读2-独占锁解锁
  3. Java-AQS同步器 源码解读3-共享锁
  4. Java-AQS同步器 源码解读4-条件队列上
  5. Java-AQS同步器 源码解读5-条件队列下

ReentrantLock简介

ReentrantLock中文翻译:重入锁。那具体重入是什么意思呢,如果看过前面几篇文章的人,应该了解一下,简答的说就是AQS同步器里面的State相当于一个计数器,如果某一个线程获取锁了以后再再次去获取锁,这个计算器State就会+1.后面的代码中会详细的描述到。

还有一个重要的点 就是lock和Condition的联合使用,ReentrantLock可以创建一个Condition,这个我在条件队列的文章中详细描述过。

Synchronized对比

ReentrantLock是一个排他锁,也就是同一个时刻只会有一个线程能获取到锁,这个主要利用的就是AQS的独占模式实现的。ReentrantLock能保证在多线程的情况下的线程执行安全,那就会想到Synchronized的关键字。Synchronized是JVM实现的,ReentrantLock是由JDK实现的,ReentrantLock相对于比较灵活,可以设置时间等待,线程中断,锁投票等,但是一定用完记得要在finally手动释放,Synchronized是JVM做自动释放锁的操作。

用法

/**
* @ClassName ReentrantLockDemo
* @Auther burgxun
* @Description: 重入锁的Demo
* @Date 2020/4/5 14:21
**/
public class ReentrantLockDemo { private static ReentrantLock reentrantLock = new ReentrantLock(); public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
reentrantLock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("获取锁的线程是:" + finalI);
reentrantLock.unlock();
}).start();
}
}
}

执行结果是:

org.example.ReentrantLockDemo
获取锁的线程是:1-开始执行
获取锁的线程是:1-执行结束
获取锁的线程是:2-开始执行
获取锁的线程是:2-执行结束
获取锁的线程是:4-开始执行
获取锁的线程是:4-执行结束
获取锁的线程是:3-开始执行
获取锁的线程是:3-执行结束
获取锁的线程是:5-开始执行
获取锁的线程是:5-执行结束 Process finished with exit code 0

从执行结果上 我们可以看到 一个线程执行完成释放锁后才能执行另外一个线程

源码分析

看完了上面的简介和用法,我们进入源码去分析看下 是怎么实现的

代码结构

方法分析

从UML类图上面我们可以看到 ReentrantLock有3个内部类,一个是抽象的静态类Sync还有2个实现了Sync的类一个是非公平锁的实现NonfairSync,还有一个是公平锁的实现FairSync

Sync

首先我们看下Sync这个抽象类

abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock(); /**
* 非公平锁锁的tryAcquire的实现 AQS中tryAcquire方法的需要子类重写
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取CAS的State值
if (c == 0) {//如果等于0 说明可以获取锁
if (compareAndSetState(0, acquires)) {//CAS的方式 修改State 状态
setExclusiveOwnerThread(current);//CAS修改成功后 设置当前线程为锁的持有者
return true;
}
} else if (current == getExclusiveOwnerThread()) {//判断当前线程 是否是锁的持有者线程
int nextc = c + acquires;//新增重入次数
if (nextc < 0) // overflow //如果小于0 说明是出问题了 抛出异常
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
} /**
* AQS 中方法的重写 释放资源
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// 算下 当前同步器中state的差值
//确保释放和加锁线程是同一个 上一篇中我也提到过
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;//是否完全了释放资源
if (c == 0) {//如果C的值是0 说明没有线程拥有锁了
free = true;
/*
* 设置拥有锁的线程为null 因为现在锁是没线程暂用的 如果不修改 下次别的线程去获取锁的会有这个判断
*/
setExclusiveOwnerThread(null);
}
setState(c);//修改 同步器的State 状态
return free;
} /**
* 判断当前线程 是否是拥有锁的线程一致 重写了AQS的方法
*/
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
} final ConditionObject newCondition() {
return new ConditionObject();
} /**
* 获取当前重入锁的拥有者
*/
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
} /**
* 当前锁占用的State 数量 也就是重入了几次
*/
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
} /**
* 重入锁 是否可以被占用 State等于0 别的线程才能来抢占到锁
*/
final boolean isLocked() {
return getState() != 0;
}
}

上面是我写了注解的Sync的整个类,一会儿我们挨个分析下,从继承关系 我们可以看到Sync是继承于我们的AQS的,所以里面很多底层的方法都是用的AQS里面的实现,所以说呀 理解了AQS 那整个JUC下的锁和各种线程安全的集合什么的 看源码都会轻松很多~

NonfairSync

//非公平锁
static final class NonfairSync extends Sync { @Override
void lock() {
if (compareAndSetState(0, 1))//如果CAS能设置成功 说明能获取到锁 就设置当前线程为锁的owner 不公平的体现
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
} /**
* 尝试获取资源,复写的AQS类里面方法
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

FairSync

  //公平锁
static final class FairSync extends Sync { @Override
void lock() {
acquire(1);
} /**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/*
* hasQueuedPredecessors 返回是否Sync中是否有在当前线程前面等待的线程 false没有
* 如果false 那就CAS 修改State 成功后更新当前线程是锁的拥有者线程
* */
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

非公平锁VS公平锁

什么是公平非公平

看了上面的代码 有人可能不明白什么叫做TMD公平!!!哈哈

具体的代码下文描述,我先举个小例子,让读者带入下:

话说大家在火车站排队买票,小明和小强放寒假了,都去火车站提前买票准备回家,他们俩到了车站一看,售票窗口就开了一个,而且前面有3个人在排队,那小明和小强没办法就只能排除在了队伍里面,等了5分钟终于排到了小强,小强刚买完票,突然看到同个宿舍的小张也来买票了,立马和小张说,来来来 这边 我这边可以买票,小张立马插队到了小强后面,强行去买了票,小明心里嘀咕说居然插队,素质真差,看你人高马大的 就算了吧!哈哈~

其实上面的类子中 公平和非公平的体现就是 卖票的窗口只有一个,就像获取独占锁,当有一个线程占有了锁,那其余的线程就必须在后面排队等待,就像买票一样,非公平的锁的实现就相当于插队,管你后面有没有人 我都要去尝试下买票

从上面的分析我们也能知道公平和非公平是指的是获取资源时候的行为。

ReentrantLock

ReentrantLock的构造函数

/**
* 默认实现非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
} /**
* 带参数的构造函数
*/
public ReentrantLock(boolean fair) {
sync = fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync();
}

从上面可以看到ReentrantLock默认使用的是一个非公平锁的实现

lock加锁方法

    /**
* 加锁
*/
@Override
public void lock() {
sync.lock();
} /**
* 加锁 响应中断
*/
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
} @Override
/**
* 只做一次获取锁的尝试 不会阻塞线程
*/
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
} @Override
/**
* 只做一次获取锁的尝试 不会阻塞线程
*/
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

非公平的加锁

从代码中 我们看到lock方法调用的是Sync的lock方法,但是Sync中的lock方法是一个抽象方式是子类实现的

那我从NonfairSync类中找到了lock的具体实现,入下:

 void lock() {
//如果CAS能设置成功 说明能获取到锁 就设置当前线程为锁的owner 不公平的体现
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

首先方法一开始先执行一个CAS修改State的操作,如果能够执行成功,说明当前的AQS同步器中的状态值是0,那么线程就可以占有锁,然后设置当前线程为锁的Owner线程.setExclusiveOwnerThread这个方法我在第一篇文章中也描述过,这个方法是在AQS的父类AbstractOwnableSynchronizer方法里面的!

如果执行失败,那么方法就执行acquire方法:

 /**
* 此方法位于AQS中
* 获取资源
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} /**
* 此方法位于NonfairSync中
* 尝试获取资源,复写的AQS类里面方法
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
} /**
* 此方法位于Sync中
* 非公平锁锁的tryAcquire的实现 AQS中tryAcquire方法的需要子类重写
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取CAS的State值
if (c == 0) {//如果等于0 说明可以获取锁
if (compareAndSetState(0, acquires)) {//CAS的方式 修改State 状态
setExclusiveOwnerThread(current);//CAS修改成功后 设置当前线程为锁的持有者
return true;
}
} else if (current == getExclusiveOwnerThread()) {//判断当前线程 是否是锁的持有者线程
int nextc = c + acquires;//新增重入次数
if (nextc < 0) // overflow //如果小于0 说明是出问题了 抛出异常
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

acquire方法位于AQS中 此方法会首先调用tryAcquire方法去尝试获取下锁,因为虽然lock方法一开始获取锁失败了,可能这边锁又被别的线程释放了,所以要再次尝试获取下锁,具体tryAcquire怎么做的 上面的代码中我已经描述的很清楚了~

公平的加锁

先看下代码:

     /**
* 公平锁版本的加锁
*/
void lock() {
acquire(1);
} /**
* 此方法位于AQS中
* 获取资源
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} /**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/*
* hasQueuedPredecessors 返回是否Sync中是否有在当前线程前面等待的线程 false没有
* 如果false 那就CAS 修改State 成功后更新当前线程是锁的拥有者线程
* */
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

首先这边lock的时候,直接执行了acquire方法,而非和NonfairSync一样,有个CAS的尝试操作,这也是体现公平的一部分,和非公锁的方法一样的是这边的acquire方法也是在AQS上中的,但是调用的tryAcquire方法 是自己fairSync类自己实现的,而非调用的Sync的默认实现,这边唯一有区别的就是hasQueuedPredecessors 这个方法,hasQueuedPredecessors方法是获取当前Sync队列中是否还有别的等待线程,如果有 就算当前的状态State是满足条件的,也是要加入Sync等待队列中的,这个是acquireQueued方法里面做的事情,不清楚这个acquireQueued方法的,看看前面几篇文章,我就不再赘述了~

unlock解锁

@Override
public void unlock() {
sync.release(1);//调用的AQS里面的方法
} /**
* 此方法在AQS中
* 释放当前资源
* 唤醒等待线程去获取资源
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {//说明释放资源成功
Node h = head;//当前队列里面的head节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//通知阻塞队列里面的head的next节点去获取锁
return true;
}
return false;
} /**
* 此方法在Sync中
* AQS 中方法的重写 释放资源
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// 算下 当前同步器中state的差值
//确保释放和加锁线程是同一个 上一篇中我也提到过
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;//是否完全了释放资源
if (c == 0) {//如果C的值是0 说明没有线程拥有锁了
free = true;
/*
* 设置拥有锁的线程为null 因为现在锁是没线程暂用的 如果不修改 下次别的线程去获取锁的会有这个判断
*/
setExclusiveOwnerThread(null);
}
setState(c);//修改 同步器的State 状态
return free;
}

从源码上看 解锁的方法只有一个 因为公平锁和非公共锁 只是描述的是加锁的行为,解锁的行为其实都是一致的,都是释放当前线程占用State值,然后唤醒SyncQueue的头部Head节点的下一个节点去尝试获取锁!

有些没帖子上面的方法描述 请在前面AQS中的几篇文章中看下 都详细描述过~

总结

公平锁 VS 非公平锁

首先2者并没有好坏之分,是要根据对应的场景选择对应的锁技术

公平锁 则重的是公平性

非公平锁 则重的是并发性

非公平锁 是抢占式的,忽略了SyncQueue重其他的等待线程,线程在进入等待队列之前会进行2次尝试获取锁,这大大增加了获取锁的机会,这种好处体现在2个方面:

  • 线程不必加入等待队列就可以获取锁,不仅免去了构造节点并加入队列的繁琐操作,同时也节省了线程阻塞的唤醒开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发的情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过了线程持有并释放锁的时间的开销,那么这种抢占式的特性对并发的性能的提高会很明显
  • 减少CAS竞争,如果线程必须要加入阻塞队列才能去获取锁,那么入队时的CAS竞争将变得异常的激烈,CAS操作虽然不会导致线程失败而挂起,但不断的失败重试导致对CPU的浪费是不能忽略的

Synchronized VS ReentrantLock

从整个文章的分析来看,ReentrantLock是比Synchronized更加的灵活的,

  • ReentrantLock提供了更多 更全面的API 可以设置等待时间,可以中断方法等,还提供了Trylock等非阻塞的方法
  • ReentrantLock还可以配和Condition一起使用,使得线程等待的时候更加灵活,可以设置不同的条件等待 等等

带你看看Java的锁(一)-ReentrantLock的更多相关文章

  1. 带你看看Java的锁(三)-CountDownLatch和CyclicBarrier

    带你看看Java中的锁CountDownLatch和CyclicBarrier 前言 基本介绍 使用和区别 核心源码分析 总结 前言 Java JUC包中的文章已经写了好几篇了,首先我花了5篇文章从源 ...

  2. 带你看看Java的锁(二)-Semaphore

    前言 简介 Semaphore 中文称信号量,它和ReentrantLock 有所区别,ReentrantLock是排他的,也就是只能允许一个线程拥有资源,Semaphore是共享的,它允许多个线程同 ...

  3. 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

    问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...

  4. 一篇blog带你了解java中的锁

    前言 最近在复习锁这一块,对java中的锁进行整理,本文介绍各种锁,希望给大家带来帮助. Java的锁 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人 ...

  5. Java多线程之ReentrantLock重入锁简介与使用教程

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6543947.html  我们知道,线程安全问题需要通过线程之间的同步来解决,而同步大多使用syncrhoize ...

  6. 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

    问题 (1)重入锁是什么? (2)ReentrantLock如何实现重入锁? (3)ReentrantLock为什么默认是非公平模式? (4)ReentrantLock除了可重入还有哪些特性? 简介 ...

  7. java并发系列(三)-----ReentrantLock(重入锁)功能详解和应用演示

    1. ReentrantLock简介 jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock.虽然在性能上ReentrantLock和synchronize ...

  8. Java多线程--锁的优化

    Java多线程--锁的优化 提高锁的性能 减少锁的持有时间 一个线程如果持有锁太长时间,其他线程就必须等待相应的时间,如果有多个线程都在等待该资源,整体性能必然下降.所有有必要减少单个线程持有锁的时间 ...

  9. java synchronized和(ReentrantLock)区别

    原文:http://blog.csdn.net/zheng548/article/details/54426947 区别一:API层面 syschronized使用 synchronized即可修饰方 ...

随机推荐

  1. (转) POJO和javabean的异同

    参考:http://blog.csdn.net/lushuaiyin/article/details/7436318 一:什么是POJOPOJO的名称有多种,pure old java object ...

  2. [一起读源码]走进C#并发队列ConcurrentQueue的内部世界 — .NET Core篇

    在上一篇<走进C#并发队列ConcurrentQueue的内部世界>中解析了Framework下的ConcurrentQueue实现原理,经过抛砖引玉,得到了一众大佬的指点,找到了.NET ...

  3. 10行代码,用python能做出什么骚操作

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:小栗子 PS:如有需要Python学习资料的小伙伴可以加点击下方链接自 ...

  4. 百度AI开发平台简介

    AIstudio https://aistudio.baidu.com/aistudio/index 关于AI Studio AI Studio是基于百度深度学习平台飞桨的一站式AI开发平台,提供在线 ...

  5. 谁说 Vim 不好用?送你一个五彩斑斓的编辑器!

    相信大家在使用各种各样强大的 IDE 写代码时都会注意到,代码中各种类型的关键字会用独特的颜色标记出来,然后形成一套语法高亮规则.这样不仅美观,而且方便代码的阅读. 而在上古神器 Vim 中,我们通常 ...

  6. 数据结构与算法--二分搜索(binary search)

    前言 之前面试准备秋招,重新翻起了<编程之美>.在第三章节看到了一道关于二分搜索的讨论,觉得有许多细节是自己之前也没怎么特别注意地方,比如二分搜索的初始条件,转化.终止条件之类的. 问题 ...

  7. Volatile的应用DCL单例模式(四)

    Volatile的应用 单例模式DCL代码 首先回顾一下,单线程下的单例模式代码 /** * 单例模式 * * @author xiaocheng * @date 2020/4/22 9:19 */ ...

  8. mybatis源码配置文件解析之二:解析settings标签

    在前边的博客中分析了mybatis解析properties标签,<mybatis源码配置文件解析之一:解析properties标签>.下面来看解析settings标签的过程. 一.概述 在 ...

  9. Hadoop环境搭建(centos)

    Hadoop环境搭建(centos) 本平台密码83953588abc 配置Java环境 下载JDK(本实验从/cgsrc 文件中复制到指定目录) mkdir /usr/local/java cp / ...

  10. 详解 Properties类

    (请观看本人博文--<详解 I/O流>) Properties类: 概念: Properties 类的对象 是 一个持久的属性集 Properties 可 保存在流中 或 从流中加载 属性 ...