ReentrantLock

在面对一些线程安全问题时,需要加入一把锁来进行防范,而 synchronized 就是这方面的代表之一。可能你会有所疑惑,既然有了 synchronized 这把锁,为什么JDK中还会有 Lock 的定义呢?其实这个还和 synchronized 的历史背景有关。

在 JDK 早期版本中,synchronized 关键字性能确实不佳,每一次加锁都需要和操作系统内核打交道,所以后来 JDK 作者索性自己写了一个Lock接口,减少和内核态之间的交道,降低这种性能的损耗。

但是自从 JDK1.6 以后,JDK 作者对 synchronized 进行了优化之后,synchronized 又开始被推荐使用,何况性能问题通过优化,大多都是可以解决的,所以性能并不是Lock接口出现的原因。

其实 Lock 接口至今一直被大众所喜爱,主要不在于说它的性能方面,而是它更加具备有灵活性。这一点我们从 Lock 的接口定义上可以看到明显的效果,具体代码如下所示:

public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();

}

通过这些接口函数的定义,我们可以发现 Lock 要比 synchronized 更具备一些灵活性,尤其是在它的以下几个特性方面:

  • 支持获取锁超时机制;

  • 支持非阻塞方式获取锁;

  • 支持可中断方式获取锁。

在JDK包中,Lock 实现类的代表莫过于是 ReentrantLock,下边我们重点以 ReentrantLock 为具体类进行深入介绍。

非阻塞方式获取锁

ReentrantLock 中提供了一个非阻塞方式获取锁的接口,名字叫 tryLock,其具体使用方式为:

public void tryLockMethod() {
    if (reentrantLock.tryLock()) {
        try {
            i++;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    } else {
        //todo
    }
}

这种使用方式在获取锁时如果失败了,则不会继续等待,而是会立马返回一个布尔状态值。这一点相比于 synchronized 关键字而言要灵活些,synchronized 关键字在抢夺锁失败之后,只能够进入一个等待队列中,而 tryLock 可以迅速告知调用方结果,从而进入对应的程序分支中进行处理。

锁超时机制

Lock 支持锁超时机制,这个功能要比单纯的 lock 函数更加强大,当获取锁超过一定时间,便会主动退出等待,例如下边这段案例:

public void tryLockMethod_2() {
    try {
        if (reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                i++;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        } else {
            //todo
}
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

在这段代码中,通过调用 tryLock 方法,当等待锁超过 3 秒便会自动退出抢夺。要注意,由于是调用了 tryLock 函数,所以程序不一定会获取锁,因此在 finally 模块中,如果不确定当前线程依然是持有锁的状态,那么就需要在释放前先调用 isLocked 函数进行判断。通常使用 tryLock 的时候需要注意可能会导致活锁发生,所以睡眠时间可以采用随机值,更合适一些。

对可中断方式获取锁

我认为 ReentrantLock.lockInterruptibly 是 Lock 接口定义中非常特别的一个函数,它在使用的时候基本和 Lock#lock 函数是相同效果,使用的案例代码如下所示:

 public void lockInterruptiblyMethod() {
        try {
            reentrantLock.lockInterruptibly();
            i++;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (reentrantLock.isLocked()) {
                reentrantLock.unlock();
            }
        }
}

lockInterruptibly 允许在等待时,由其它线程调用等待线程的 Thread.interrupt 方法来中断等待线程的等待,这一点和 Lock.lock 函数以及 synchronized 关键字是有所不同的。下边让我们通过一个实战案例来理解这几种方法在加锁过程中,遇到线程中断之后会有什么不同的表现。

package 并发编程04.锁中断案例;

import org.junit.jupiter.api.Test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author idea
 * @Date created in 8:42 上午 2022/6/4
 */
public class LockInterruptDemo {


    static class LockInterruptThread_1 implements Runnable {
        private ReentrantLock lock = new ReentrantLock();

        @Override
        public void run() {
            try {
                //外界可以中断这里面的处理
                lock.lockInterruptibly();
                try {
                    System.out.println("enter");
                    long startTime = System.currentTimeMillis();
                    for (; ; ) {
                        if (System.currentTimeMillis() - startTime >= 5000) {
                            break;
                        }
                    }
                    System.out.println("end");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if(lock.isLocked()){
                        lock.unlock();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    static class LockInterruptThread_2 implements Runnable {
        @Override
        public void run() {
            synchronized (this) {
                System.out.println("enter");
                long startTime = System.currentTimeMillis();
                for (; ; ) {
                    if (System.currentTimeMillis() - startTime >= 5000) {
                        break;
                    }
                }
                System.out.println("end");
            }
        }
    }


    static class LockInterruptThread_3 implements Runnable {
        private ReentrantLock lock = new ReentrantLock();

        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("enter");
                long startTime = System.currentTimeMillis();
                for (; ; ) {
                    if (System.currentTimeMillis() - startTime >= 5000) {
                        break;
                    }
                }
                System.out.println("end");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if(lock.isLocked()){
                    lock.unlock();
                }
            }
        }
    }

    @Test
    public void testLockInterruptThread_1() throws InterruptedException {
        LockInterruptThread_1 lockInterruptThread_1 = new LockInterruptThread_1();
        //使用ReentrantLock#lockInterruptibly 在没有获取到锁处于等待过程中是可以被中断的。
        Thread t1 = new Thread(lockInterruptThread_1);
        Thread t2 = new Thread(lockInterruptThread_1);

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

        Thread.sleep(200);
        System.out.println("开始触发中断操作");
        t2.interrupt();
        System.out.println("发起了中断操作");
    }

    @Test
    public void testLockInterruptThread_2() throws InterruptedException {
        LockInterruptThread_2 lockInterruptThread_2 = new LockInterruptThread_2();
        //使用synchronized关键字 在没有获取到锁处于等待过程中是无法随意中断的。
        Thread t1 = new Thread(lockInterruptThread_2);
        Thread t2 = new Thread(lockInterruptThread_2);

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

        Thread.sleep(200);
        System.out.println("开始触发中断操作");
        t2.interrupt();
        System.out.println("发起了中断操作");
    }

    @Test
    public void testLockInterruptThread_3() throws InterruptedException {
        LockInterruptThread_3 lockInterruptThread_3 = new LockInterruptThread_3();
        //使用synchronized关键字 在没有获取到锁处于等待过程中是无法随意中断的。
        Thread t1 = new Thread(lockInterruptThread_3);
        Thread t2 = new Thread(lockInterruptThread_3);

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

        Thread.sleep(200);
        System.out.println("开始触发中断操作");
        t2.interrupt();
        System.out.println("发起了中断操作");
    }

}

首先我们可以将这段代码执行一下,看看对应的效果。

下边是三种不同的测试用例在执行之后的效果:

img

img

img

通过实验结果可以发现,在多线程场景下,使用 lock函数和synchronized 关键字的代码块在尝试获取锁的过程中,如果获取失败了,就会进入等待队列中等待,而此时使用 Thread.interupt 是无法直接中断在等待状态中的线程的。

但是对于使用 lockInterruptibly 方法的线程来说,是可以在 Thread.interupt 调用之后立马进入到中断状态的。

我做一些简单的归纳,主要如下所示:

  • Lock 类中提供了 tryLock() 函数,这个方法支持线程以非阻塞性的方式去获取锁 ,如果获取失败则立马返回结果;

  • Lock 类中提供了 tryLock(long timeout, TimeUnit unit) 函数,这个方法支持锁的超时机制,当获取锁的时候会先进入等待状态,当等待时间达到预期之后才会返回结果。

  • lockInterruptibly 是一个支持线程中断的函数,当线程在执行该函数之后会进入等待状态,在等待的过程中是可以通过 Thread.interupt 函数去进行中断的。

posted @ 2023-03-19 15:08  Dazzling!  阅读(17)  评论(0编辑  收藏  举报