ReentrantLock详解

tech2022-08-09  147

java.util.concurrent包(由Doug lea,李二狗设计的),从java1.5版本开始纳入了java正统体系中; ReentrantLock应用:使用其lock()方法加锁和unlock()方法解锁,在需要上锁的代码块前后加上;就可以给这段代码上锁;

cas算法,比较与交换:保证原子性,保证锁的互斥性; 假如两个线程同时要修改一个值refresh,此时我们会给refresh加一个版本,比如线程A1和A2,主线线程A,在刚开始的时候他们的版本都是0; 当A1的refresh值改变并且传到A中,会去比较版本是不是相等如果相等,那么会把A线程的版本变成1,如果A2再来改值,那么A2的版本就和A的版本不同无法更改回来,那么A2只能再去A中读值和版本,读完后再运行A2再重新去重新比对修改A中的值;其中比较与交换的过程是原子性操作。比如在上面A1的版本和A版本比对版本是否相同,并且提成版本为1的过程是原子性,任意时刻只有一个人可以执行成功; 使用cas机制让只有一个线程可以获取锁; 先使用自旋的方式,不断的去判断线程是否可以拥有锁; 如果自旋了一段时间还是不能拥有锁那么就调用LockSupport.park()阻塞线程,并且使用队列的方式统计,在unlock方法中唤醒锁LockSupport.unpark(线程名); 核心: 自旋,LockSupport,cas加锁,queue队列(实现公平锁)

ReentrantLock加锁方法lock(); lock()方法由ReentrantLock类中的内部类sync来实现, 1、sync继承自AbstractQueuedSynchronizer类; 2、sync还有两个字类NonfairSync(非公平锁)和FairSync(公平锁)做lock()方法的具体实现, ReentrantLock lock=new ReentrantLock(true);入参的方式调用公平锁; 默认是非公平锁 FairSync

final void lock() { acquire(1); }

公平锁实现lock()方法时调用了acquire(1);

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //去竞争锁,如果失败,运行acquireQueued(addWaiter(Node.EXCLUSIVE),把抢锁失败的线程拿去队列排序 selfInterrupt(); }

tryAcquire(arg)锁竞争逻辑 tryAcquire(arg)方法调回了FairSync类里的tryAcquire方法;tryAcquire是具体加锁的逻辑;AbstractQueuedSynchronizer类中有tryAcquire方法,FairSync继承自sync类,sync类继承自AbstractQueuedSynchronizer,FairSync是重写了tryAcquire方法;

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前的线程对象 int c = getState();//获取当前锁的状态,非0的状态则是已经被加锁了 if (c == 0) { 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);//这里不需要使用cas的方式去给state赋值,因为这个判断里面只可能是当前线程进入,不存在并发 return true; } return false; }

可重入的意义 当我们方法里面调用了两次lock()方法,可重入的意义就在于,每一个lock()方法对应相应的unlock()方法;不让unlock()关掉不属于他的lock()方法; acquireQueued(addWaiter(Node.EXCLUSIVE)线程入队

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; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

Node节点是AbstractQueuedSynchronizer的内部类,采用双向链表的方式来管理线程,可以想象成在一个队列容器里面存了;需要拿线程时从链表的头部取出。类似队列的方式;

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)) { pred.next = node; return node; } } //走这个方法,自旋 enq(node); return node; }

1、Node.EXCLUSIVE代表的是独占的意思,Node有两种属性是独占(互斥)和共享,Node类中定义了两种属性SHARED和EXCLUSIVE去判断当前的节点是互斥还是共享的。 2、 Node node = new Node(Thread.currentThread(), mode); 我们创建了一个节点,这个节点中有几个非常重要的属性 pre:创建双向链表必不可少的指向前面一个节点。 next:创建双向链表必不可少的指向下面一个节点。 waitState:节点的生命状态(信号量),对特性的封装公用,在不同的环境下怎么用,用waitState来标记用法; ①SIGNAL=-1:可被唤醒 ②CANCELLED=1:代表异常,节点需要被废弃掉 ③CONDITION=-2:条件等待 ④PROPAGATE=-3:可以用来广播的状态 ⑤初始化状态为(init状态):0 thread:保存线程以便在unlock中被唤醒

我们这边传入Node.EXCLUSIVE独占模式,基于Node new一个节点,这个节点通过thread属性指向实际的线程,刚被创建出来时waitState的值为初始值0;把tail指针赋值给pre,但是tail在第一次时也是null值,下面那个方法pred!=null里面的内容不会运行,直接运行enq(node);自旋,

private Node enq(final Node node) { for (;;) { Node t = tail;//tail在定义的时候什么都没给,所以第一次tail是null,在下面给他初始化为new node() //第二次会变成new node(); if (t == null) { // Must initialize //这边给t判断是否为null,也就是给tail判断,如果是null,那么队列并没有产生,那么head也是null //给head初始化,给他一个空壳子,并且做cas运算,免得生成多个队列的头和尾,这会导致生成多个队列 if (compareAndSetHead(new Node())) tail = head; //生成队尾和队头,初始化new Node() } else { //进到这步进来说明至少把head和tail初始化了 node.prev = t; //直接把node的prev属性指向t,但是不需要做原子操作,因为这里还没有加入队列 if (compareAndSetTail(t, node)) { //这一步操作如果t与队尾相等,那么把node指向队尾 t.next = node; //把t指向node,t如果不是第一次进来这边的话,t其实就是上个线程的元素,指向下一个node线程元素 return t; //返回t每什么实际意义,代表着加入队列成功了 } } } }

自旋的目的是入队一定要成功用cas的方式创建Node()保证入队成功, compareAndSetHead(new Node()。将当前对象初始化 语言描述流程: node第一次进入线程:Node t=队尾;如果队尾为空,使用原子操作创建队头,把队尾与其相等,创建队尾;原子操作的原因在于不能允许 node第二次进入线程:此时队尾不为空了,有个壳子,已经被初始化了,依然进行Node t=队尾,创建节点t,t指向队尾,并且,对头此时等于队尾,那么代表t指向对头和队尾,然后对进入方法的node(当前线程的)进行操作,把他的pre指向t,再进行原子操作,保证只有一个线程进行这步操作,是入队,如果t等于队尾,那么把队尾指向node,然后把t的next指向node;此时就形成了,head指向t,t与node之间有双向链表,队尾又指向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; } if (shouldParkAfterFailedAcquire(p, node) && //第一轮循环不会去阻塞只是去把这个线程的前面一个node的状态改成-1.也就是标记当前线程可以被阻塞 parkAndCheckInterrupt()) //进行阻塞操作 interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

通过这个方法把那些加入队列的方法进行阻塞; 入队成功后不会进行马上阻塞,因为阻塞到唤醒,用户态到内核态的操作需要消耗比较多的时间,在阻塞之前还会去抢一次锁,如果获取到了,马上出队,如果获取不到就阻塞线程;

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果为-1可以被唤醒 前面那个节点的状态为-1 return true; if (ws > 0) { //如果大于0那么就是出现异常被干掉了,正常情况下就走另一个选择 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //原子操作把ws,上一个节点的waitStatus设置为Node.SIGNAL也就是-1,那么他的下一个节点就可以被唤醒了,通过记录上一个节点的状态决定下个节点是否可以被唤醒,也从这里判断第一次循环是没有阻塞线程的,而只是把队首的状态改成-1 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

前驱设置状态,当前面一个的线程被回收后,依旧能保证他下下个的线程状态永远都是0;就是初始化状态;这样的话我们要去判断下个线程能否被使用时都需要重新去判断以下状态是否已经被改变了,下个线程是否能被阻塞; 释放锁:unlock()方法

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }

在加锁的时候,头部节点会经过两轮循环,第一次把head的状态变成-1,再经过判断head的状态为-1使得第一个线程可以执行加锁操作, 在解锁的的时候把head的状态从-1改为0,如果解锁失败,再继续通过两轮操作从新给线程加锁。

最新回复(0)