ReentrantLock 可重入锁

基本介绍

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

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与 synchronized 一样,都支持可重入
// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 王天赐
 * @title: TestReentrantLock
 * @projectName hm-juc-codes
 * @description: 测试可重入锁
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-22 17:23
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestReentrantLock")
public class TestReentrantLock {

    /**
     * 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
     * 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
     */

    static ReentrantLock lock = new ReentrantLock();

    public static void method01() {

        lock.lock();

        try {
            log.debug("execute method01");
            method02();
        } finally {
            // 释放锁
            lock.unlock();
        }

    }

    private static void method02() {
        lock.lock();

        try {
            log.debug("execute method02");
            method03();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    private static void method03() {

        lock.lock();
        try {
            log.debug("execute method03");
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        method01();
    }

}

ReentrantLock 可重入锁_第1张图片

可以看到执行结果, 当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行

可打断

可打断就是, 当前线程t1在等待锁的时候, 可以被其他的线程t2使用 t1.interrupt() 方法打断.

lock.lockInterruptibly()尝试获取锁, 并且这个等待锁是可以被打断的

         /**
         * 除非当前线程被中断,否则获取锁。
         * 如果没有被另一个线程持有,则获取锁并立即返回,将锁持有计数设置为 1。
         * 如果当前线程已经持有这个锁,那么持有计数加一并且方法立即返回。
         * 如果锁被另一个线程持有,那么当前线程将被禁用以用于线程调度目的并处于休眠状态,直到发生以下两种情况之一:
         * 锁被当前线程获取;或者
         * 其他一些线程中断当前线程。
         * 如果当前线程获取了锁,则锁持有计数设置为 1。
         * 如果当前线程:
         * 在进入此方法时设置其中断状态;或者
         * 在获取锁时被中断,
         * 然后抛出InterruptedException并清除当前线程的中断状态。
         * 在此实现中,由于此方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁。
         * 抛出:
         * InterruptedException – 如果当前线程被中断
         */
        lock.lockInterruptibly();

我们通过下面的代码去演示

package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

import static java.lang.Thread.sleep;

/**
 * @author 王天赐
 * @title: TestLockInterruptibly
 * @projectName hm-juc-codes
 * @description: 测试可打断
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-23 14:01
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestLockInterruptibly")
public class TestLockInterruptibly {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {


        Thread t1 = new Thread(() -> {
            try {
                // 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常
                // 如果有竞争就进入阻塞队列
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("获取锁的过程中被打断");
                return;
            }

            try {
                log.debug("获得锁... ");
            } finally {
                lock.unlock();
            }
        }, "t1");


        Thread t2 = new Thread(() -> {

            try {
                lock.lock();
                log.debug("获取锁 ... ");
                sleep(1);

                log.debug("打断t1线程的等待锁的过程!");
                t1.interrupt();

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }, "t2");

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

ReentrantLock 可重入锁_第2张图片

我们可以看到 t2 线程先获取锁, 然后t1线程在等待锁获取的过程中被t2线程打断

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

修改下上面的代码

  Thread t1 = new Thread(() -> {
            try {
                // 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常
                // 如果有竞争就进入阻塞队列
                // lock.lockInterruptibly();
                lock.lock();
            } catch (Exception e) {
                e.printStackTrace();
                log.debug("没有获得锁, 返回");
                return;
            }

            try {
                log.debug("获得锁... ");
            } finally {
                lock.unlock();
            }
        }, "t1");

ReentrantLock 可重入锁_第3张图片

可以看到, 即使执行了 interrupt 但是实际上还是没有打断

锁超时

锁超时就是, 如果无法获取锁, 不仅如此阻塞队列, 直接结束

// 仅当调用时没有被另一个线程持有时才获取锁。
// 如果没有被另一个线程持有,则获取锁,并立即返回值为true ,将锁持有计数设置为 1。
// 即使此锁已设置为使用公平排序策略,调用tryLock()也会立即获取该锁(如果可用),无论其他线程当前是否正在等待该锁。
// 这种“闯入”行为在某些情况下可能很有用,即使它破坏了公平性。
// 如果您想尊重此锁的公平设置,请使用几乎等效的tryLock(0, TimeUnit.SECONDS) (它也检测中断)。
// 如果当前线程已经持有这个锁,那么持有计数加一并且该方法返回true 。
// 如果锁被另一个线程持有,则此方法将立即返回值为false 。
// 返回:true锁是空闲的并且被当前线程获取,或者锁已经被当前线程持有,则返回 true;否则false

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
}
package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @author 王天赐
 * @title: TestTimeOutLock
 * @projectName hm-juc-codes
 * @description: 测试锁超时
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-23 15:29
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestTimeOutLock")
public class TestTimeOutLock {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获取锁");
            if (!lock.tryLock()) {
                log.debug("获取锁失败!, 直接跑路");
                return;
            }

            try {
                log.debug("获得了锁 !");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("获取锁");

        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
            log.debug("释放锁...");
        }
    }
}

ReentrantLock 可重入锁_第4张图片

可以看到这里获取锁失败直接就退出了 , 我们也可以使用 tryLock(long, TimeUnit) 方法来设置尝试的时间

package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @author 王天赐
 * @title: TestTimeOutLock
 * @projectName hm-juc-codes
 * @description: 测试锁超时
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-23 15:29
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestTimeOutLock")
public class TestTimeOutLock {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获取锁");
            try {
                if (!lock.tryLock(2, TimeUnit.SECONDS)) {
                    log.debug("获取锁失败!, 直接跑路");
                    return;
                }
                log.debug("获得了锁 !");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("获取锁");

        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
            log.debug("释放锁...");
        }
    }
}

ReentrantLock 可重入锁_第5张图片

可以看到, 主线程在获得锁后 1s 后释放锁, 而 t1 线程等待2s ,在t1线程释放锁后, 第一时间获取了锁

解决哲学家就餐问题

筷子类

需要继承 ReentrantLock 类

package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 王天赐
 * @title: Chopstick
 * @projectName hm-juc-codes
 * @description: 筷子类
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-22 14:37
 */
@Slf4j(topic = "c.Chopstick")
public class Chopstick extends ReentrantLock {

    private String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" +
                "name='" + name + '\'' +
                '}';
    }
}

哲学家类

package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @author 王天赐
 * @title: Philosopher
 * @projectName hm-juc-codes
 * @description: 哲学家类
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-22 14:37
 */
@Slf4j(topic = "c.Philosopher")
@SuppressWarnings("all")
public class Philosopher extends Thread {

    private Chopstick left;
    private Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    public void eat() {
        log.debug("eat ... ");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void run() {
        while (true) {
            if (left.tryLock()) {
                try {
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }

                } finally {
                    left.unlock();
                }
            }
        }
    }
}

可以看到, 我们需要使用 tryLock 方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后.

要在 finally 里释放锁

测试类

package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

/**
 * @author 王天赐
 * @title: TestPhilosopher
 * @projectName hm-juc-codes
 * @description: 哲学家进餐问题
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-22 15:24
 */
@SuppressWarnings("all")
@Slf4j(topic = "TestPhilosopher")
public class TestPhilosopher {

    public static void main(String[] args) {

        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();

    }
}

公平锁

公平锁一般没有必要,会降低并发度

ReentrantLock 默认是不公平的 , 也就是说并不是按照阻塞队列中先来先得的顺序得到锁的, 随机分配锁的

package cn.knightzz.reentrantlock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 王天赐
 * @title: TestFairLock
 * @projectName hm-juc-codes
 * @description: 测试公平锁
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-23 16:31
 */
@SuppressWarnings("all")
public class TestFairLock {

    public static void main(String[] args) throws InterruptedException {

        // 构造参数设置公平锁
        ReentrantLock lock = new ReentrantLock(false);
        lock.lock();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "t" + i).start();
        }
        // 1s 之后去争抢锁
        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start...");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " running...");
            } finally {
                lock.unlock();
            }
        }, "强行插入").start();
        lock.unlock();
    }
}

ReentrantLock 可重入锁_第6张图片

注意:该实验不一定总能复现

如果是公平锁, 随机插入一定是在最后插入

条件变量

基本介绍

  • synchronized 中也有条件变量,就是 waitSet 休息室,当条件不满足时进入 waitSet 等待
  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点

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

代码案例

package cn.knightzz.reentrantlock;

import lombok.extern.slf4j.Slf4j;

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

import static java.lang.Thread.sleep;

/**
 * @author 王天赐
 * @title: TestConditionLock
 * @projectName hm-juc-codes
 * @description: 测试条件变量
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-23 16:45
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestConditionLock")
public class TestConditionLock {

    static ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitBreakfastQueue = lock.newCondition();

    static boolean hasCigarette = false;
    static boolean hasBreakfast = false;

    private static void sendCigarette() {
        lock.lock();
        try {
            log.debug("烟送来了 ...");
            hasCigarette = true;
            // 唤醒对应waitset阻塞的线程
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }

    }

    private static void sendBreakfast() {
        lock.lock();
        try {
            log.debug("早餐送来了 ...");
            hasBreakfast = true;
            // 唤醒对应waitset阻塞的线程
            waitBreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            try {
                // 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似
                lock.lock();
                while (!hasCigarette) {
                    // 不满足条件就到对应的 waitSet 等待
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("等到烟了");
            } finally {
                lock.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            try {
                // 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似
                lock.lock();
                while (!hasBreakfast) {
                    // 不满足条件就到对应的 waitSet 等待
                    try {
                        waitBreakfastQueue.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("等到早餐了");
            } finally {
                lock.unlock();
            }
        }, "小白").start();


        sleep(1000);
        sendCigarette();
        sleep(1000);
        sendBreakfast();
    }
}
  • 小南需要等待烟过来, 否则就一直等待
  • 小白需要等待早餐, 否则就一直等待

ReentrantLock 可重入锁_第7张图片

你可能感兴趣的:(Java系列,并发编程系列,java,jvm,开发语言)