ReentrantLock与synchronized比较分析

ReentrantLock:完成了Lock接口,是一个可重入锁,并且支持线程公正竞赛和非公正竞赛两种形式,默认情况下对错公正形式。ReentrantLock算是synchronized的补充和替代计划。

公正竞赛:遵照先来后到的规则,先到先得
非公正竞赛:正常情况下是先到先得,可是答应见缝插针。即持有锁的线程刚开释锁,等候行列中的线程刚准备获取锁时,突然半路杀出个程咬金,抢到了锁,等候行列中等候获取锁的线程只能干瞪眼,接着等抢到锁的线程开释锁

ReentrantLock与synchronized比较:
  1、ReentrantLock底层是经过将堵塞的线程保存在一个FIFO行列中,synchronized底层是堵塞的线程保存在锁目标的堵塞池中
  2、ReentrantLock是经过代码机制进行加锁,所以需求手动进行开释锁,synchronized是JAVA关键字,加锁和开释锁有JVM进行完成
  3、ReentrantLock的加锁和开释锁必须在办法体内履行,可是能够不必同一个办法体,synchronized能够在办法体内作为办法块,也能够在办法声明上
  4、synchronized进行加锁时,假如获取不到锁就会直接进行线程堵塞,等候获取到锁后再往下履行。ReentrantLock既能够堵塞线程等候获取锁,也能够设置等候获取锁的时刻,超越等候获取时刻就放弃获取锁,不再堵塞线程

ReentrantLock源码剖析:

仿制代码
/** Synchronizer providing all implementation mechanics */ private final Sync sync; /** * Base of synchronization control for this lock. Subclassed
 * into fair and nonfair versions below. Uses AQS state to
 * represent the number of holds on the lock. */ abstract static class Sync extends AbstractQueuedSynchronizer {
    ....
} /** * Sync object for non-fair locks */ static final class NonfairSync extends Sync {
    ....
} /** * Sync object for fair locks */ static final class FairSync extends Sync {
    ....
}
仿制代码

 

 

 从省略细节的源码中咱们能够很明晰的看到,ReentrantLock内部定义了三个内部类,全部直接或许间接的承继AbstractQueuedSynchronizer,ReentrantLock有一个特点sync,默认情况下为NonfairSync类型,即为非公正锁。实际上ReentrantLock的所有操作都是有sync这个特点进行的,ReentrantLock仅仅一层外皮。

ReentrantLock已然完成类Lock接口,咱们就先以加锁、解锁进行剖析:
1、加锁(非公正):

 public void lock() {
        sync.lock();
}

就像上面说的相同,ReentrantLock的功用都是靠底层sync进行完成,ReentrantLock的加锁很简略,就一句话运用sync的lock()办法,咱们直接看源码

仿制代码
final void lock() { if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);
} protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
} protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}
仿制代码

sync的lock()办法也是比较简略的,先经过CAS试图将AQS的state由0变为1。假如成功,表明该线程获取锁成功(设值时没有线程在持有锁),就将当时线程设置为锁的拥有者(这便是前面所说得见缝插针,后边还有);假如失利,只表明设置值没有成功,不表明该线程获取锁失利(由于有或许是重入加锁),开端调用AQS的acquire(int)办法进行获取锁,咱们直接看acquire(int)办法

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

这是AQS定义的模板办法,由AQS的子类去重写tryAcquire(int) 办法,由AQS去调用,进行测验获取锁。假如获取锁成功,办法直接完毕;假如获取锁失利,就将当时线程进行堵塞并加入到FIFO的CLH行列中。咱们先看ReentrantLock内部类是如何重写tryAcquire(int)办法的

仿制代码
protected final boolean tryAcquire(int acquires) {
  // 直接调用父类nonfairTryAcquire(int)办法 return nonfairTryAcquire(acquires);
} final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current); return true;
        }
    } 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;
}
仿制代码

咱们直接看sync声明的nonfairTryAcquire(int)办法,先获取保存的state,假如值为0,表明当时没有线程持有锁,处理和前面的一致,直接见缝插针,经过CAS测验直接设置值,设值失利就表明获取锁失利了,直接回来失利成果;假如state不是0,表明锁被线程持有,就比较下持有锁的线程是否是当时线程,假如是,表明线程重入持有锁,进行state值的累加。假如不是,直接回来持有锁失利成果。tryAcquire(int)办法获取锁失利后,会去履行AQS声明的acquireQueued(Node, int)办法将当时线程封装到CLH行列的节点Node中,并进行堵塞。咱们先看看AQS是将当时线程封装到Node中都做了什么操作

仿制代码
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; // 将队尾的node改为新建的node if (compareAndSetTail(pred, node)) {
            pred.next = node; return node;
        }
    }
    enq(node); return node;
}
仿制代码

将当时线程装进Node实例中,并设置改Node为独享形式。然后获取队尾的Node实例,由于走到这一步的时分有两种获取锁失利的场景:1、竞赛锁时,没有线程持有锁;2、竞赛锁时,已有其他线程持有锁。所以会先判别下队尾Node是否为null,假如为空,表明是第一种场景获取锁失利,假如不为空,这是第二种场景获取锁失利。先看第2种场景的处理流程,直接将队尾的Node设置为新建Node实例的prev(表明在行列中的前一个Node节点),然后经过CAS测验将新建的Node节点设置为队尾(这个时分用CAS是由于有或许存在多线程竞赛),假如设置队尾成功,就将上一任队尾的next节点设置为新建的node节点;假如设置队尾失利(多线程竞赛才会出现),和场景1进行相同的处理,先看源码

仿制代码
private Node enq(final Node node) { for (;;) {
        Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t; if (compareAndSetTail(t, node)) {
                t.next = node; return t;
            }
        }
    }
}
仿制代码

处理很简略,先获取队尾,假如获取的队尾为null,表明上一步场景1过来的,经过CAS将队首设置为一个空的Node节点(该节点表明正在持有锁的线程封装的Node,仅仅是代表,没有实值),并将队尾和队首指向同一个节点;假如获取的队尾不为null,将队尾设置为参数Node的上一个节点,并经过CAS测验将参数Nodo设置为队尾,假如设置成功,将新队尾设置为上一任队尾的next节点,并直接回来;假如设置失利,往复循环,直到成功停止。

经过前面对addWaiter(Node)源码的剖析,咱们能够清楚的了解到addWaiter办法将当时线程封装到CLH行列的独享形式的Node节点中,并经过CAS将当时线程的Node节点设置为队尾。下面咱们接着看acquireQueued(final Node node, int arg)办法会做什么处理

仿制代码
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节点的上一任节点,假如上一任节点是队首时,会去再次调用tryAcquire(int)办法去测验持有锁,假如成功持有锁,就将参数Node直接设置为队首,一起将上一任队首的next节点设置为null(去除引证,利于GC),然后直接回来当时线程是否要进行中止操作。假如上一任节点不是队首或许再次测验持有锁失利,会先调用shouldParkAfterFailedAcquire(Node pre, Node node)进行判别是否需求进行线程堵塞,假如需求线程堵塞再调用parkAndCheckInterrupt()进行线程堵塞(该办法回来值表明该线程是否是中止状况)。线程堵塞后会等候上一任节点开释锁时唤醒完毕堵塞,rar密码破解线程完毕堵塞后会循环再次去获取锁。可是假如完毕堵塞后去获取锁时,有新的线程见缝插针直接获取到锁了,那就只能再次在行列中进行堵塞了。其实shouldParkAfterFailedAcquire和parkAndCheckInterrupt看办法名称就能猜个大约了,咱们仍是直接先看shouldParkAfterFailedAcquire(Node pre, Node 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 { /* * 等候状况必须为0或传达。将节点状况设置为SIGNAL,告知上一任节点后边节点需求开释信号告知,但先不进行堵塞。呼叫者将需求重试,以确保它不能在堵塞前取得锁。 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    } return false;
}
仿制代码

先获取上一任节点的waitStatus(等候状况),1.假如上一任节点的等候状况为Node.SIGNAL(表明后边的节点需求上一任节点开释锁时进行告知,完毕后边节点的堵塞),直接回来true,履行后边的堵塞流程;2.1假如上一任节点的等候转态值大于0,表明上一任节点被撤销了(抢夺锁的线程因过了设定时刻,获取锁失利,从行列中删去节点的时分,Node节点会被设为撤销状况),跳过上一任节点,往前找,直到找到不是撤销状况的节点,直接将找到的有效上一任节点的next节点设置为当时节点;2.2假如上一任节点不是撤销状况(或许是初始值0、等候状况或许传达状况),经过CAS测验将上一任节点的waitStatus设置为Node.SIGNAL(不论设置成功与否,都直接回来false,后边会再次循环履行,用来确保该节点的线程在堵塞前必定不会获取到锁,由于存在见缝插针去争取持有锁的线程)。shouldParkAfterFailedAcquire办法回来true,会进行线程堵塞,回来false,调用层会进行循环让当时线程再次获取一次锁,失利后再次被调用进行清洗现已撤销的节点或许进行上一任节点的等候状况设置为Node.SIGNAL。

下面咱们再来看看parkAndCheckInterrupt()办法的源码

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); return Thread.interrupted();
}

parkAndCheckInterrupt()办法的内容很简略,运用LockSupport的park(Object blocker)办法进行线程堵塞,有爱好的同学能够自行去深化了解,等候上一任节点调用调用LockSupport的unpark(Thread thread)唤醒该线程。然后在该线程完毕堵塞后回来该线程是否是中止的状况。

至此,非公正锁获取锁的流程就剖析完了,总结下非公正锁加锁流程

  1.不论线程有没有被持有,先测验获取锁
  2.锁未被持有,直接获取锁。2.1获取锁成功,完毕;2.2获取锁失利,封装成CLH行列的Node节点,并进行线程堵塞
  3.锁被持有。3.1线程重入加锁,进行state累加,等候开释锁时进行减除;3.2封装成CLH行列的Node节点,并进行线程堵塞

2、定长时刻加锁(非公正)

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

没什么可说的,依然是运用sync进行操作,咱们直接看sync的完成源码

仿制代码
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
仿制代码

这儿直接先判别当时线程是否是中止状况(或许是由于ReentrantReadWriteLock中的WriteLock也会运用才进行判别),假如是中止状况,直接抛反常。咱们这边必定不会是中止状况的啦,接着往下走,调用tryAcquire(int)办法测验获取锁,忘了过程的同学请往前翻。假如获取锁成功,那就直接完毕。获取锁失利时,履行doAcquireNanos(int arg, long nanosTimeout)办法以独占定时形式获取锁。咱们直接看源码

仿制代码
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC failed = false; return true;
            }
            nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException();
        }
    } finally { if (failed)
            cancelAcquire(node);
    }
}
仿制代码

相同是先创立一个Node节点并放置到CLH行列的队尾,然后相同的是开端进行循环处理,不同的地方是,定长获取锁运用了LockSupport.pargNanos(Object blocker, long nanos)进行堵塞一段时刻,假如在线程堵塞未自动完毕时,前一个节点开释了锁,该节点相同会被免除堵塞去抢夺锁,假如不幸被其他线程见缝插针抢去了锁,那就接着去堵塞定长时刻(这个定长时刻是依据开始设定的时刻和当时时刻的差值),等候锁的开释。假如超越了定长时刻仍是没有获取到锁,就会调用cacelAcquire(Node node)去删去该节点。

3、公正竞赛加锁

公正锁目标FairSync的lock()办法直接调用了acquire(int)办法,前面咱们剖析了,acquire(int)办法会先调用tryAcquire(int)办法去测验获取锁,依据回来成果去判别是否需求加入到行列中。下面咱们直接FairSync的源码

仿制代码
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final 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) { 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;
    }
}
仿制代码

源码的逻辑比较明晰简略,破解rar先判别当时锁是否是闲暇状况(state是否是0),假如是闲暇的,就去测验获取锁,回来抢夺成果;假如不是闲暇的,判别是否是线程重入,假如是就累加state,回来成功,假如不是重入,直接回来失利。假如tryAcquire办法回来了false,那么就会将该线程封装到CLH行列的Node中并进行线程堵塞,后边的流程和非公正锁时一致的。

总结下公正锁加锁的流程
  1.锁未被持有,测验直接获取锁。2.1获取锁成功,完毕;2.2获取锁失利,封装成CLH行列的Node节点,并进行线程堵塞
  2.锁被持有。2.1线程重入加锁,进行state累加,等候开释锁时进行减除;2.2封装成CLH行列的Node节点,并进行线程堵塞

4、开释锁:

public void unlock() {
    sync.release(1);
}

ReentrantLock的开释锁直接调用的sync特点的release(int)办法,实际是直接调用的AbstractQueuedSynchronizer的release(int)办法,咱们直接看源码

仿制代码
public final boolean release(int arg) { if (tryRelease(arg)) {
        Node h = head; if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); return true;
    } return false;
}
仿制代码

依然是相同的配方,调用子类重写的tryRelease(int)办法去真实开释锁,假如开释成功,而且行列中有节点在等候队首开释锁后进行告知,就会调用unparkSuccessor(Node node)去免除下一个节点的线程堵塞状况,让下一个线程去获取锁。

咱们先看看子类重写的tryRelease(int)办法

仿制代码
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;
}
仿制代码

源码比较简略,先比较当时线程是否是持有锁的线程,假如不是,直接抛反常,阐明调用者运用不标准,没有先去获取锁。然后进行state的减除,先判别此刻state是否为0,为0表明线程彻底开释了锁。假如为0,就将锁的持有者变为null。不论最后有没有彻底开释锁都会将state设置成新值。

咱们再看看unparkSuccessor(Node node)都做了什么

仿制代码
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    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)
        LockSupport.unpark(s.thread);
}
仿制代码

此刻参数node代表的head,假如head的waitStatus小于0,表明后边节点需求在等候告知锁被开释的信号,先将head的waitStatus改为0,然后去看看head的next节点是存在并且next节点的waitStatus小于0,。假如next节点为null或许waitStatus大于0,就从队尾tail节点顺次往前找,找到head节点后第一个waitStatus不大于0的节点,然后完毕该节点的线程堵塞状况;假如head的next节点存在并且waitStatus不大于0,直接免除head的next节点线程堵塞状况。

总结下锁开释过程:
  1.先判别当时线程是否能够进行锁开释
  2.state减除,假如减除后的state为0,就将锁的持有者设为空,并免除下一个等候节点的线程堵塞状况

为了加深印象,我专门还花了三个流程图,一起看看

到这儿基本上ReentrantLock的主要功用点都说完了,还有一个Condition功用没说,接着搞起来。

ReentrantLock的Condition:

仿制代码
ReentrantLock办法: public Condition newCondition() { return sync.newCondition();
}

Sync内部类办法: final ConditionObject newCondition() { return new ConditionObject();
}
仿制代码

仍是自始自终的配方,ReentrantLock的newCondition依然用的是sync特点去完成功用,Sync也简略,直接便是创立一个AbstractQueuedSynchronizer的内部列ConditionObject的实例。咱们直接去看ConditionObject的源码去剖析

Condition主要是wait()、signal()两个办法,咱们先来看wait()办法

仿制代码
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException();
    Node node = addConditionWaiter(); int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) {
        LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;
    } if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled  unlinkCancelledWaiters(); if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
仿制代码

剖析之前先阐明下,ConditionObject中相同保存的有CLH行列,和外部类AbstractQueuedSynchronizer相似。

咱们经过await()办法的源码简略剖析下:
  1、首要调用了addConditionWaiter()办法将当时线程封装到Node.CONDITION形式的Node中,放入到ConditionObject的CLH行列中,顺便去除了一些行列中waitStatus不是Node.CONDITION的节点。
  2、调用fullyRelease(Node node)去彻底开释掉锁,并去免除AbstractQueuedSynchronizer中行列的head节点,办法回来节点彻底开释前的state值
  3、循环:对当时节点进行线程堵塞,直到被其他线程运用signal()或许signalAll()办法免除线程堵塞状况
  4、将节点加入到AbstractQueuedSynchronizer的CLH行列中,等候抢夺锁

全体上Condition的wait()办法的内容就这么多,源码也比较简略,有爱好的能够自己深化看看。

现在看下signal()的源码:

仿制代码
public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException();
    Node first = firstWaiter; if (first != null)
        doSignal(first);
} private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
} final boolean transferForSignal(Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false;

    Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread); return true;
}
仿制代码

signal()办法的主要作业仍是放在了doSignal(Node first)办法和transferForSignal(Node node)两个办法上。在doSignal(Node first)办法中,获取到ConditionObject的CLH行列的队首,然后调用transferForSignal(Node node)办法先将节点的waitStatus改为0,然后将节点放入到AbstractQueuedSynchronizer的CLH行列队尾,假如上一任队尾的waitStatus大于0或许将上一任队尾的waitStatus改为Node.SIGNAL失利时,直接免除节点的线程堵塞状况,完毕wait()办法中的循环,调用acquireQueued(Node node, int savedState)去测验抢夺锁,由于此刻当时线程仍然持有锁,所以节点最后仍是会被线程堵塞。由于此刻节点node现已从ConditionObject的CLH行列搬迁到了AQS的CLH行列队尾,即使if条件不满足,不能免除node节点的线程堵塞状况,比及上一任队尾节点开释锁时仍是会免除node节点的线程堵塞状况。

Condition还有await(long time, TimeUnit unit)、signalAll()等等其它办法,原理差不多,这儿就不一一赘述了。至此,ReentrantLock的知识点基本上也说的差不多了。

转载请保留 https://blog.csdn.net/dafengit/article/details/106073709

posted @ 2020-05-13 14:54  ITPS  阅读(339)  评论(0编辑  收藏  举报