《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)(四)

简介: 《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)

3.15 ReentrantLock

相对于 synchronized 它具备如下特点

可中断(使用其他线程或者方法取消锁)

可以设置超时时间(一段时间内未获取到锁,放弃去争抢锁,执行一些其他逻辑操作)

可以设置为公平锁(先进先出,防止出现锁饥饿现象)

支持多个条件变量(允许有多个WaitSet,不满足条件1时,去waitSet1中等待,不满足2时,去waitSet2中等待。当然唤醒时,也可以根据条件进行唤醒)

这里的中断是指,别的线程可以破坏你的blocking状态,而不是指自己中断阻塞状态


**相同点:**与 synchronized 一样,都支持可重入(同一线程对对象可以重复加锁)


基本语法

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

3.15.1 可重入

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

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

@Slf4j(topic = "c.Test17")
public class Test17 {
    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:33:20.791 c.Test17 [main] - execute method1
15:33:20.794 c.Test17 [main] - execute method2
15:33:20.794 c.Test17 [main] - execute method3

3.15.2 可打断

/**
 * @author lxy
 * @version 1.0
 * @Description ReentrantLock可打断特性示例
 * @date 2022/7/3 17:29
 * 使用lock.lockInterruptibly()的优点:可以防止死锁的产生,避免长时间的等待。
 */
@Slf4j(topic = "c.Test18")
public class Test18 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                //如果没有竞争那么此方法就会获取lock对象锁
                //如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法
                log.debug("尝试获取锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁,返回");
                return;
            }
            try {
                log.debug("获取到锁");
            } finally {//为啥俩try不能被合并:因为被打断的话,不能unlock,只有获取锁了才能继续往下走,往下走又必须来一个finally来保证锁一定被释放
                lock.unlock();//释放锁
            }
        }, "t1");
    //1.当只有一个t1线程时
        //t1.start();
        //2.当有t1和main线程时  
        // lock.lock();
        // t1.start();
        //3.当t1被interrupt打断时
        // lock.lock();
        // t1.start();
        // Sleeper.sleep(2);
        // log.debug("打断t1");
        // t1.interrupt();
    }
}

输出:

  • 当只有一个t1线程时
17:55:18.341 c.Test18 [t1] - 尝试获取锁
17:55:18.343 c.Test18 [t1] - 获取到锁



当有t1和main线程时


0b0803e9ec21808d254bdca9070d3487.png


当t1被interrupt打断时


04efd6e7a861ddd12f85f659841e85ce.png


如果将上锁代码替换成lock.lock();


766e72e5e65d9818d912ff71172c8a96.png


==可打断锁的意义:==可以防止死锁的产生,避免长时间的等待。


3.15.3 锁超时介绍以及应用


可打断和锁超时的区别:可打断是线程t1调用interrupt方法进行打断阻塞状态,是被动的;而锁超时是超过一定时间就放弃获得,是主动的。


使用方式一:tryLock


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

输出:

19:27:15.757 c.Test19 [main] - 获得到锁
19:27:15.759 c.Test19 [t1] - 尝试获得锁
19:27:15.759 c.Test19 [t1] - 获取不到锁
  • 使用方式2:使用trylock(long timeout,TimeUnit unit) 且超时
    8d5caadbc244e0e7e73e02e70363e2fd.png

输出:超时后放弃获取锁

2cbe098a0531e3cfc32e0ef3f399d4cf.png

  • 使用方式三:使用trylock(long timeout,TimeUnit unit) 且未超时
@Slf4j(topic = "c.Test19")
public class Test19 {
    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;
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            try {
                log.debug("获得到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        log.debug("获得到锁");
        t1.start();
        Sleeper.sleep(1);
        lock.unlock();
        log.debug("释放了锁");
    }
}

输出:

9bc774e01477bf30369466f440eb21ed.png

锁超时的应用-解决哲学家就餐问题

//测试死锁
public class TestDeadLock {
    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 {//为了保证获取右手锁后可以成功释放,所以需要加一个 try...catch块
                            right.unlock();
                        }
                    }
                }finally {//为了保证获取左手锁后可以成功释放,所以需要加一个try...catch
                    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 + '}';
    }
}

输出:

14:20:25.184 c.Philosopher [亚里士多德] - eating...
14:20:25.199 c.Philosopher [苏格拉底] - eating...
14:20:25.705 c.Philosopher [柏拉图] - eating...
14:20:25.705 c.Philosopher [赫拉克利特] - eating...
14:20:26.205 c.Philosopher [苏格拉底] - eating...//哲学家们可以正常的进餐

3.15.4 公平锁

ReentrantLock 默认是不公平的,但是可以通过构造方法来设置是否是公平锁。

公平的举例:

不公平的锁,比如synchronized;每次当有线程A占用锁对象,其他线程会进入阻塞队列进行等待,当A使用完后,释放锁,其他线程就会进行竞争锁,抢到了就可以使用。所以这个过程时不公平的。

公平的锁,比如 ReentrantLock ,按照线程进入阻塞队列的顺序来获得锁,先来先得。

公平锁主要来解决线程饥饿问题。前面我们使用tryLock()也可以进行解决。所以公平锁一般没有必要,会降低并发度,后面分析原理时会讲解


**注:**关于公平锁的示例代码,这里不再演示。后面源码剖析时详细介绍。


3.15.5 条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的(休息室),这就好比


synchronized 是那些不满足条件的线程都在一间休息室等消息

而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:


await 前需要获得锁(同synchronized)

await 执行后,会释放锁,进入 conditionObject 等待

await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

竞争 lock 锁成功后,从 await 后继续执行

演示代码

@Slf4j(topic = "c.Test20")
public class Test20 {
    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();
        Sleeper.sleep(1);
        new Thread(()->{
            ROOM.lock();
            try {
                hasTakeout = true;
                log.debug("外卖送到了");
                waitTakeoutSet.signal();
            }finally {
                ROOM.unlock();
            }
        },"送外卖的").start();
        Sleeper.sleep(1);
        new Thread(()->{
            ROOM.lock();
            try {
                hasCigarette = true;
                log.debug("烟送到了!");
                waitCigaretteSet.signal();
            }finally {
                ROOM.unlock();
            }
        },"送烟的").start();
    }
}

输出:

d41a539a2a024eef939f4b46fec8f059.png

3.15.5 同步模式之顺序控制

本章小结


本章我们需要重点掌握的是


分析多线程访问共享资源时,哪些代码片段属于临界区

使用 synchronized 互斥解决临界区的线程安全问题

掌握 synchronized 锁对象语法

掌握 synchronzied 加载成员方法和静态方法语法

掌握 wait/notify 同步方法

使用 lock 互斥解决临界区的线程安全问题

掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量

学会分析变量的线程安全性、掌握常见线程安全类的使用

了解线程活跃性问题:死锁、活锁、饥饿

应用方面

互斥:使用 synchronized 或 Lock 达到共享资源互斥效果

同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果

原理方面

monitor(管程)、synchronized 、wait/notify 原理

synchronized 进阶原理

park & unpark 原理

模式方面

同步模式之保护性暂停

异步模式之生产者消费者

同步模式之顺序控制

注意:


synchronized的互斥是为了临界区的代码不上下文切换,产生指令交错,从而保证临界区代码的原子性。synchronized的同步是为了解决当条件不满足时线程等待,条件满足时继续运行。ReentrantLock也可以实现互斥和同步。


monitor的源码是用C++写的,Java也实现了Monitor锁,即ReentrantLock



相关文章
|
8天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
28 9
|
11天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
25 3
|
26天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
16 1
|
26天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
24 1
|
26天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
34 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
26天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
26天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
26天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
26天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1

热门文章

最新文章