ReentrantLock

公正锁和非公正锁

这个类是接口 Lock的完成类,也是失望锁的一种,可是它供给了 lockunlock办法用于主动进行锁的加和拆。在之前运用的 sychronized关键字是隐式加锁机制,而它是显现加锁,一起,这个类的结构办法供给了公正和非公正的两种机制。

什么是公正和非公正呢?便是多线程对共享资源进行抢夺的时分,会呈现一个线程或几个线程完全占有共享资源,使得某些线程在长时间处于等候状况。公正便是要等候时间过长的线程先取得锁。

而在 ReentrantLock类中,供给了公正锁和非公正锁的运用。

ReentrantLock源码中,结构器供给了一个参数入口,

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

当fair为true的时分,会创造一个 FairSync目标给 sync特色,FairSync是承继自 Sync的类,其中有一个 Lock办法,而在 ReentrantLock的Lcok中运用的是 sync特色的 Lock办法,故能够确保“公正”。

运用非公正锁就不需求在结构器中传参数。

在运用的时分,需求手动上锁和解锁。

运用公正锁,会将占优势的线程进行约束,康复挂起的线程,可是这个过程在CPU层面来讲,是存在显着时间差异的,非公正锁的履行功率相对更高,所以一般来说不建议运用公正锁,除非现实业务上需求符合实际需求。

重入锁

ReentrantLock自身还支撑重入的功用。

重入锁(Reentrant Lock)是一种支撑重入的独占锁,它允许线程屡次获取同一个锁,在开释锁之前有必要相应地屡次开释锁。重入锁通常由两个操作组成:上锁(lock)和解锁(unlock)。当一个线程获取了重入锁后,能够再次获取该锁而不被堵塞,一起有必要经过相同数量的解锁操作来开释锁。

重入锁具有如下特色:

  1. 重入性:重入锁允许同一个线程屡次获取同一把锁,避免了死锁的发生。
  2. 独占性:与公正锁和非公正锁一样,重入锁也是一种独占锁,同一时刻只能有一个线程持有该锁。
  3. 可中止性:重入锁支撑在等候锁的过程中中止该线程的履行。
  4. 条件变量:在运用 java.util.concurrent.locks.Condition 类配合重入锁完成等候/通知机制时,等候状况总是与重入锁相关联的。

重入锁相关于 synchronized 关键字的优势在于,重入锁具有更高的灵活性和扩展性,支撑公正锁和非公正锁、可中止锁和可轮询锁等特性,能够更好地满足多线程环境下的并发操控需求。synchroized也有重入性。

ReentrantLock lock = new ReentrantLock(true);
public void get(){
    while(true){
        try{
            lock.lock();
            lock.lock();
        }catch(Exception exception){
        }finally{
            lock.unlock();
            lock.unlock();
        }
    }
}

可重入的前提 lock是同一个目标,而关键字 synchroizedMonitor也是同一个目标充任,才干断定为重入。

public void get(){
    while(true){
        synchronized(this){
            System.out.println("外层");
            synchronized(this){
                System.out.println("内层");
            }
        }
    }
}

那么Java是怎么检测锁的重入和获取锁的次数的呢?在之前说过的 ObjectMobitor的C++源代码中有 _recursions和_count来记录锁的重入次数和线程获取锁的次数。这样在Java层面就表示一个锁目标都具有一个锁计数器 _count和一个指向持有这个锁的线程的指针 _owner只要当时持有锁的线程才干使得计数器+1,其他线程只要等候锁被开释(计数器置0)才干持有并+1。

在源码中,非公正锁的lock办法如下:

//ReentrantLock类中:
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
//0的参数为是expect,是希望值,而1是update,是更新值

在履行comparaAndSetState办法的时分,它会问询锁的计数器(在底层履行compareAndSwapInt的本地办法),并希望数值为0,假如为0返回true,然后设置履行线程主是当时线程。假如非0,那么他就会履行acquire

//AbstractQueuedSynchronizer类中:
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//这里的tryAcquire,需求在其承继的子类中进一步完成对应的功用
//子类能够根据自己的需求重新定义tryAcquire(int arg)的完成方式,然后完成更优秀的锁操控方案:
//而在其子类FairSync中便覆盖了这个办法
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

将线程放入等候行列。

一起计数器是经过 unlock来-1,所以 lock和unlock次数不匹配就会发生死锁,也便是当两个线程调用同一个 ReentrantLock假如一个线程中的上锁解锁次数不相等,那么计数器没有被清零,当另一个线程恳求锁的时分,看到锁计数器不是0,就认为被的线程仍然持有它,所以一向等候它被开释。需求了解底层的能够去看AQS中的release办法。

而在 ReentrantLock中有一个抽象内部类 Sync,它承继自抽象类AbstractQueuedSynchronizer(简称AQS),这个类中有一个内部 Node类,当有线程等候这把锁的时分,会创建一个等候行列,放置这些处于等候的线程。(AQS完成比较复杂,有兴趣能够看看“竹子爱熊猫”大佬的文章。)

小结

ReentrantLock类中,有内部类三个,Sync,FairSync,NonfairSync,他们的联系是Sync是后两个的父类,后两个是兄弟类,一起Sync承继自AQS类,在AQS中有很多完成公正和非公正、可重入的机制,而具体完成作用的是Sync,FairSync,NonfairSync

疑问

在下列代码中,为什么在第一个线程的最终加上.join(),没有使得线程堵塞,而没有它就会堵塞?

Lock lock = new ReentrantLock();
new CompletableFuture().runAsync(() -> {
    lock.lock();
    try{
        System.out.println(1);
        TimeUnit.SECONDS.sleep(2);
    }catch(Exception e){
    }finally{
}});
//上面加上.join()
new CompletableFuture().runAsync(() -> {
    lock.lock();
    try{
        System.out.println(2);
    }catch(Exception e){
    }finally{
        lock.unlock();
}}).join();