首页 > 编程笔记

Java ReentrantLock重入锁详解

重入锁(ReentrantLock)是对 synchronized 的升级,synchronized 是通过 JVM 实现的,ReentrantLock 是通过 JDK 实现的。

重入锁指的是可以给同一个资源添加多个锁,并且解锁的方式与 synchronized 也有不同。synchronized 的锁是线程执行完毕之后会自动释放的,ReentrantLock 的锁必须手动释放,可以通过 ReentrantLock 实现访问量统计。

举个简单的例子:
public class Account implements Runnable{
    private static int num;
    private ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        // TODO Auto-generated method stub
        reentrantLock.lock();
        num++;
        System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
        reentrantLock.unlock();
    }
}
首先需要实例化 ReentrantLock 的成员变量,在业务方法中需要加锁的地方直接调用对象的 lock() 方法即可,同理需要解锁的地方直接调用对象的 unlock() 方法即可,运行程序,结果是:

线程1是当前的第1位访客
线程2是当前的第2位访客

效果与 synchronized 一样,在此基础之上可以添加多把锁,只需要多次调用 lock() 方法即可,比如:
public class Account implements Runnable{
    private static int num;
    private ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        // TODO Auto-generated method stub
        reentrantLock.lock();
        reentrantLock.lock();
        num++;
        System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
        reentrantLock.unlock();
        reentrantLock.unlock();
    }
}

我们说过 ReentrantLock 需要手动解锁,如果我们只加锁而不手动解锁:
public class Account implements Runnable{
    private static int num;
    private ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        // TODO Auto-generated method stub
        reentrantLock.lock();
        reentrantLock.lock();
        num++;
        System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
        reentrantLock.unlock();
    }
}
运行程序,结果如下图所示:


图 1 程序发生阻塞

可以看到这里只输出了第 1 位访客信息,然后程序就静止了,一直处于运行状态但是不会自动结束。原因就是线程 1 进来,加了两把锁。执行完线程 1 的输出语句之后,只释放了一把锁。线程 2 永远无法获得第 2 把锁,就一直处于阻塞状态,程序也就一直卡在这里了,所以使用重入锁的时候需要注意加了几把锁就必须释放几把锁。

ReentrantLock 除了可重入之外,还有一个可中断的特点,可中断是指某个线程在等待获取锁的过程中可主动终止线程,通过调用对象的 lockInterruptibly() 来实现。

假设这样一个场景,线程 1 和线程 2 并行访问某个方法,该方法执行完成需要 5000ms。肯定会有一个线程先拿到锁,然后另外一个线程就进入阻塞状态。现在设置当线程启动 1000ms之后,没有拿到锁的线程自动中断,具体实现如下:
public class StopLock implements Runnable {
public ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            reentrantLock.lockInterruptibly();
            System.out.println(Thread.currentThread().getName()+" get lock");
            Thread.currentThread().sleep(5000);
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        StopLock lock = new StopLock();
        Thread t1 = new Thread(lock, "线程1");
        t1.start();
        Thread t2 = new Thread(lock, "线程2");
        t2.start();
        try {
            Thread.currentThread().sleep(1000);
            t2.interrupt();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
运行程序,结果如下图所示:


图 2 程序自动中断

ReentrantLock 还具备限时性的特点,指可以判断某个线程在一定的时间内能否获取锁,通过调用 tryLock(long timeout, TimeUnit unit) 方法来实现。其中 timeout 指时间数值,unit 指时间单位,返回值为 boolean 类型,true 表示在该时间段内获取了锁,false 表示在该时间段内没有获取锁,具体实现如下:
public class TimeLock implements Runnable{
    public ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            if(reentrantLock.tryLock(3, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName()+"get lock");
                Thread.currentThread().sleep(5000);
            }else {
                System.out.println(Thread.currentThread().getName()+"not lock");
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(reentrantLock.isHeldByCurrentThread()) {
                reentrantLock.unlock();
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        TimeLock lock = new TimeLock();
        new Thread(lock,"线程1").start();
        new Thread(lock,"线程2").start();
    }
}
线程 1 和线程 2 并行访问,业务方法的执行需要 5000ms。reentrantLock.tryLock(3, TimeUnit.SECONDS) 表示如果线程启动之后的 3s 内该线程没有拿到锁,则返回 false,反之返回 true。

很显然 5000ms 大于 3s,会有一个线程是肯定拿不到锁的,运行程序,结果为:

线程1get lock
线程2not lock

线程 1 拿到了锁,线程 2 没有拿到锁,因为它的等待时间为 3s,而线程 1 从拿到锁到释放锁需要 5s,现在对程序进行修改:
public class TimeLock implements Runnable{
    public ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            if(reentrantLock.tryLock(6, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName()+"get lock");
                Thread.currentThread().sleep(5000);
            }else {
                System.out.println(Thread.currentThread().getName()+"not lock");
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(reentrantLock.isHeldByCurrentThread()) {
                reentrantLock.unlock();
            }
        }
    }
}
reentrantLock.tryLock(6, TimeUnit.SECONDS) 表示如果线程启动之后的 6s 内该线程没有拿到锁,则返回 false,反之返回 true,运行程序,结果为:

线程1get lock
线程2get lock

线程 1 拿到了锁,线程 2 也拿到了锁,因为等待时间为 6s,线程 1 从获取锁到释放锁的时间是 5s。

推荐阅读