当前位置:网站首页>【面试:并发篇22多线程:ReentrantLock】

【面试:并发篇22多线程:ReentrantLock】

2022-07-23 18:06:00 I cream

【面试:并发篇22多线程:ReentrantLock】

00.前言

如果有任何问题请指出,感谢。

01.介绍

RennttrantLock也是锁和synchronized一样具有锁定同步代码块的作用,不过和synchronized还是有很多不一样的地方。
RennttrantLock是java.util.concurrent(juc) 下的一个类,相对于synchronized它具备如下特点:

1.可以打断处于BLOCKED状态的其他线程
2.可以设置处于BLOCKED状态的线程超时时间,如果超过这个时间就放弃争抢锁
3.可以设置为公平锁,线程按照先入先出获取锁
4.支持多个条件变量,这个是指 对于等待区 我们可以由多个 不同对象调用锁处于等待状态时所在的等待区不同,对于synchronized锁来说 它的等待区就只有一个WaitSet

与synchronized一样 都支持锁重入,即自己可以再次获取自己内部的锁

02.基本语法

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

注意:lock.lock();可以放在try外面,也可以放在try里面,最后在finally中需要进行lock.unlock();释放锁

03.可锁重入

例子

public class TestInterKCR {
    
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
    
        lock.lock();
        try {
    
            System.out.println("enter main");
            m1();
        }finally {
    
            lock.unlock();
        }
    }
    private static void m1(){
    
        lock.lock();
        try {
    
            System.out.println("enter m1");
            m2();
        }finally {
    
            lock.unlock();
        }
    }

    private static void m2(){
    
        lock.lock();
        try {
    
            System.out.println("enter m2");
        }finally {
    
            lock.unlock();
        }
    }
}

结果

enter main
enter m1
enter m2

解释
可以看出自己内部的锁都可以调用,没有发生死锁,说明可以锁重入

04.可打断

介绍

我们先要明确一个概念,这里说的可打断 说的是 一个已经获得锁的线程t1 和一个处于阻塞状态线程的t2(因为t1已经获取了锁),我们可以在其他线程中 把 t2线程打断 使之 跳出阻塞状态 运行其它部分代码。

synchronized为什么不可以打断

例子

public class TestInterSync {
    
    private static String a = "s";
    public static void main(String[] args) {
    
        Thread t1 = new Thread(()->{
    
            System.out.println("t1尝试获取锁");
            synchronized (a){
    
                System.out.println("t1获取到锁");
                    boolean interrupted = Thread.currentThread().isInterrupted();
                    if (interrupted){
    
                        System.out.println("t1被打断");
                    }
            }
        });

        Thread t2 = new Thread(()->{
    
            System.out.println("t2尝试获取锁");
            synchronized (a){
    
                System.out.println("t2获取到锁");
                while (true){
    

                }
            }
        });
        t2.start();
        Sleeper.sleep(0.5);
        t1.start();
        Sleeper.sleep(0.5);
        System.out.println("打断正在等待锁的t1线程");
        t1.interrupt();
    }

}

结果

程序没有结束:
t2尝试获取锁
t2获取到锁
t1尝试获取锁
打断正在等待锁的t1线程

解释
我们发现程序并没有结束,我们的t2线程先获取到锁且没有释放锁,导致t1线程阻塞,现在我们在主线程中打断t1线程 但是发现无法打断

lock方法可以打断吗?

例子

public class TestInterReenY {
    
        private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
    
        Thread t1 = new Thread(()->{
    

            System.out.println("t1尝试获取锁");
            lock.lock();
            try {
    
                System.out.println("t1获取到锁");
            }finally {
    
                lock.unlock();
            }
        });

        Thread t2 = new Thread(()->{
    
            System.out.println("t2尝试获取锁");
            lock.lock();
            try {
    
                System.out.println("t2获取到锁");
                Sleeper.sleep(2);
            }finally {
    
            }
        });
        t2.start();
        Sleeper.sleep(0.5);
        t1.start();
        Sleeper.sleep(0.5);
        System.out.println("打断正在等待锁的t1线程");
        t1.interrupt();
    }

}

结果

程序没有结束:
t2尝试获取锁
t2获取到锁
t1尝试获取锁
打断正在等待锁的t1线程

解释
我们发现lock方法依然不能打断处于阻塞状态的线程,那我们应该用什么,答案是lockInterruptibly方法

lockInterruptibly方法

例子

public class TestInterReen {
    
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
    
        Thread t1 = new Thread(()->{
    
            try {
    
                System.out.println("t1尝试获取锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
    
                System.out.println("t1被打断");
                return;
            }

            try {
    
                System.out.println("t1获取到锁");
            }finally {
    
                lock.unlock();
            }
        });

        Thread t2 = new Thread(()->{
    
            try {
    
                System.out.println("t2尝试获取锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
    
                System.out.println("t2被打断");
                return;
            }
            try {
    
                System.out.println("t2获取到锁");
                Sleeper.sleep(2);
            }finally {
    
            }
        });
        t2.start();
        Sleeper.sleep(0.5);
        t1.start();
        Sleeper.sleep(0.5);
        System.out.println("打断正在等待锁的t1线程");
        t1.interrupt();
    }

}

结果

程序结束:
t2尝试获取锁
t2获取到锁
t1尝试获取锁
打断正在等待锁的t1线程
t1被打断

解释
可以看出我们用lockInterruptibly方法可以实现 打断处于阻塞状态的线程

05.锁超时

介绍

锁超时是指,一旦锁被其他线程占用 如果没有指定超时时间 则本线程会直接跳出阻塞状态 不参与竞争,如果指定了超时时间 则 超时时间内处于阻塞状态 超出超时时间 则跳出阻塞状态。

例子:无时限

@Slf4j(topic = "c.TestInterCS")
public class TestInterCS {
    
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
    
        Thread t1 = new Thread(()->{
    
            log.debug("t1尝试获取锁");
            if (!lock.tryLock()){
    
                    log.debug("t1获取不到锁");
                    return;
            }

            try {
    
                log.debug("t1获取到锁");
            }finally {
    
            }
        },"t1");

        Thread t2 = new Thread(()->{
    
            log.debug("t2尝试获取锁");
            if (!lock.tryLock()){
    
                log.debug("t2获取不到锁");
                return;
            }

            try {
    
                log.debug("获取到锁");
            }finally {
    
                lock.unlock();
            }
        },"t2");
        t1.start();
        Sleeper.sleep(1);
        t2.start();
    }
}

结果

00:40:27.595 c.TestInterCS [t1] - t1尝试获取锁
00:40:27.597 c.TestInterCS [t1] - t1获取到锁
00:40:28.600 c.TestInterCS [t2] - t2尝试获取锁
00:40:28.600 c.TestInterCS [t2] - t2获取不到锁

解释
可以看出t1线程获取到锁并且没有释放,t2线程一直处于阻塞状态,我们用tryLock方法 且没有指定超时时间 也就是一旦发现t2线程处于阻塞状态lock.tryLock()返回false,我们执行其它代码

例子:有时限

@Slf4j(topic = "c.TestInterCS")
public class TestInterCS {
    
    private 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("t1获取不到锁");
                    return;
                }
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
            try {
    
                log.debug("t1获取到锁");
            }finally {
    
            }
        },"t1");

        Thread t2 = new Thread(()->{
    
            log.debug("t2尝试获取锁");
            if (!lock.tryLock()){
    
                log.debug("t2获取不到锁");
                return;
            }

            try {
    
                log.debug("获取到锁");
            }finally {
    
                lock.unlock();
            }
        },"t2");
        t1.start();
        Sleeper.sleep(1);
        t2.start();
    }
}

结果

00:44:46.180 c.TestInterCS [t1] - t1尝试获取锁
00:44:46.183 c.TestInterCS [t1] - t1获取到锁
00:44:47.183 c.TestInterCS [t2] - t2尝试获取锁
00:44:47.183 c.TestInterCS [t2] - t2获取不到锁

解释
我们给tryLock方法指定时间为2s,但是因为t1线程始终没有释放锁,所以结果lock.tryLock()依然返回false,我们进行逻辑判断,执行其它的代码

哲学家就餐问题

介绍
之前我们介绍过哲学家就餐问题,当时我们用 顺序加锁的方法解决了死锁的问题,但是当时产生了饥饿问题,这次我们用锁超时解决饥饿问题
代码

@Slf4j(topic = "c.Test23")
public class Test23 {
    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();
}
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    
    Chopstick left;
    Chopstick right;

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

    @Override
    public void run() {
    
        while (true) {
    
            // 尝试获得左手筷子
            if(left.tryLock()) {
    
                try {
    
                    // 尝试获得右手筷子
                    if(right.tryLock()) {
    
                        try {
    
                            eat();
                        } finally {
    
                            right.unlock();
                        }
                    }
                } finally {
    
                    left.unlock(); // 释放自己手里的筷子
                }
            }
        }
    }

    Random random = new Random();
    private void eat() {
    
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick extends ReentrantLock {
    
    String name;

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

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

结果

.
.
.

00:54:18.866 c.Philosopher [苏格拉底] - eating…
00:54:18.866 c.Philosopher [亚里士多德] - eating…
00:54:19.373 c.Philosopher [柏拉图] - eating…
00:54:19.373 c.Philosopher [阿基米德] - eating…
00:54:19.881 c.Philosopher [苏格拉底] - eating…
00:54:19.881 c.Philosopher [赫拉克利特] - eating…
00:54:20.386 c.Philosopher [苏格拉底] - eating…
00:54:20.386 c.Philosopher [亚里士多德] - eating…
00:54:20.894 c.Philosopher [亚里士多德] - eating…
00:54:20.894 c.Philosopher [苏格拉底] - eating…
00:54:21.401 c.Philosopher [赫拉克利特] - eating…
00:54:21.401 c.Philosopher [柏拉图] - eating…
00:54:21.908 c.Philosopher [苏格拉底] - eating…
00:54:21.908 c.Philosopher [赫拉克利特] - eating…
00:54:22.417 c.Philosopher [苏格拉底] - eating…
00:54:22.417 c.Philosopher [赫拉克利特] - eating…
.
.
.
.

解释
我们可以看出现在没有产生死锁 且 也没有线程处于饥饿,这里的做法是让Chopstick继承ReentrantLock类,然后Chopstick对象也就是筷子 使用tryLock方法 如果和其他哲学家产生竞争就放弃获取,这样自然而然没有了 由于互相争抢而产生的死锁问题了

06.公平锁

介绍
多个线程按照申请锁的顺序获取锁
源码

可以看出,ReentrantLock有参构造传递的为true则创建出的对象就为公平锁(默认是不公平锁),公平锁一般没有必要 会降低并发度,之后我们学习AQS时会分析它的源码

07.条件变量

介绍

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

使用流程

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

例子

还是之前那个wait/notify的例子,这次使用RenntrantLock实现

@Slf4j(topic = "c.Test24")
public class Test24 {
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {
    


        new Thread(() -> {
    
            ROOM.lock();
            try {
    
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
    
                    log.debug("没烟,先歇会!");
                    try {
    
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
    
                ROOM.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
    
            ROOM.lock();
            try {
    
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
    
                    log.debug("没外卖,先歇会!");
                    try {
    
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
    
                ROOM.unlock();
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
    
            ROOM.lock();
            try {
    
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
    
                ROOM.unlock();
            }
        }, "送外卖的").start();

        sleep(1);

        new Thread(() -> {
    
            ROOM.lock();
            try {
    
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
    
                ROOM.unlock();
            }
        }, "送烟的").start();
    }

}

结果

01:20:49.292 c.Test24 [小南] - 有烟没?[false]
01:20:49.295 c.Test24 [小南] - 没烟,先歇会!
01:20:49.295 c.Test24 [小女] - 外卖送到没?[false]
01:20:49.295 c.Test24 [小女] - 没外卖,先歇会!
01:20:50.300 c.Test24 [小女] - 可以开始干活了
01:20:51.309 c.Test24 [小南] - 可以开始干活了

解释
我们创建了两个休息室,小南在没有获得烟这个条件前 进入的是waitCigaretteSet休息室,小女在没有获得外卖这个条件前 进入的是waitTakeoutSet这个休息室,相比于之前使用synchronized时小女和小南没有获得条件之前进入的都是WaitSet,这里更加细分了。后面唤醒时 也是按照休息区来唤醒。

原网站

版权声明
本文为[I cream]所创,转载请带上原文链接,感谢
https://blog.csdn.net/m0_71229547/article/details/125946397