深入JUC源码看锁

1 篇文章 0 订阅
1 篇文章 0 订阅
锁,相信大家都听过也用过。如下伪代码:
Lock lk = new Lock();
 
try{ 
    lk.lock(); 
    // 不为人知的代码块… 
    … 
} finally { 
    lk.unLock(); 
}
FAQ:
用起来简单丝滑有没有?但是请仔细思考一下如下几个问题:
  1. 什么场景下用它?
  2. 它帮我们解决了啥问题?
  3. 为啥区区一个锁就能解决这个问题,它为何这么diao,用了啥技术?
一般情况下只有在“并发”访问“共享”资源情况下,为了保证共享资源被安全的“读写”。往往我们就需要某种同步机制来保证“共享资源并发安全”,锁是同步机制的一种实现。
场景类比:

必要基础知识点

  • CAS
  • volatile
  • 什么是自旋
  • AQS
  • 如何实现对线程的阻塞和唤起
要彻底整明白JUC底层是如何实现锁的需要有如上基础知识的储备。(如果您上车还没来得及买票的,请下车后及时补票!!!O(∩_∩)O哈哈~)

源码分析

给大家分享一下源码分析的个人心得:(不仅限于本次JUC代码分析的场景)
看源码首先得有个大的思想:就是理解什么是程序?
个人理解:程序 = 数据结构 + 算法
在源码阅读的场景下,程序即代码,我更倾向于把:数据结构 + 算法,翻译成:模型+流程。 即:如何封装类模型,通过模型构成一个个“数据结构”,通过一些流程控制代码比如,if,for,return等等来实现“算法”。
最终源码阅读就分解为了:把代码中关键的模型类理清楚,然后有哪些算法在控制它们的读写。把“模型”和流程通过流程图的方式呈现出来。

主角ReentrantLock

       ReentrantLock是基于AQS实现的一种可重入锁,它有公平和非公平锁的两种实现。

JUC的基石AQS

       AQS即AbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是 同步队列条件队列。其中 同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而 条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的 等待状态-唤醒的工作。
 AbstractQueuedSynchronizer
其实是一个很重要的“模型”类,里面有非常非常非常重要的“数据结构”定义。内部类:Node尤为重要;
如下AQS一些关键的属性,及内部类,理解它们的含义及使用场景很重。
Thread
有一个隐性的点,很容易让人忽略。那就是线程:线程是程序执行的载体,它贯彻于程序允许的整个生命周期。

概览

代码性质数据结构

逻辑性数据结构

  1. CLH 线程同步队列 (带有头、尾指针的双向链表,其节点就是 AQS 的内部类:Node的实例

  1. 等待队列(单向链表,其节点也是 AQS 的内部类:Node的实例

          它不是今天的主角,不是本文内容的要点,这里只是把它的存在展示给大家。

算法=流程

在我看来理解就是代码的逻辑控制,各个数据通过什么样的流程构建数据结构,并在不同的流程中流转;
说白了:根据代码把流程图整清楚就达到理解“算法”的目的了。
如下图:整理了ReetrantLock的加锁和解锁流程

ReentrantLock实现原理

由于篇幅问题:这里只介绍它的非公平锁的流程
它与AQS的关系如下图所示:
AbstractQueuedSynchronizer、Sync、NonfairSync、FairSync 它们之间是模板模式的一种应用实现。在选择公平和非公平锁的逻辑可以看成是策略模式的变种。
ReentrantLock锁结构:
        首先ReentrantLock继承自父类Lock,然后有3个内部类,其中Sync内部类继承自AQS,另外的两个内部类继承自Sync,这两个类分别是用来 公平锁和非公平锁的。 通过Sync重写的方法tryAcquire、tryRelease可以知道, ReentrantLock 实现的是 AQS 的独占模式,也就是独占锁,这个锁是悲观锁
ReentrantLock有个重要的成员变量:
private final Sync sync;
        这个变量是用来指向Sync的子类的,也就是FairSync或者NonfairSync,这个也就是多态的 父类引用指向子类,具体Sycn指向哪个子类,看构造方法:
public ReentrantLock() { 
    sync = new NonfairSync(); 
} 

public ReentrantLock(boolean fair) { 
    sync = fair ? new FairSync() : new NonfairSync(); 
}
ReentrantLock有两个构造方法,无参构造方法默认是创建 非公平锁,而传入true为参数的构造方法创建的是 公平锁

非公平锁的实现原理

当我们使用无参构造方法构造的时候即ReentrantLock lock = new ReentrantLock(),创建的就是非公平锁。
public ReentrantLock() { 
    sync = new NonfairSync(); 
} 

//或者传入false参数 创建的也是非公平锁 
public ReentrantLock(boolean fair) { 
    sync = fair ? new FairSync() : new NonfairSync(); 
}

lock方法获取锁

  1. lock方法调用CAS方法设置state的值,如果state等于期望值0(代表锁没有被占用),那么就将state更新为1(代表该线程获取锁成功),然后执行setExclusiveOwnerThread方法直接将该线程设置成锁的所有者。如果CAS设置state的值失败,即state不等于0,代表锁正在被占领着,则执行acquire(1),即下面的步骤。
  1. nonfairTryAcquire方法首先调用getState方法获取state的值,如果state的值为0(之前占领锁的线程刚好释放了锁),那么用CAS这是state的值,设置成功则将该线程设置成锁的所有者,并且返回true。如果state的值不为0,那就 调用 getExclusiveOwnerThread 方法查看占用锁的线程是不是自己,如果是的话那就直接将state + 1,然后返回true。如果state不为0且锁的所有者又不是自己,那就返回false, 然后线程会进入到同步队列中
final void lock() {
    //CAS操作设置state的值
    if (compareAndSetState(0, 1))
        //设置成功 直接将锁的所有者设置为当前线程 流程结束
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //设置失败 则进行后续的加入同步队列准备
        acquire(1);
}


public final void acquire(int arg) {
    //调用子类重写的tryAcquire方法 如果tryAcquire方法返回false 那么线程就会进入同步队列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}


//子类重写的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    //调用nonfairTryAcquire方法
    return nonfairTryAcquire(acquires);
}


final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果状态state=0,即在这段时间内 锁的所有者把锁释放了 那么这里state就为0
    if (c == 0) {
        //使用CAS操作设置state的值
        if (compareAndSetState(0, acquires)) {
            //操作成功 则将锁的所有者设置成当前线程 且返回true,也就是当前线程不会进入同步
            //队列。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果状态state不等于0,也就是有线程正在占用锁,那么先检查一下这个线程是不是自己
    else if (current == getExclusiveOwnerThread()) {
        //如果线程就是自己了,那么直接将state+1,返回true,不需要再获取锁 因为锁就在自己
        //身上了。
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果state不等于0,且锁的所有者又不是自己,那么线程就会进入到同步队列。
    return false;
}

tryRelease锁的释放

  1. 判断当前线程是不是锁的所有者,如果是则进行步骤2,如果不是则抛出异常。
  1. 判断此次释放锁后state的值是否为0,如果是则代表 锁有没有重入,然后将锁的所有者设置成null且返回true,然后执行步骤3,如果不是则 代表锁发生了重入执行步骤4。
  1. 现在锁已经释放完,即state=0,唤醒同步队列中的后继节点进行锁的获取。
  1. 锁还没有释放完,即state!=0,不唤醒同步队列。
public void unlock() {
    sync.release(1);
}


public final boolean release(int arg) {
    //子类重写的tryRelease方法,需要等锁的state=0,即tryRelease返回true的时候,才会去唤醒其
    //它线程进行尝试获取锁。
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
    
protected final boolean tryRelease(int releases) {
    //状态的state减去releases
    int c = getState() - releases;
    //判断锁的所有者是不是该线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        //如果所的所有者不是该线程 则抛出异常 也就是锁释放的前提是线程拥有这个锁,
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果该线程释放锁之后 状态state=0,即锁没有重入,那么直接将将锁的所有者设置成null
    //并且返回true,即代表可以唤醒其他线程去获取锁了。如果该线程释放锁之后state不等于0,
    //那么代表锁重入了,返回false,代表锁还未正在释放,不用去唤醒其他线程。
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值