【搞定Java并发】5.3 重入锁 ReentrantLock

 

目录

1、重入锁ReentrantLock的基本概念:支持重进入的锁,它表示该线程能够支持一个线程对资源的重复加锁。除此之外,重入锁还支持获取锁时的公平和非公平性选择。

2、重入锁ReentrantLock的源码分析

2.1 获取锁和释放锁(lock和unlock)

调用默认无参构造器会将NonfairSync实例赋值给sync,此时锁是非公平锁,即Reentrant默认是非公平锁。有参构造器允许通过参数来指定是将FairSync实例还是NonfairSync实例赋值给sync。

NonfairSync和FairSync都是继承自Sync类并重写了lock()方法,所以公平锁和非公平锁在获取锁的方式上有些区别。

再来看看释放锁的操作,每次调用unlock()方法都只是去执行sync.release(1)操作,这步操作会调用AbstractQueuedSynchronizer类的release()方法

这个release()方法是AQS提供的释放锁操作的API,它首先会去调用tryRelease()方法去尝试获取锁,tryRelease()方法是抽象方法,它的实现逻辑在子类Sync里面。

这个tryRelease()方法首先判断如果不是持有锁的线程会抛异常,如果是持有锁,则会获取当前同步状态,并将当前同步状态减去传入的参数值得到新的同步状态,然后判断新的同步状态是否等于0**,如果等于0则表明当前锁被释放,然后先将锁的释放状态置为真,再将当前占有锁的线程清空,最后调用setState()方法设置新的同步状态并返回锁的释放状态。

2.2 公平锁和非公平锁

2.2.1 非公平锁的获取方式lock:线程第一步就会以CAS方式将同步状态的值从0改为1。即尝试获取锁,如果更改成功则表明线程刚来就获取了锁,而不必再去同步队列里面排队了。如果更改失败则表明线程刚来时锁还未被释放,所以接下来就调用AQS类中的acquire()方法【acquire调用NonfairSync重写的tryAcquire,这个方法再调用nonfairTryAcquire方法尝试去获取锁】。

nfairTryAcquire()方法是Sync的方法,线程进入此方法后首先去获取同步状态,如果同步状态为0就使用CAS操作更改同步状态,其实这又是获取了一遍锁。如果同步状态不为0表明锁被占用,此时会先去判断持有锁的线程是否是当前线程,如果是的话就将同步状态加1,否则的话这次尝试获取锁的操作宣告失败。于是acquire会调用addWaiter方法将线程添加到同步队列,然后acquireQueued会阻塞直到在获取到锁后才返回。

综上来看,在非公平锁的模式下一个线程在进入同步队列之前会尝试获取两遍锁,如果获取成功则不进入同步队列排队,否则才进入同步队列排队。

2.2.2 公平锁的获取方式

调用公平锁的lock方法时会直接调用acquire方法。acquire方法首先会调用FairSync重写的tryAcquire方法来尝试获取锁。在该方法中也是首先获取同步状态的值,如果同步状态为0则表明此时锁刚好被释放,这时和非公平锁不同的是它会先去调用 hasQueuedPredecessors() 方法查询同步队列中是否有人在排队,如果没人在排队才会去修改同步状态的值。如果同步状态不为0或者有人在排队,此时会先去判断持有锁的线程是否是当前线程,如果是的话就将同步状态加1,否则的话这次尝试获取锁的操作宣告失败。于是acquire会调用addWaiter方法将线程添加到同步队列,然后acquireQueued会阻塞直到在获取到锁后才返回。

综上所述,可以看到公平锁在进入同步队列之前只检查了一遍锁的状态,即使是发现了锁是开的也不会自己马上去获取,而是先让同步队列中的线程先获取,所以可以保证在公平锁下所有线程获取锁的顺序都是先来后到的,这也保证了获取锁的公平性。

2.2.3  非公平锁优点:由于线程的挂起和唤醒操作存在较大的开销而影响系统性能,特别是在竞争激烈的情况下公平锁将导致线程频繁的挂起和唤醒操作,而非公平锁可以减少这样的操作,所以在性能上将会优于公平锁

公平锁的优点:虽然公平锁的效率往往没有非公平的效率高,但公平锁能减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。




1、重入锁ReentrantLock的基本概念:支持重进入的锁,它表示该线程能够支持一个线程对资源的重复加锁。除此之外,重入锁还支持获取锁时的公平和非公平性选择。

所谓的公平与非公平指的是在请求先后顺序上,先对锁进行请求的就一定先获取到锁,那么这就是公平锁;反之,如果对于锁的获取并没有时间上的先后顺序,如后请求的线程可能先获取到锁,这就是非公平锁。

一般而言非,非公平锁机制的效率往往会胜过公平锁的机制,但在某些场景下,可能更注重时间先后顺序,那么公平锁自然是很好的选择。需要注意的是ReetrantLock支持对同一线程重加锁,但是加锁多少次,就必须解锁多少次,这样才可以成功释放锁。

ReetrantLock是基于AQS并发框架实现的。这里简单回顾下AQS的工作原理:

AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个 int 类型的成员变量 state 来控制同步状态,当 state=0 时,则说明没有任何线程占有共享资源的锁,当 state=1 时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。AQS内部通过内部类 Node 构成 FIFO 的同步队列来完成线程获取锁的排队工作,同时利用内部类 ConditionObject 构建等待队列,当 Condition 调用 wait() 方法后,线程将会加入等待队列中,而当 Condition 调用 signal() 方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。

AQS作为基础组件,对于锁的实现存在两种不同的模式,即共享模式(如Semaphore)和独占模式(如ReetrantLock),无论是共享模式还是独占模式的实现类,其内部都是基于AQS实现的,也都维持着一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点并将线程当前必要的信息存储到node结点中,然后加入同步队列等待获取锁,而这一系列操作都是AQS协助我们完成的。这也是AQS作为基础组件的原因,无论是Semaphore还是ReetrantLock,其内部绝大多数方法都是间接调用AQS完成的。

下面就看下ReentrantLock 和 AQS 之间的关系:

  • AbstractOwnableSynchronizer:抽象类,定义了存储独占当前锁的线程和获取的方法。
  • AbstractQueuedSynchronizer:抽象类,AQS框架核心类,其内部以虚拟队列的方式管理线程的锁获取与锁释放,其中获取锁(tryAcquire方法)和释放锁(tryRelease方法)并没有提供默认实现,需要子类重写这两个方法实现具体逻辑,目的是使开发人员可以自由定义获取锁以及释放锁的方式。
  • Node:AbstractQueuedSynchronizer 的内部类,用于构建虚拟队列(链表双向链表),管理需要获取锁的线程。
  • Sync:抽象类,是ReentrantLock的内部类,继承自AbstractQueuedSynchronizer,实现了释放锁的操作(tryRelease()方法),并提供了lock抽象方法,由其子类实现。
  • NonfairSync:是ReentrantLock的内部类,继承自Sync,非公平锁的实现类。
  • FairSync:是ReentrantLock的内部类,继承自Sync,公平锁的实现类。
  • ReentrantLock:实现了Lock接口的,其内部类有Sync、NonfairSync、FairSync,在创建时可以根据fair参数决定创建NonfairSync(默认非公平锁)还是FairSync。

ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync。其中Sync继承自AQS实现了解锁tryRelease()方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法。ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。

从上述类图可以看出AQS是一个抽象类,但请注意其源码中并没一个抽象的方法,这是因为AQS只是作为一个基础组件,并不希望直接作为直接操作类对外输出,而更倾向于作为基础组件,为真正的实现类提供基础设施。如构建同步队列,控制同步状态等,事实上,从设计模式角度来看,AQS采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作,

为什么这么做?这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做的好处是显而易见的,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可。

2、重入锁ReentrantLock的源码分析

2.1 获取锁和释放锁(lock和unlock)

先看下使用ReentrantLock加锁的示例代码。

public void doSomething() {
    // 默认是获取一个非公平锁
    ReentrantLock lock = new ReentrantLock();
    try{
        // 执行前先加锁
        lock.lock();   
        //执行操作...
    }finally{
        // 最后释放锁
        lock.unlock();
    }
}

以下是获取锁和释放锁这两个操作的API:


// 获取锁的操作
public void lock() {
    sync.lock();
}
// 释放锁的操作
public void unlock() {
    sync.release(1);
}

可以看到获取锁和释放锁的操作分别委托给Sync对象的lock方法和release方法。

public class ReentrantLock implements Lock, java.io.Serializable {
 
    private final Sync sync;
 
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
    }
 
    // 实现非公平锁的同步器
    static final class NonfairSync extends Sync {
        final void lock() {
            ...
        }
    }
 
    / /实现公平锁的同步器
    static final class FairSync extends Sync {
        final void lock() {
            ...
        }
    }
}

每个ReentrantLock对象都持有一个Sync类型的引用,这个Sync类是一个抽象内部类它继承自AbstractQueuedSynchronizer,它里面的lock方法是一个抽象方法。ReentrantLock的成员变量sync是在构造时赋值的,下面我们看看ReentrantLock的两个构造方法都做了些什么?

// 默认无参构造器
public ReentrantLock() {
    sync = new NonfairSync();
}
 
// 有参构造器
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

调用默认无参构造器会将NonfairSync实例赋值给sync,此时锁是非公平锁,即Reentrant默认是非公平锁。有参构造器允许通过参数来指定是将FairSync实例还是NonfairSync实例赋值给sync。

NonfairSync和FairSync都是继承自Sync类并重写了lock()方法,所以公平锁和非公平锁在获取锁的方式上有些区别。

再来看看释放锁的操作,每次调用unlock()方法都只是去执行sync.release(1)操作,这步操作会调用AbstractQueuedSynchronizer类的release()方法

// 释放锁的操作(独占模式)
public final boolean release(int arg) {
    // 拨动密码锁, 看看是否能够开锁
    if (tryRelease(arg)) {
        // 获取head节点
        Node h = head;
        // 如果head结点不为空并且等待状态不等于0就去唤醒后继节点
        if (h != null && h.waitStatus != 0) {
            // 唤醒后继节点
            unparkSuccessor(h);
        }
        return true;
    }
    return false;
}

这个release()方法是AQS提供的释放锁操作的API,它首先会去调用tryRelease()方法去尝试获取锁,tryRelease()方法是抽象方法,它的实现逻辑在子类Sync里面。

abstract static class Sync extends AbstractQueuedSynchronizer {
 
    ...
 
    // 尝试释放锁
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        // 如果持有锁的线程不是当前线程就抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new IllegalMonitorStateException();
        }
        boolean free = false;
        // 如果同步状态为0则表明锁被释放
        if (c == 0) {
            // 设置锁被释放的标志为真
            free = true;
            // 设置占用线程为空
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
 
    ...
}

这个tryRelease()方法首先判断如果不是持有锁的线程会抛异常,如果是持有锁,则会获取当前同步状态,并将当前同步状态减去传入的参数值得到新的同步状态,然后判断新的同步状态是否等于0**,如果等于0则表明当前锁被释放,然后先将锁的释放状态置为真,再将当前占有锁的线程清空,最后调用setState()方法设置新的同步状态并返回锁的释放状态。

2.2 公平锁和非公平锁

如果是公平锁,线程将按照它们发出请求的顺序来获得锁;

但在非公平锁上,则允许插队行为:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程可以跳过队列中所有等待的线程直接获得这个锁。

2.2.1 非公平锁的获取方式lock:线程第一步就会以CAS方式将同步状态的值从0改为1。即尝试获取锁,如果更改成功则表明线程刚来就获取了锁,而不必再去同步队列里面排队了。如果更改失败则表明线程刚来时锁还未被释放,所以接下来就调用AQS类中的acquire()方法【acquire调用NonfairSync重写的tryAcquire,这个方法再调用nonfairTryAcquire方法尝试去获取锁】。

// 非公平锁的获取
static final class NonfairSync extends Sync {
    // 实现父类的抽象获取锁的方法
    final void lock() {
        // 使用CAS方式设置同步状态
        if (compareAndSetState(0, 1)) {
            // 如果设置成功则表明锁没被占用
            setExclusiveOwnerThread(Thread.currentThread());
        } else {
            // 否则表明锁已经被占用, 调用acquire让线程去同步队列排队获取
            // acquire(1)是AQS类中的方法
            acquire(1);
        }
    }
    // 尝试获取锁的方法
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires); //*********
    }
}
 
// 调用的是AQS中的方法:以不可中断模式获取锁(独占模式) ***********
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}

nfairTryAcquire()方法是Sync的方法,线程进入此方法后首先去获取同步状态,如果同步状态为0就使用CAS操作更改同步状态,其实这又是获取了一遍锁。如果同步状态不为0表明锁被占用,此时会先去判断持有锁的线程是否是当前线程,如果是的话就将同步状态加1,否则的话这次尝试获取锁的操作宣告失败。于是acquire会调用addWaiter方法将线程添加到同步队列,然后acquireQueued会阻塞直到在获取到锁后才返回。

综上来看,在非公平锁的模式下一个线程在进入同步队列之前会尝试获取两遍锁,如果获取成功则不进入同步队列排队,否则才进入同步队列排队。

abstract static class Sync extends AbstractQueuedSynchronizer {
 
    ...
 
    // 非公平的获取锁
    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取当前同步状态
        int c = getState();
        // 如果同步状态为0则表明锁没有被占用
        if (c == 0) {
            // 使用CAS更新同步状态
            if (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;
    }
 
    ...
}

2.2.2 公平锁的获取方式

调用公平锁的lock方法时会直接调用acquire方法。acquire方法首先会调用FairSync重写的tryAcquire方法来尝试获取锁。在该方法中也是首先获取同步状态的值,如果同步状态为0则表明此时锁刚好被释放,这时和非公平锁不同的是它会先去调用 hasQueuedPredecessors() 方法查询同步队列中是否有人在排队,如果没人在排队才会去修改同步状态的值。如果同步状态不为0或者有人在排队,此时会先去判断持有锁的线程是否是当前线程,如果是的话就将同步状态加1,否则的话这次尝试获取锁的操作宣告失败。于是acquire会调用addWaiter方法将线程添加到同步队列,然后acquireQueued会阻塞直到在获取到锁后才返回。

综上所述,可以看到公平锁在进入同步队列之前只检查了一遍锁的状态,即使是发现了锁是开的也不会自己马上去获取,而是先让同步队列中的线程先获取,所以可以保证在公平锁下所有线程获取锁的顺序都是先来后到的,这也保证了获取锁的公平性。

// 调用的是AQS中的方法:以不可中断模式获取锁(独占模式) ***********
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}



// 实现公平锁的同步器
static final class FairSync extends Sync {
    // 实现父类的抽象获取锁的方法
    final void lock() {
        // 调用acquire让线程去同步队列排队获取
        acquire(1);
    }

    // 尝试获取锁的方法
    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);
            return true;
        }
        // 如果不是当前线程持有锁则获取失败
        return false;
    }
}

2.2.3  非公平锁优点:由于线程的挂起和唤醒操作存在较大的开销而影响系统性能,特别是在竞争激烈的情况下公平锁将导致线程频繁的挂起和唤醒操作,而非公平锁可以减少这样的操作,所以在性能上将会优于公平锁

  另外,由于大部分线程使用锁的时间都是非常短暂的,而线程的唤醒操作会存在延时情况,有可能在A线程被唤醒期间B线程马上获取了锁并使用完释放了锁,这就导致了双赢的局面,A线程获取锁的时刻并没有推迟,但B线程提前使用了锁,并且吞吐量也获得了提高。

公平锁的优点:虽然公平锁的效率往往没有非公平的效率高,但公平锁能减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

猜你喜欢

转载自blog.csdn.net/ZHAOJING1234567/article/details/89415975