8.ReentrantLock

ReentrantLock

简介

相对于 synchronized 它具备如下特点

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

==与 synchronized 一样,都支持可重入==

基础语法:

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

可重入

@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {

    static ReentrantLock lock = new ReentrantLock();

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


    public static void method1(){
        lock.lock();
        try{
            log.debug("execute method1");
            method2();
        }finally {
            lock.unlock();
        }
    }


    public static void method2(){
        lock.lock();
        try{
            log.debug("execute method2");
            method3();
        }finally {
            lock.unlock();
        }
    }

    public static void method3(){
        lock.lock();
        try{
            log.debug("execute method3");

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

输出:

15:39:20.988 c.ReentrantLockTest [main] - execute method1
15:39:20.990 c.ReentrantLockTest [main] - execute method2
15:39:20.990 c.ReentrantLockTest [main] - execute method3

Process finished with exit code 0

可打断 lock.lockInterruptibly()

@Slf4j(topic = "c.LockInterruptiblyTest")
public class LockInterruptiblyTest {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");

            try {
                //没有竞争就会获取锁
                //有竞争就进入阻塞队列等待,但可以被打断
                lock.lockInterruptibly();
                //lock.lock(); //不可打断
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断");
                return;
            }

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

        lock.lock();
        log.debug("获得了锁");
        t1.start();

        try {
            SleepUtil.sleep(1);
            log.debug("执行打断");
            t1.interrupt();
        } finally {
            lock.unlock();
        }
    }

}

输出:

15:40:30.630 c.LockInterruptiblyTest [main] - 获得了锁
15:40:30.633 c.LockInterruptiblyTest [t1] - 启动...
15:40:31.639 c.LockInterruptiblyTest [main] - 执行打断
15:40:31.640 c.LockInterruptiblyTest [t1] - 等锁的过程中被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at cn.com.wuhm.juc.three.n4.LockInterruptiblyTest.lambda$main$0(LockInterruptiblyTest.java:24)
	at java.lang.Thread.run(Thread.java:750)

Process finished with exit code 0

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

@Slf4j(topic = "c.LockInterruptiblyTest")
public class LockInterruptiblyTest {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");

//            try {
//                //没有竞争就会获取锁
//                //有竞争就进入阻塞队列等待,但可以被打断
//                lock.lockInterruptibly();
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//                log.debug("等锁的过程中被打断");
//                return;
//            }
            
            //不可打断
            lock.lock(); 
            try {
                log.debug("获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("获得了锁");
        t1.start();

        try {
            SleepUtil.sleep(1);
            log.debug("执行打断");
            t1.interrupt();
        } finally {
            lock.unlock();
        }
    }

}

输出:

15:41:41.069 c.LockInterruptiblyTest [main] - 获得了锁
15:41:41.071 c.LockInterruptiblyTest [t1] - 启动...
15:41:42.073 c.LockInterruptiblyTest [main] - 执行打断
15:41:42.073 c.LockInterruptiblyTest [t1] - 获得了锁

Process finished with exit code 0

锁(可设置)超时 lock.tryLock()

立刻返回结果:

@Slf4j(topic = "c.TestTryLock")
public class TestTryLock {

    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("t1开始尝试获取锁");
            if(!lock.tryLock()){
                log.debug("未获取到锁");
                return;
            }
            try{
                log.debug("获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");

        log.debug("获取锁");
        lock.lock();
        t1.start();
        SleepUtil.sleep(1);
        lock.unlock();

    }

}

输出:

17:39:53.222 c.TestTryLock [main] - 获取锁
17:39:53.224 c.TestTryLock [t1] - t1开始尝试获取锁
17:39:53.224 c.TestTryLock [t1] - 未获取到锁

Process finished with exit code 0

尝试等待多少时间后返回结果

@Slf4j(topic = "c.TestTryLock")
public class TestTryLock {

    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("t1开始尝试获取锁");
            try {
                if(!lock.tryLock(2, TimeUnit.SECONDS)){
                    log.debug("未获取到锁");
                    return;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            try{
                log.debug("获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");

        log.debug("获取锁");
        lock.lock();
        t1.start();
        SleepUtil.sleep(1);
        lock.unlock();

    }

}

输出:

17:38:27.472 c.TestTryLock [main] - 获取锁
17:38:27.474 c.TestTryLock [t1] - t1开始尝试获取锁
17:38:28.478 c.TestTryLock [t1] - 获取到锁

Process finished with exit code 0

使用 ReentrantLock 的 tryLock() 解决哲学家就餐问题

代码改造:

筷子类继承 ReentrantLock

public class Chopstick extends ReentrantLock {
    private String name;

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

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

}

哲学家类 run方法改造

@Override
public void run() {
    while (true){
        // 使用 ReentrantLock 改造
        if(left.tryLock()){
            try{
                if(right.tryLock()){
                    try {
                        eat();
                    }finally {
                        right.unlock();
                    }
                }
            }finally {
                left.unlock();
            }
        }
    }
}

测试结果:

。。。。。。
17:44:42.255 c.Philosopher [苏格拉底] - eating......
17:44:42.255 c.Philosopher [亚里士多德] - eating......
17:44:43.259 c.Philosopher [苏格拉底] - eating......
17:44:43.259 c.Philosopher [亚里士多德] - eating......
17:44:44.263 c.Philosopher [亚里士多德] - eating......
17:44:44.263 c.Philosopher [苏格拉底] - eating......
17:44:45.267 c.Philosopher [亚里士多德] - eating......
17:44:45.267 c.Philosopher [阿基米德] - eating......
17:44:46.272 c.Philosopher [阿基米德] - eating......
17:44:46.272 c.Philosopher [亚里士多德] - eating......
17:44:47.276 c.Philosopher [亚里士多德] - eating......
17:44:47.276 c.Philosopher [苏格拉底] - eating......
。。。。。。

不再产生死锁饥饿的问题

(多个)条件变量

synchronized也有条件变量,synchronized是操作系统层面实现的,而ReentrantLock是java层面实现的,但是其实现 的逻辑和思路大致相同,操作系统的Monitor由,waitSetEntryListOwner,三部分组成,其中waitSet等于ReentrantLock的条件变量。ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
@Slf4j(topic = "c.Test10")
public class Test10 {

    static ReentrantLock lock = new ReentrantLock();
    // 定义两个条件变量
    static Condition smokeWaitSetCondition = lock.newCondition();
    static Condition takeoutWaitSetCondition = lock.newCondition();
    static volatile boolean hasSmoke = false;

    static volatile boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(()->{
            lock.lock();
            try {
                while (!hasSmoke) {
                    log.debug("没有烟,休息一会");
                    try {
                        smokeWaitSetCondition.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("来了一口烟,神清气爽,继续工作");
            }finally {
                lock.unlock();
            }
        }, "小明").start();

        new Thread(()->{
            lock.lock();
            try {
                while (!hasSmoke) {
                    log.debug("外卖还没到,休息一会");
                    try {
                        takeoutWaitSetCondition.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("来了一口饭,横扫饥饿,继续工作");
            }finally {
                lock.unlock();
            }
        }, "小强").start();

        SleepUtil.sleep(1);
        sendSmoke();
        sendTakeout();
    }

    private static void sendSmoke(){
        lock.lock();
        try{
            log.debug("送烟来了");
            hasSmoke = true;
            smokeWaitSetCondition.signal();
        }finally {
            lock.unlock();
        }
    }

    private static void sendTakeout(){
        lock.lock();
        try{
            log.debug("外卖来了");
            hasTakeout = true;
            takeoutWaitSetCondition.signal();
        }finally {
            lock.unlock();
        }
    }
}

输出:

14:26:50.387 c.Test10 [小明] - 没有烟,休息一会
14:26:50.389 c.Test10 [小强] - 外卖还没到,休息一会
14:26:51.391 c.Test10 [main] - 送烟来了
14:26:51.391 c.Test10 [main] - 外卖来了
14:26:51.391 c.Test10 [小明] - 来了一口烟,神清气爽,继续工作
14:26:51.391 c.Test10 [小强] - 来了一口饭,横扫饥饿,继续工作

Process finished with exit code 0

运用案例

控制线程执行顺序

需要控制线程2总是比线程1先执行

方式一:wait & notify

@Slf4j(topic = "c.SequenceControl1")
public class SequenceControl1 {

    static Object lock = new Object();
    static boolean running = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock){
                while (!running){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("t1 start execute");
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock){
                log.debug("t2 start execute");
                running = true;
                lock.notifyAll();
            }
        }, "t2");

        SleepUtil.sleep(0.1);
        t1.start();
        t2.start();
    }
}

局限性:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了运行标记来判断该不该wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为同步对象上的等待线程可能不止一个

方式二:park & unpark (推荐)

park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。

并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』

@Slf4j(topic = "c.SequenceControl2")
public class SequenceControl2 {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("t1 start execute");
        }, "t1");

        Thread t2 = new Thread(() -> {
            log.debug("t2 start execute");
            LockSupport.unpark(t1);
        }, "t2");

        SleepUtil.sleep(0.1);
        t1.start();
        t2.start();
    }
}

输出:

15:05:47.893 c.SequenceControl2 [t2] - t2 start execute
15:05:47.895 c.SequenceControl2 [t1] - t1 start execute

Process finished with exit code 0

==注意使用park和unpark与输出语句的先后顺序==

交替打印

场景:线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现

方式一:wait & notifyAll

解析:之前控制顺序是通过标志位,现在是多个线程顺序控制,我们可以通过1,2,3……这样的数值型表示线程执行的循序

@Slf4j(topic = "c.AlternatePrint1")
public class AlternatePrint1 {

    public static void main(String[] args) {
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);

        new Thread(()->{
            syncWaitNotify.print(1, 2, "a");
        }, "t1").start();

        new Thread(()->{
            syncWaitNotify.print(2, 3, "b");
        }, "t2").start();

        new Thread(()->{
            syncWaitNotify.print(3, 1, "c");
        }, "t3").start();
    }
}

class SyncWaitNotify{
    /**
     * 线程的标记:线程1-1;线程2-2;线程3-3
     */
    private int flag;

    /**
     * 循环次数
     */
    private int loopNumber;

    public SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }

    public void print(int currentFlag, int nextFlag, String ch){
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this){
                // 如果当前标记不是这个线程的,进入阻塞队列
                while (this.flag != currentFlag){
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                // 如果相等输出字符
                System.out.print(ch);
                // 将标记为移向下一个线程标志位
                this.flag = nextFlag;
                // 唤醒阻塞队列所有线程
                this.notifyAll();
            }
        }
    }
}

输出:

abcabcabcabcabc

方式二:ReentrantLock

@Slf4j(topic = "c.AlternatePrint2")
public class AlternatePrint2 {

    public static void main(String[] args) {
        SyncAWaitSignal syncAWaitSignal = new SyncAWaitSignal(5);
        Condition conditionA = syncAWaitSignal.newCondition();
        Condition conditionB = syncAWaitSignal.newCondition();
        Condition conditionC = syncAWaitSignal.newCondition();

        new Thread(()->{
            syncAWaitSignal.print(conditionA, conditionB, "a");
        }, "t1").start();

        new Thread(()->{
            syncAWaitSignal.print(conditionB, conditionC, "b");
        }, "t2").start();

        new Thread(()->{
            syncAWaitSignal.print(conditionC, conditionA, "c");
        }, "t3").start();

        // 主线程需要主动唤醒第一个线程工作
        SleepUtil.sleep(0.2);
        syncAWaitSignal.lock();
        try{
            conditionA.signal();
        }finally {
            syncAWaitSignal.unlock();
        }
    }
}

class SyncAWaitSignal extends ReentrantLock {

    /**
     * 循环次数
     */
    private int loopNumber;

    public SyncAWaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(Condition currentCondition, Condition nextCondition, String ch){
        for (int i = 0; i < loopNumber; i++) {
            // 刚开始让所有线程进入阻塞状态
            this.lock();
            try{
                try {
                    currentCondition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.print(ch);
                // 唤醒下一个线程
                nextCondition.signalAll();
            }finally {
                this.unlock();
            }
        }
    }
}

输出:

abcabcabcabcabc

方式三:park & unpark

@Slf4j(topic = "c.AlternatePrint3")
public class AlternatePrint3 {
    static Thread t1;

    static Thread t2;

    static Thread t3;

    public static void main(String[] args) {
        SyncParkUnPark syncParkUnPark = new SyncParkUnPark(5);

        t1 = new Thread(() -> {
            syncParkUnPark.print(t2, "a");
        }, "t1");

        t2 = new Thread(()->{
            syncParkUnPark.print(t3, "b");
        }, "t2");

        t3 = new Thread(()->{
            syncParkUnPark.print(t1, "c");
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
        // 主线程需要主动唤醒第一个线程工作
        SleepUtil.sleep(0.2);
        LockSupport.unpark(t1);
    }
}

class SyncParkUnPark extends ReentrantLock {

    /**
     * 循环次数
     */
    private int loopNumber;

    public SyncParkUnPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(Thread nextThread, String ch){
        for (int i = 0; i < loopNumber; i++) {
            // 刚开始让所有线程进入阻塞状态
            LockSupport.park();
            System.out.print(ch);
            LockSupport.unpark(nextThread);
        }
    }
}

输出:

abcabcabcabcabc
0%