Skip to main content

重入锁ReentrantLock

huhxAbout 3 minjavaConcurrency-ToolkitConcurrency

之前介绍的synchronized关键字就是一种最简单的同步控制方法,而在JDK 5引入了重入锁与使用synchronized具有相同的基本行为,但具有扩展功能。现在我们来学习下重入锁。

重入锁的使用

重入锁使用java.util.concurent.locks.ReentrantLock类来实现。下面是一段最简单的重入锁使用案例:

public class Reenterlock implements Runnable {
    public static final Lock LOCK = new ReentrantLock();
    public static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 1000_000; j++) {
            LOCK.lock();
            try {
                i++;
            } finally {
                LOCK.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        var reenterlock = new Reenterlock();

        var t1 = new Thread(reenterlock);
        var t2 = new Thread(reenterlock);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i); // 2000000
    }
}







 



 
















 


上述代码8~12行,使用重入锁保护临界区资源i,确保多线程对i操作的安全性。与synchronized略有不同,我们需要手动的加锁和释放锁,这提供了更好的灵活性。

Warning

在退出临界区时,必须记得释放锁unlock(),否则其他线程就没有机会再访问临界区了。

为什么叫重入锁

之所以叫重入锁,是因为同一个线程是可以连续获得同一把锁。试想一下场景:同步代码直接或间接调用包含同步代码的方法,并且两组代码使用相同的锁。如果没有可重入同步,同步代码将必须采取许多额外的预防措施,以避免线程导致自身阻塞。

LOCK.lock();
LOCK.lock();
try {
    i++;
} finally {
    LOCK.unlock();
    LOCK.unlock();
}

在这种情況下,一个线程连续两次获得同一把锁。如果说一个线程多次获得了锁,那么也必须释放相同次数的锁。假如线程获得锁m次,释放锁n

  • m > n: 此线程仍旧持有这个锁,其他线程想进入临界区需要等待
  • m = n: 此线程不再持有这个锁,其他线程想进入临界区无需等待
  • m < n: 这里会有抛出IllegalMonitorStateException的异常

对于判断当前线程是否持有锁,或者说持有多少次锁,JDK提供了相应的方法:isHeldByCurrentThread()getHoldCount,下面给一个简单的例子:

public class Reenterlock implements Runnable {
    public static final ReentrantLock LOCK = new ReentrantLock();

    @Override
    public void run() {
        LOCK.lock();
        LOCK.lock();

        System.out.printf("HoldCount %d and isHeld %s.\n", LOCK.getHoldCount(), LOCK.isHeldByCurrentThread());

        LOCK.unlock();

        System.out.printf("HoldCount %d and isHeld %s.\n", LOCK.getHoldCount(), LOCK.isHeldByCurrentThread());
    }

    public static void main(String[] args) throws InterruptedException {
        var reenterlock = new Reenterlock();
        var t1 = new Thread(reenterlock);

        t1.start();

        System.out.printf("In main thread: HoldCount %d and isHeld %s.\n", LOCK.getHoldCount(), LOCK.isHeldByCurrentThread());

        t1.join();
    }
}

输出结果为:

In main thread: HoldCount 0 and isHeld false.
HoldCount 2 and isHeld true.
HoldCount 1 and isHeld true.

中断响应lockInterruptibly

对于synchronized来说,如果一个线程在等待锁,要么它获得这把锁继续执行,要么它就保持等待。而重入锁提供另外一种可能,那就是在获取锁的过程中线程可以被中断,从而取消对锁的请求。这主要得益于方法lockInterruptibly

下面给出一个使用lockInterruptibly解决死锁的例子:

限时等待锁申请

tryLock()方法

公平锁和非公平锁

public ReentrantLock (boolean fair)

FAQ

synchronized和ReentrantLock如何选择?

总结

参考