【多线程:ReentrantLock】

简介: 【多线程:ReentrantLock】

【多线程:ReentrantLock】

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,这里更加细分了。后面唤醒时 也是按照休息区来唤醒。

目录
相关文章
|
6月前
|
Java 开发者
Java多线程教程:使用ReentrantLock实现高级锁功能
【4月更文挑战第6天】`ReentrantLock`是Java并发编程中一个强大的同步工具,比`synchronized`提供更丰富功能。它支持可响应性、可中断性、公平性选择及条件变量。通过示例展示了创建、公平性设置、可中断锁定、尝试锁定及条件变量的使用。`ReentrantLock`使线程同步更灵活,适用于高性能应用,但使用需谨慎,理解其原理并恰当使用。
83 2
|
6月前
|
Java Linux 调度
剑指JUC原理-7.线程状态与ReentrantLock(中)
剑指JUC原理-7.线程状态与ReentrantLock
57 0
|
3月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
90 0
|
3月前
|
Java
【多线程面试题十六】、谈谈ReentrantLock的实现原理
这篇文章解释了`ReentrantLock`的实现原理,它基于Java中的`AbstractQueuedSynchronizer`(AQS)构建,通过重写AQS的`tryAcquire`和`tryRelease`方法来实现锁的获取与释放,并详细描述了AQS内部的同步队列和条件队列以及独占模式的工作原理。
【多线程面试题十六】、谈谈ReentrantLock的实现原理
|
3月前
|
Java 开发者
Java多线程教程:使用ReentrantLock实现高级锁功能
Java多线程教程:使用ReentrantLock实现高级锁功能
42 1
|
3月前
|
Java C++
【Java 并发秘籍】synchronized vs ReentrantLock:揭秘线程同步神器的对决!
【8月更文挑战第24天】本文详细对比了Java并发编程中`synchronized`关键字与`ReentrantLock`的不同之处。`synchronized`作为内置关键字,提供自动锁管理但不支持中断或公平锁;`ReentrantLock`则通过显式调用方法控制锁,具备更多高级功能如可中断、公平锁及条件变量。文章通过两个计数器类实例展示了两种机制的具体应用,帮助读者理解其差异及适用场景。掌握这两者对于提升多线程程序设计能力至关重要。
59 0
|
4月前
多线程线程安全问题之synchronized和ReentrantLock在锁的释放上有何不同
多线程线程安全问题之synchronized和ReentrantLock在锁的释放上有何不同
|
4月前
|
安全 Java 开发者
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
59 0
|
4月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
87 0
|
6月前
|
安全 Java API
多线程(JUC, ReentrantLock, 原子类, 线程池, 信号量 Semaphore, CountDownLatch)
多线程(JUC, ReentrantLock, 原子类, 线程池, 信号量 Semaphore, CountDownLatch)
54 4