Java(76)Lock之ReentrantLock

Java(76)Lock之ReentrantLock,第1张

Java(76)Lock之ReentrantLock

 在上一篇文章:《多线程学习04-->解决多线程安全-Lock》中

https://blog.csdn.net/fen_fen/article/details/121470551

学Lock时,发现创建对象用的是ReentrantLock。这里也了解下“ReentrantLock”

//创建一个锁对象

private final Lock lock = new ReentrantLock();

ReentrantLock简介

ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

独占锁:同一时间内只有一个线程可以获取这个锁并占用资源。其他线程想要获取锁,必须等待这个线程释放锁。

ReentrantLock类图:

ReentrantLock类中:

ReentrantLock实现了Lock接口,构造方法默认是非公平锁。

内部有三个内部类,Sync、NonfairSync、FairSync;

1.Sync是一个抽象类,它继承AbstractQueuedSynchronizer,Sync实现了AbstractQueuedSynchronizer的tryRelease方法。

       其中AbstractQueuedSynchronizer是一个模板类,它实现了许多和锁相关的功能,并提供了钩子方法供用户实现,比如tryAcquire,tryRelease等。

2.NonfairSync和FairSync两个类继承自Sync,实现了lock方法,然后分别公平抢占和非公平抢占针对tryAcquire有不同的实现。

ReentrantLock的特性

可重入锁
公平锁、非公平锁
可中断
可定时

ReentrantLock使用

ReentrantLock是属于java的一个类,需要lock()和unlock()方法配合try/finally语句块来完成。

下面是ReentrantLock使用的格式,在使用了ReentrantLock加锁以后一定要给它解锁。

主要用到了:lock和unlock这两个方法

ReentrantLocklock = new ReentrantLock();
try{
    lock.lock();//加锁 *** 作
}finally{

    //解锁 *** 作
    lock.unlock();
}

上面示例中

ReentrantLock lock = new ReentrantLock();

lock.lock();//调用lock方法,加锁 *** 作

看了下ReentrantLocklock源码,

1、在初始化ReentrantLock的时候,如果我们不传参数,那么默认使用非公平锁,也就是NonfairSync。

2、上面调用ReentrantLock的lock方法的时候,实际上是调用了NonfairSync(非公平锁)的lock方法。

public ReentrantLock() {
    sync = new NonfairSync();       //非公平锁
}
static final class NonfairSync extends Sync {     //非公平锁NonfairSync继承Sync
    private static final long serialVersionUID = 7316153563782823691L;

    
    //这个方法先用CAS *** 作,去尝试抢占该锁。如果成功,就把当前线程设置在这个锁上,表示抢占成功。如果失败,则调用acquire模板方法,等待抢占
    final void lock() {
        //以CAS方式尝试将AQS中的state从0更新为1
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread()); //获取锁成功则将当前线程标记为持有锁的线程,然后直接返回
        else
            acquire(1); //获取锁失败则执行该方法
    }

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

CAS全称:compareAndSetState

参考:深入理解并发之CompareAndSet(CAS) - 小程故事多 - ITeye博客
AQS全称:AbstractQueuedSynchronizer类

compareAndSetState(0, 1)首先尝试快速获取锁,以CAS的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。

若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。

若获取锁失败,则执行acquire(1)方法。

以下是acquire(int arg)方法

该方法主要的逻辑都在if判断条件中,这里面有3个重要的方法tryAcquire(),addWaiter()和acquireQueued(),这三个方法中分别封装了加锁流程中的主要处理逻辑。

----->调用acquire(1)实际上使用的是AbstractQueuedSynchronizer类的acquire方法,它是一套锁抢占的模板,总体原理是先去尝试获取锁,如果没有获取成功,就在CLH队列中增加一个当前线程的节点,表示等待抢占。然后进入CLH队列的抢占模式,进入的时候也会去执行一次获取锁的 *** 作,如果还是获取不到,就调用LockSupport.park将当前线程挂起。那么当前线程什么时候会被唤醒呢?当持有锁的那个线程调用unlock的时候,会将CLH队列的头节点的下一个节点上的线程唤醒,调用的是LockSupport.unpark方法。

类:AbstractQueuedSynchronizer
public final void acquire(int arg) {
    //使用tryAcquire这个钩子方法去尝试再次获取锁
    if (!tryAcquire(arg) &&      //当前线程尝试获取锁,若获取成功返回true,否则false    
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))   //只有当前线程获取锁失败才会执行者这部分代码
        selfInterrupt();
}

tryAcquire(arg)返回成功,则说明当前线程成功获取了锁(第一次获取或者重入),由取反和&&可知,整个流程到这结束,只有当前线程获取锁失败才会执行后面的判断。先来看addWaiter(Node.EXCLUSIVE)
部分,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。

tryAcquire是AQS中定义的钩子方法:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
  //该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法
}

---->acquire方法内部先使用tryAcquire这个钩子方法去尝试再次获取锁,这个方法在NonfairSync这个类中其实就是使用了nonfairTryAcquire,具体实现原理是先比较当前锁的状态是否是0,如果是0,则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程),如果当前锁的状态不是0,就去比较当前线程和占用锁的线程是不是一个线程,如果是,会去增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁。如果以上两种情况都不通过,则返回失败false。
tryAcquire()方法在NonfairSync类(非公平模式)的实现:

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

底层调用了nonfairTryAcquire()
从方法名上我们就可以知道这是非公平模式下尝试获取锁的方法,具体实现如下:

        
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();  //获取当前线程实例
            int c = getState();       //获取state变量的值,即当前锁被重入的次数
            if (c == 0) {             //state为0,说明当前锁未被任何线程持有
                if (compareAndSetState(0, acquires)) {   //以cas方式获取锁,
                    setExclusiveOwnerThread(current);     //将当前线程标记为持有锁的线程
                    return true;  //获取锁成功,非重入
                }
            }
            //当前锁的状态不是0
            else if (current == getExclusiveOwnerThread()) {   //当前线程就是持有锁的线程,说明该锁被重入了
                int nextc = c + acquires;   //计算state变量要更新的值
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);  //非同步方式更新state值
                return true;   //获取锁成功,重入
            }
            return false;   //以上两种情况都不通过,则返回失败false
        }

tryAcquire一旦返回false,就会则进入acquireQueued流程,也就是基于CLH队列的抢占模式:

首先,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用addWaiter实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点就行了。

AbstractQueuedSynchronizer中addWaiter源码:

    
    private Node addWaiter(Node mode) {
        //首先创建一个新节点,并将当前线程实例封装在内部,mode这里为null
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 当CLH队列不为空的视乎,直接在队列尾部插入一个节点
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //当CLH队列为空的时候,调用enq方法初始化队列
        //入队的逻辑这里都有
        enq(node);
        return node;
    }

    
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;      //t指向当前队列的最后一个节点,队列为空则为null
            if (t == null) { // Must initialize   /队列为空
                if (compareAndSetHead(new Node()))  //构造新结点,CAS方式设置为队列首元素,当head==null时更新成功
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {   //CAS将尾指针指向当前结点,当t(原来的尾指针)==tail(当前真实的尾指针)时执行成功
                    t.next = node;    //原尾结点的next指针指向当前结点
                    return t;
                }
            }
        }
    }

.......待续

参考:

Java多线程(六) 解决多线程安全——ReentrantLock及源码解析

https://zhuanlan.zhihu.com/p/413377004

 参考:《ReentrantLock——可重入锁的实现原理》​​​​​​https://www.cnblogs.com/heqiyoujing/p/11145146.html

理解源码:

从源码角度彻底理解ReentrantLock(重入锁) - takumiCX - 博客园

深入浅出ReentrantLock - 简书

Java常见Lock(二): lock之ReentrantLock

https://blog.csdn.net/a355586533/article/details/78355806

java中ReentrantLock核心源码详解

https://www.cnblogs.com/xxyyy/p/12673920.html

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5606152.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-15

发表评论

登录后才能评论

评论列表(0条)

保存