ReentrantLock

7 篇文章 0 订阅
本文详细介绍了ReentrantLock相对于synchronized的特性,包括可中断、可设置超时、公平锁以及支持多个条件变量。通过实例展示了可重入、可打断、锁超时的使用,并解释了公平锁的概念。此外,还探讨了ReentrantLock的条件变量,以及在实际应用中的注意事项。文章最后提到了ReentrantLock的非公平锁实现原理,涉及AQS队列和锁的获取与释放过程。
摘要由CSDN通过智能技术生成

有一段时间没使用过了,复习复习ReentrantLock

相对于 synchronized 它具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与synchronized一样,都可以支持可重入,当然顾名思义,Reentrant 本来就是可重入的意思。

基本语法:

与synchronized不同,synchronized是作为关键字,基于JVM层面对临界区进行保护,而ReentrantLock是基于API的,也就是对象层面。因此需要创建ReenrantLock对象,调用lock()方法来获取锁,使用unlock()方法来释放锁,一定要保证lock() 和 unlock()方法是成对出现的,使用使用 try{}finally{}代码块来保证 unlock()的必须执行,释放锁。

 ReentrantLock reentrantLock = new ReentrantLock();
        // 获取锁
        reentrantLock.lock();
        try{
            // 临界区
        }finally {
            // 释放锁
            reentrantLock.unlock();
        }

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。

如果是不可重入锁,那么第二次获得锁时,自己也会被锁住。

可重入代码演示:

/**
 * @author Claw
 * @date 2022/3/6 11:39.
 */
@Slf4j(topic = "c.ReentrantLock1")
public class ReentrantLock1 {
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        // 获取锁
        reentrantLock.lock();
        try {
            log.debug("enter main");
            m1();
        } finally {
            // 释放锁
            reentrantLock.unlock();
        }
    }
    private static void m1() {
        reentrantLock.lock();
        try {
            log.debug("enter m1");
            m2();
        } finally {
            reentrantLock.unlock();
        }
    }

    private static void m2() {
        reentrantLock.lock();
        try {
            log.debug("entry m2");
        } finally {
            reentrantLock.unlock();
        }
    }
}

输出结果:可以看持有同一把锁的方法在重入

可打断

可打断所调用的方法是 lockInterruptibly(),可以尝试获取锁,也能被打断。

下面这段代码演示了 lockInterruptibly()方法的基本使用,由t1线程调用 lockInterruptibly(),来获取锁,这个方法会抛出 InterruptedException 的异常,需要catch住,当抛出这个异常时,说明没有获取到锁,返回。

然后由main线程先获取到锁,再让t1启动,在这一步就会陷入阻塞状态。

用lockInterruptibly() 去打断t1线程,就能避免阻塞,让程序结束。

/**
 * @author Claw
 * @date 2022/3/6 11:39.
 */
@Slf4j(topic = "c.ReentrantLock2")
public class ReentrantLock2 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        // t1 线程尝试获取锁,如果没有获取到则返回,不用一直阻塞.
       Thread t1 = new Thread(() -> {
            try {
                log.debug("尝试获取锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁,返回");
                return;
            }
            try {
                log.debug("获取到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
       // 主线程先去获取锁
       lock.lock();
       // t1 再去执行,就无法拿到锁,进入阻塞状态
       t1.start();
       // 将T1打断,防止阻塞.
       t1.interrupt();
    }

}

 可打断锁的意义就是加入了可打断的机制,避免了无限制的等待,这也是一种防止死锁的方式。

锁超时

可打断为了避免死等,但是它是一种被动的方式,需要其他线程来调用interrupt()方法。

而锁超时是主动的,它也是一种避免死锁的手段。

立即结束的方式,tryLock() ,代码演示 ()

使用tryLock()方法来获取锁,从名字上也知道,它会尝试去获取锁,返回布尔值来表示是否获取到锁。

使用while来判断,当获取不到锁时,直接return,避免阻塞。

Main线程先获取锁,再让t1启动

/**
 * @author Claw
 * @date 2022/3/6 11:39.
 */
@Slf4j(topic = "c.ReentrantLock3")
public class ReentrantLock3 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 尝试获取锁,没有获取到则返回
            if (!lock.tryLock()) {
                log.debug("{}没有获取到锁,返回", Thread.currentThread().getName());
                return;
            }
            try {
                log.debug("{}获取到锁", Thread.currentThread().getName());
            } finally {
                lock.unlock();
            }
        }, "t1");
        // 主线程先获取锁
        lock.lock();
        log.debug("{}获取到锁", Thread.currentThread().getName());
        // t1 此时执行,没有获取到锁.
        t1.start();
    }
}

结果:

 等待一段时间结束的方式,tryLock(long timeout, TimeUnit unit),代码演示

tryLock()第二种使用方式,带时间参数的方法。在指定时间没有获取到锁的时候,会结束等待。

主线程先拿到锁,然后启动t1,此时开始等待2秒,1秒以后main线程进入睡眠,调用unlock()方法解锁,t1在超时时间内尝试获取锁成功。

/**
 * @author Claw
 * @date 2022/3/6 11:39.
 */
@Slf4j(topic = "c.ReentrantLock4")
public class ReentrantLock4 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 尝试获取锁,没有获取到则返回
            try {
                if (!lock.tryLock(2, TimeUnit.SECONDS)) {
                    log.debug("{}没有获取到锁,返回", Thread.currentThread().getName());
                    return;
                }
            } catch (InterruptedException e) {
                // tryLock 也可以被打断,会抛出 InterruptedException
                e.printStackTrace();
                log.debug("{}没有获取到锁,返回", Thread.currentThread().getName());
                return;
            }
            try {
                log.debug("{}获取到锁", Thread.currentThread().getName());
            } finally {
                lock.unlock();
            }
        }, "t1");
        // 主线程先获取锁
        lock.lock();
        log.debug("{}获取到锁", Thread.currentThread().getName());
        // t1 此时执行,没有获取到锁.
        t1.start();
        try {
            Thread.sleep(1000);
            lock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 执行结果:

公平锁

ReentrantLock 默认是非公平锁,synchronized也是非公平锁。

当多个线程竞争一把锁的时候,谁先得到谁就能拿到锁,并不是先来后到的顺序,谁先等待谁先获得,这种称为非公平锁。

但是ReentrantLock 可以通过构造方法来设置称为公平锁。

 ReentrantLock lock = new ReentrantLock(true);

构造方式源码,可以看到当设置为true的时候,返回一个公平锁,调整为公平锁以后,线程获得锁的方式就变为了哪个线程先进入等待队列, 谁就先拿到锁。

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

公平锁的本意是解决线程饥饿的问题,有均等的机会通过先入先得的方式拿到锁,但是实际上,公平锁一般没有必要,会降低并发度。

条件变量

synchronized 也有条件变量,在synchronized原理中,提到过Monitor里的waitSet,当线程拿不到锁的时候,会进入waitSet里面去等待。

ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的。

synchronized 把不能获取锁的 阻塞线程都放入了waitSet里等待,notifyAll()会叫醒所有的线程,一些线程并不能满足拿到锁的条件,造成了虚假唤醒

ReentrantLock的条件变量更灵活,condition相当于多个条件,当唤醒线程时,唤醒可以满足条件的线程,这就更为灵活。

使用流程:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入conditionObject等待
  • await的线程被唤醒(打断、超时)获取重新竞争lock锁
  • 竞争lock锁成功后,从await后继续执行

代码演示:

代码思路,用两个线程来模拟工作的人,一个需要music才能工作,一个要有coffee才能工作,当条件不满足时,它们都进入了等待。

此时再安排两个线程,一个送咖啡,一个塞耳机,把hasMusic和hasCoffee设为true,并唤醒等待music和coffee的线程。

/**
 * @author Claw
 * @date 2022/3/6 11:39.
 */
@Slf4j(topic = "c.ReentrantLock5")
public class ReentrantLock5 {
    static ReentrantLock lock = new ReentrantLock(false);
    static Condition musicCondition = lock.newCondition();
    static Condition coffeeCondition = lock.newCondition();
    static boolean hasMusic;
    static boolean hasCoffee;

    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("有music吗?{}", hasMusic);
                while (!hasMusic) {
                    log.debug("没有music,不想工作");
                    musicCondition.await();
                }
                log.debug("有music了,claw开始干活");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "一个需要music的claw").start();
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("有coffee吗?{}", hasMusic);
                while (!hasCoffee) {
                    log.debug("没有coffee,不想工作");
                    coffeeCondition.await();
                }
                log.debug("有coffee了,PuppyCoding开始干活");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "一个需要coffee的PuppyCoding").start();
        new Thread(() -> {
            lock.lock();
            try {
                hasCoffee = true;
                coffeeCondition.signalAll();
                log.debug("送了coffee过来");
            } finally {
                lock.unlock();
            }
        }, "送coffee").start();
        new Thread(() -> {
            lock.lock();
            try {
                hasMusic = true;
                musicCondition.signalAll();
                log.debug("塞上耳机,music!");
            } finally {
                lock.unlock();
            }
        }, "有music").start();
    }
}

结果展示:

 相较于synchronized 的notify 和notifyAll 叫醒的线程,使用ReentrantLock的条件唤醒的线程更精确。

注意事项

  • lock和unlock必须要成对使用,unlock必须要在finally中释放
  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。

总结

根据ReentrantLock的特点:可重入,可超时/打断,支持多个的条件condition,还能设置为公平锁,可以整理出与synchronized异同。

比synchronized好的地方:

  • 可以设置超时时间,也可以打断,不用一直阻塞等待
  • 可以设置为公平锁
  • condition 让线程之间的通信更加灵活
  • ReentrantLock让同步代码块更加灵活,比如使用synchronized的时候只能在一个结果中获取和释放

ReentrantLock底层原理

要说ReentrantLock的原理就要先说AQS。

AQS是用来构建锁和其他同步器组件的重量级基础框架和基石,通过内置的队列来完成资源获取线程的排队工作,并通过一个int类型的变量来表示持有锁的状态。

ReentrantLock 非公平锁锁实现原理(课程笔记,待完善)

先从构造器看,默认为非公平锁实现 

NonfairSync继承sync,sync又继承于AbstractQueuedSynchronizer

没有竞争时,用compareAndSetState去修改state,如果成功了修改为1,并且把exclusiveOwnerTheard设置为自己,这样就加锁成功了。

 如果出现了竞争时

 如果出现了竞争,在刚刚上面的源码中会走到else逻辑,即调用acquire方法,它会再去尝试一次能不能获取到锁,没有获取到的话就走acquireQuened(addWaiter)的逻辑,构造Node队列。

 

在没有竞争的情况下,一开始会造一个空节点来进行占位,Node节点是一个双向的链表,此时Thread1竞争失败,排在空节点后面,再进入acuireQueued的逻辑。

 

  1. acuireQueued会在一个死循环中不断尝试获得锁,然后进入Park阻塞。
  2. 如果主机紧邻着head(排第二位),那么在此tryAcquire尝试获取锁,当然这时state仍为1,失败。
  3. 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为-1,这次返回flase(修改为-1的时候表示前驱node有责任在锁释放的时候唤醒后驱node)

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值