《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



相关文章
|
24天前
|
Java 程序员
从菜鸟到大神:JAVA多线程通信的wait()、notify()、notifyAll()之旅
【6月更文挑战第21天】Java多线程核心在于wait(), notify(), notifyAll(),它们用于线程间通信与同步,确保数据一致性。wait()让线程释放锁并等待,notify()唤醒一个等待线程,notifyAll()唤醒所有线程。这些方法在解决生产者-消费者问题等场景中扮演关键角色,是程序员从新手到专家进阶的必经之路。通过学习和实践,每个程序员都能在多线程编程的挑战中成长。
|
5天前
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
9 0
|
5天前
|
存储 安全 Java
Java面试题:假设你正在开发一个Java后端服务,该服务需要处理高并发的用户请求,并且对内存使用效率有严格的要求,在多线程环境下,如何确保共享资源的线程安全?
Java面试题:假设你正在开发一个Java后端服务,该服务需要处理高并发的用户请求,并且对内存使用效率有严格的要求,在多线程环境下,如何确保共享资源的线程安全?
12 0
|
5天前
|
安全 Java 开发者
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
9 0
|
5天前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
9 0
|
10天前
|
存储 SQL 安全
Java共享问题 、synchronized 线程安全分析、Monitor、wait/notify以及锁分类
Java共享问题 、synchronized 线程安全分析、Monitor、wait/notify以及锁分类
13 0
|
13天前
|
Java
Java中的线程通信:wait、notify与Condition详解
Java中的线程通信:wait、notify与Condition详解
|
18天前
|
开发框架 安全 .NET
技术好文共享:进程和线程的区别
技术好文共享:进程和线程的区别
13 0
|
24天前
|
Oracle Java 关系型数据库
面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?
面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?
18 0
|
5天前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
18 1