文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

ReentrantLock从源码解析Java多线程同步学习

2023-05-16 17:41

关注

前言

如今多线程编程已成为了现代软件开发中的重要部分,而并发编程中的线程同步问题更是一道难以逾越的坎。在Java语言中,synchronized是最基本的同步机制,但它也存在着许多问题,比如可重入性不足、死锁等等。为了解决这些问题,Java提供了更加高级的同步机制——ReentrantLock。

管程

管程(Monitor)是一种用于实现多线程同步的抽象数据类型,它可以用来协调不同线程之间的互斥和同步访问共享资源。通俗地说,管程就像一个门卫,控制着进入某个共享资源区域的线程数量和时间,以避免多个线程同时访问导致的数据竞争和混乱。

管程模型

在Java中,采用的是基于Mesa管程模型实现的管程机制。具体地,Java中的synchronized关键字就是基于Mesa管程模型实现的,包括Java中的AbstractQueuedSynchronizer(AQS)可以被看作是一种基于管程模型实现的同步框架。

MESA模型

主要特点

MESA模型采用了互斥访问的机制,即同一时刻只能有一个线程进入管程执行代码。

MESA模型还引入了条件变量的概念,用于实现线程间的等待和唤醒操作。条件变量提供了一种机制,使得线程可以在等待某个条件成立时挂起,并在条件成立时被唤醒。

MESA模型使用等待队列来维护处于等待状态的线程,这些线程都在等待条件变量成立。等待队列由一个或多个条件变量组成,每个条件变量都有自己的等待队列。

MESA模型要求管程中的所有操作都是原子操作,即一旦进入管程,就不能被中断,直到操作执行完毕。

AQS

在讲ReentrantLock之前先说一下AQS,AQS(AbstractQueuedSynchronizer)是Java中的一个同步器,它是许多同步类(如ReentrantLock、Semaphore、CountDownLatch等)的基础。AQS提供了一种实现同步操作的框架,其中包括独占模式和共享模式,以及一个等待队列来管理线程的等待和唤醒。AQS也借鉴了Mesa模型的思想。

共享变量

AQS内部维护了属性volatile int state表示资源的可用状态
state三种访问方式:

资源访问方式

Exclusive-独占,只有一个线程能执行,如ReentrantLock

Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

主要方法

队列

node节点等待状态

ReentrantLock源码分析

在ReentrantLock中有一个内部类Sync会继承 AQS然后将同步器所有调用都映射到Sync对应的方法。

实例化ReentrantLock


public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock还提供了一个传布尔值的实例化方式,这个传true用来创建一个公平锁的,默认是创建非公平锁。非公平锁的话 sync是用NonfairSync来进行实例化,公平锁sync是用FairSync来进行实例化。

加锁

现在假设有AB两个线程来竞争锁

A线程加锁成功

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    
    final void lock() {
        //CAS修改state状态
        if (compareAndSetState(0, 1))
           //修改成功设置exclusiveOwnerThread
            setExclusiveOwnerThread(Thread.currentThread());
        else
           //尝试获取资源
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

假定A线程先CAS修改成功,他会设置exclusiveOwnerThread为A线程

B线程尝试加锁

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

我们先看tryAcquire()方法,这里体现出了他的可重入性。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取当前资源标识
    int c = getState();
    if (c == 0) {
        //如果资源没被占有CAS尝试加锁
        if (compareAndSetState(0, acquires)) {
            //修改成功设置exclusiveOwnerThread
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //资源被占有要判断占有资源的线程是不是当前线程,加锁成功设置的exclusiveOwnerThread值在这里就派上了用处
    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;
}

根据上面源码我们可以看出B线程尝试加锁是失败的,接下来看尝试加锁失败后的方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg),该方法实现分为两个部分:

我们先解析addWaiter

private Node addWaiter(Node mode) {
    //构建一个当前线程的node节点 这里prev 和 next 都为null
    Node node = new Node(Thread.currentThread(), mode);
    // 指向双向链表的尾节点的引用
    Node pred = tail;
    //B线程进来目前还未构建任何队列这里肯定是空的
    if (pred != null) {
        //如果已经构建过队列会把当前线程的node节点的上一个node节点指向tail尾节点
        node.prev = pred;
        //CAS操作把当前线程的node节点设置为新的tail尾节点
        if (compareAndSetTail(pred, node)) {
            //把旧的tail尾节点的下一个node节点指向当前线程的node节点
            pred.next = node;
            return node;
        }
    }
    //尾节点为空执行
    enq(node);
    return node;
}
private Node enq(final Node node) {
    //死循环当tail 尾节点不为空才会跳出
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //用CAS构建出一个head空的node节点
            if (compareAndSetHead(new Node()))
            //将当前空的node节点交给尾节点(下一次循环就会走else分支)
                tail = head;
        } else {
            //把我们addWaiter中创建的node节点的prev指向了当前线程node节点
            node.prev = t;
            //将tail尾节点更改为当前线程的node节点
            if (compareAndSetTail(t, node)) {
                //将t的下一个节点指向当前线程创建的node节点
                t.next = node;
                return t;
            }
        }
    }
}

执行完这里初步的一个等待队列就构建好了

解析完addWaiter我们再来解析acquireQueued,addWaiter执行完后的结果会返回一个双向列表的node节点

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        //中断标志位
        boolean interrupted = false;
        for (;;) {
            //获取当前线程node节点的上一个节点
            final Node p = node.predecessor();
            //如果上一个节点就是head节点说明当前线程其实是处在队列第一位然后就会再次尝试加锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //这里是重点 这个方法来判断当前线程是否应该进入等待状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                //调用LockSupport.park(this)阻塞了当前线程等待被唤醒
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取上一个节点的等待状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)//表示当前节点的后继节点包含的线程需要运行
        
        return true;
    if (ws > 0) {//当前线程被取消
        
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//设置等待状态为-1
    }
    return false;
}

运行到parkAndCheckInterrupt()B线程就会被阻塞了,后续的逻辑我们在解锁操作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)
            //head节点不为空并且等待状态不是0就去进行unpark操作
            unparkSuccessor(h);
        return true;
    }
    return false;
}
private void unparkSuccessor(Node node) {
    
    //head节点等待状态
    int ws = node.waitStatus;
    //
    if (ws < 0)
       //将头节点的等待状态修改为0
        compareAndSetWaitStatus(node, ws, 0);

    
    //获取头节点的下一个节点(也就是B节点)
    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)
        //对该线程进行unpark唤醒(B节点)
        LockSupport.unpark(s.thread);
}

唤醒之后我们的B线程就能继续往下走了,我们继续看刚刚的acquireQueued()方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            //这里尝试获取锁由于A线程释放了锁这里是肯定获取成功的
            if (p == head && tryAcquire(arg)) {
                //把head设置为当前节点(也就是往前移一位,并且把上一个节点指向指为null)
                setHead(node);
                //把刚刚的上一个节点也指向为null (这里他就没引用了会被GC回收掉)
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                //刚刚在这里阻塞现在被唤醒
                parkAndCheckInterrupt())
                //设置标志中断位为true 然后开始下一次循环
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

总结

以上就是ReentrantLock从源码解析Java多线程同步学习的详细内容,更多关于Java多线程ReentrantLock的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯