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

📒博客首页:热爱编程的大李子 📒

🌞文章目的:共享对象之管程 🌞

🍄参考视频:深入学习Java并发编程🍄

🙏博主在学习阶段,如若发现问题,请告知,非常感谢🙏

💙同时也非常感谢各位小伙伴们的支持💙

🌈每日一语:未经一番寒彻骨,哪得梅花扑鼻香!🌈

💗感谢: 我只是站在巨人们的肩膀上整理本篇文章,感谢走在前路的大佬们💗

🌟最后,祝大家每天进步亿点点! 欢迎大家点赞👍➕收藏⭐️➕评论💬支持博主🤞!🌟

在这里插入图片描述



3.8 Monitor 概念

3.8.1 Java 对象头

以 32 位虚拟机为例

普通对象

|--------------------------------------------------------------|
| 			           Object Header (64 bits)          	   |
|------------------------------------|-------------------------|
|         Mark Word (32 bits)        |  Klass Word (32 bits)   |
|------------------------------------|-------------------------|
  • klass word:指向类型元数据的指针。比如 student类型的对象,指向Student类型

数组对象

|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |   Klass Word(32bits)  |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

其中 Mark Word 结构为

|-------------------------------------------------------|--------------------|
|                    Mark Word (32 bits)                |        State       |
|-------------------------------------------------------|--------------------|
|       hashcode:25      | age:4 | biased_lock:0 |  01  |       Normal       |
|-------------------------------------------------------|--------------------|
|   thread:23  | epoch:2 | age:4 | biased_lock:1 |  01  |      Biased        |
|-------------------------------------------------------|--------------------|
| 	 		    ptr_to_lock_record:30            |  00  | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|   	    ptr_to_heavyweight_monitor:30        |  10  | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| 						                         |  11  |    Marked for GC   |
|-------------------------------------------------------|--------------------|
  • hashcode:每个对象都有一个hashcode值
  • age:垃圾回收时的分代年龄
  • biased_lock:是否偏向锁
  • 加锁状态
  • ptr_to_heavyweight_monitor:指向操作系统中monitor的指针
  • ptr_to_lock_record:锁记录地址

以Integer对象为例,占 8 + 4 个字节(对象头 + value),基本类型int占 4个字节

3.8.2 原理之 Monitor(锁)

3.8.3 原理之 synchronized

3.8.4 小故事

  • 故事角色
  • 老王 - JVM
  • 小南 - 线程
  • 小女 - 线程
  • 房间 - 对象
  • 房间门上 - 防盗锁 - Monitor
  • 房间门上 - 小南书包 - 轻量级锁
  • 房间门上 - 刻上小南大名 - 偏向锁
  • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
  • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁(Monitor),当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。— 重量级锁

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?— 挂书包(轻量级锁

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式(存在竞争时,锁升级轻量级 --> 重量级)。

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,(偏向锁)下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。(存在竞争时,锁升级偏向锁 --> 轻量级锁

同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王(JVM)觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字(批量重偏向)

后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包(不可偏向)

3.8.5 原理之 synchronized 进阶

3.9 wait notify

3.9.1 小故事 - 为什么需要 wait

  • 由于条件不满足,小南不能继续进行计算

  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低

  • image-20220621150909742

  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋

  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

  • image-20220621150939178

  • 小南于是可以离开休息室,重新进入竞争锁的队列

  • image-20220621151005426

3.9.2 原理之 wait / notify

image-20220621151150378

Owner 线程获取了锁,但是发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态

WAITING线程和BLOCKED线程的区别?

  • WAITING:已经获得了锁,但是条件不满足,释放锁后进入WaitSet队列;
  • BLOCKED:没有获得锁,进入EntryList中等待,状态是BLOCKED

==相同点:==都处于阻塞状态,不占用CPU时间片,将来调度时候也不会考虑。

WAITING线程和BLOCKED线程的唤醒条件?

  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

  • BLOCKED 线程会在 Owner 线程释放锁时唤醒

3.9.3 API 介绍

方法名称描述
obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify()在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll()让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

  • wait:先成为Owner,才有资格进入WaitSet等待。

  • Notify:必须成为Owner,才能叫醒隔壁的WaitSet中的线程。

**案例一:**当没有获得锁,就使用wait/notify/notifyAll方法时,报IllegalMonitorStateException

@Slf4j(topic = "c.Test18")
public class Test18 {
    static final Object lock = new Object();
    public static void main(String[] args) {

        // synchronized (lock) { 
            try {
                lock.wait();//IllegalMonitorStateException
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // }
    }
}

image-20220621152610305

**案例二:**演示Notify和NotifyAll区别

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
           obj.notify(); // 唤醒obj上一个线程
            // obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

notify 的一种结果

15:38:17.518 c.TestWaitNotify [t1] - 执行....
15:38:17.520 c.TestWaitNotify [t2] - 执行....
15:38:19.519 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
15:38:19.519 c.TestWaitNotify [t1] - 其它代码....

notifyAll 的结果

15:37:19.908 c.TestWaitNotify [t1] - 执行....
15:37:19.910 c.TestWaitNotify [t2] - 执行....
15:37:21.908 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
15:37:21.908 c.TestWaitNotify [t2] - 其它代码....
15:37:21.908 c.TestWaitNotify [t1] - 其它代码....

3.9.4 API源码查看

wait() 和wait(long n) 的区别

  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

image-20220621155441699

public final native void notify();

public final native void notifyAll();

public final void wait() throws InterruptedException {
    wait(0);
}

public final native void wait(long timeout) throws InterruptedException; //本地方法

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
	
    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
            "nanosecond timeout value out of range");
    }
	//可以看出wait(long timeout, int nanos)的nacos参数是无用的
    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);//依旧调用的是wait(long timeout)
}

3.10 wait notify 的正确姿势

3.10.0 sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态 都是 TIMED_WAITING (相同点)
  5. 当不满足条件等待,最好使用wait.因为sleep不会释放锁。sleep一般是让CPU休息…:
/**
 * 演示sleep和wait睡眠后,是否释放锁
 */
@Slf4j(topic = "c.Test19")
public class Test19 {

    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock) {
                log.debug("获得锁");
                try {
//                    Thread.sleep(20000);
                    lock.wait(20000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();

        Sleeper.sleep(1);
        synchronized (lock) {
            log.debug("获得锁");
        }
    }
}

3.10.1 问题描述

小南只有叼有烟的时候才干活,其他人只要有时间片就干活,用代码模拟这个过程

3.10.2 step 1

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
    static final Object room = new Object();//建议:引用用final修饰,保证锁的对象就不可变。
    static boolean hasCigarette = false; // 有没有烟
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    sleep(2);
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        sleep(1);
        new Thread(() -> {
            // 这里能不能加 synchronized (room)?
            //synchronized (room) {
                hasCigarette = true;
                log.debug("烟到了噢!");
           // }
        }, "送烟的").start();
    }

}

输出1:

17:17:31.927 c.TestCorrectPosture [小南] - 有烟没?[false]
17:17:31.935 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:17:32.929 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:17:33.940 c.TestCorrectPosture [小南] - 有烟没?[true]
17:17:33.940 c.TestCorrectPosture [小南] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了

输出2:(当加上synchronized (room)

17:25:44.996 c.TestCorrectPosture [小南] - 有烟没?[false]
17:25:45.004 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:25:47.007 c.TestCorrectPosture [小南] - 有烟没?[false]
17:25:47.007 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了

分析:

  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • main加了synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门。没加
    synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

3.10.3 step 2

使用wait-notify改进上面代码

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }

}

输出:

17:42:24.719 c.TestCorrectPosture [小南] - 有烟没?[false]
17:42:24.727 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:25.719 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:42:25.719 c.TestCorrectPosture [小南] - 有烟没?[true]
17:42:25.719 c.TestCorrectPosture [小南] - 可以开始干活了

分析:

  • 此改进可以让其他线程同时运行,不会占用锁,并发效率大大提升
  • 思考:如果有其他线程也在等待,那么会不会错误的叫醒了其他线程?

3.10.4 step 3-4

加入另外一个线程 小女线程时,当外卖送到,小女可开始工作。思考并分析运行结果

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep3 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    // 虚假唤醒
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notify();
                // room.notifyAll();
            }
        }, "送外卖的").start();


    }

}

输出1:(使用notify

image-20220621175311766

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线
    程,称之为【虚假唤醒
  • 解决方法,改为 notifyAll

输出2:(使用notifyAll

image-20220621175602748

  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新
    判断的机会了(比如小南被错误唤醒后,就不能重新判断了)
  • 解决方法,用 while + wait,当条件不成立,再次 wait

3.10.5 step 5

if 改为 while ,防止虚假唤醒

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep5 {
   static final Object room = new Object();
   static boolean hasCigarette = false;
   static boolean hasTakeout = false;

   public static void main(String[] args) {


       new Thread(() -> {
           synchronized (room) {
               log.debug("有烟没?[{}]", hasCigarette);
               while (!hasCigarette) {
                   log.debug("没烟,先歇会!");
                   try {
                       room.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               log.debug("有烟没?[{}]", hasCigarette);
               if (hasCigarette) {
                   log.debug("可以开始干活了");
               } else {
                   log.debug("没干成活...");
               }
           }
       }, "小南").start();

       new Thread(() -> {
           synchronized (room) {
               Thread thread = Thread.currentThread();
               log.debug("外卖送到没?[{}]", hasTakeout);
               while (!hasTakeout) {
                   log.debug("没外卖,先歇会!");
                   try {
                       room.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               log.debug("外卖送到没?[{}]", hasTakeout);
               if (hasTakeout) {
                   log.debug("可以开始干活了");
               } else {
                   log.debug("没干成活...");
               }
           }
       }, "小女").start();

       sleep(1);
       new Thread(() -> {
           synchronized (room) {
               hasTakeout = true;
               log.debug("外卖到了噢!");
               room.notifyAll();
           }
       }, "送外卖的").start();

   }

}

输出:

image-20220621181208206

**结论:**此方法满足需求。即提高了并发效率问题,又不会出现虚假唤醒问题。

思考:while中使用wait()相比wait(long num)会不会浪费CPU呢?

只有线程被唤醒才会接着while,不唤醒就是wait,所以不会有cpu空转

3.10.6 使用wait和notify的正确姿势总结

synchronized(lock) {
	while(条件不成立) {//while防止虚假唤醒
		lock.wait();
	}
	// 干活
}

//另一个线程
synchronized(lock) {
	lock.notifyAll();//notifyAll唤醒所有等待序列中国的线程
}

3.11 Park & Unpark

3.11.1 基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

先 park 再 unpark

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            Sleeper.sleep(1);
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");

        t1.start();
        Sleeper.sleep(2);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

image-20220626141016649

先 unpark 再 park:交换main和t1线程sleep的时间

image-20220626141111113

3.11.2 特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor(锁) 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify (先notify的代码会被忽略)

3.11.3 原理之 park & unpark

3.12 重新理解线程状态转换

image-20220626144652392

  • NEW:初始状态;创建了Java线程对象,还没有与操作系统的线程相关联。

情况 1: NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2: RUNNABLE <--> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
  • 调用 obj.notify()obj.notifyAll() t.interrupt()
    • 竞争锁成功,t 线程从 WAITTING --> RUNNABLE
    • 竞争锁失败,t 线程依旧是 WAITTING --> BLOCKED

**注意:**第二步中,调用 obj.notify()obj.notifyAll() t.interrupt() 但未释放锁,t 线程会先进入EntryList 等待竞争锁,此时为BLOCKED状态。持锁线程释放锁后 EntryList中的线程会进行竞争。然后 根据竞争结果,t 线程会处于不同的状态。此过程可在后续Debug分析中清晰看到…

测试代码

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
           // obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

Debug分析:

情况 3: RUNNABLE <--> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程interrupt() 时,当前线程 WAITING --> RUNNABLE

情况 4 RUNNABLE <--> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <--> TIMED_WAITING

t 线程synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify()obj.notifyAll()t.interrupt()
    • 竞争锁成功,t 线程TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程RUNNABLE --> TIMED_WAITING
    • 注意是当前线程在t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程interrupt() 时,当前线程
    TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线
    RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的interrupt(),或是等待超时,会让目标线程从
    TIMED_WAITING--> RUNNABLE

情况 9 RUNNABLE <--> BLOCKED

  • t 线程synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争
    成功,从BLOCKED --> RUNNABLE,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <--> TERMINATED

当前线程所有代码运行完毕,进入TERMINATED

3.13 多把锁

多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法是准备多个房间(多个对象锁)

例如

public class TestMultiLock {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            bigRoom.study();
        },"小南").start();
        new Thread(() -> {
            bigRoom.sleep();
        },"小女").start();
    }
}

@Slf4j(topic = "c.BigRoom")
class BigRoom {
	
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (this) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }

}

image-20220627150338190

改进:

@Slf4j(topic = "c.BigRoom")
class BigRoom {
	//使用多把锁,相当于把房间分成了两块,允许两个没有关联的动作同时进行
    private final Object studyRoom = new Object();

    private final Object bedRoom = new Object();

    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }

}

image-20220627150445565

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

3.14 活跃性

3.14.1 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得A对象锁,接下来想获取 B对象的锁 。t2 线程 获得 B对象 锁,接下来想获取 A对象的锁。

代码演示:

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

结果:

15:13:19.339 c.TestDeadLock [t2] - lock B
15:13:19.339 c.TestDeadLock [t1] - lock A

3.14.2 定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

  • 使用jps+jstack定位死锁
F:\自学课程\底层课程\Java并发编程\concurrent>jps //查看正在运行的Java进程
11188 Launcher
17828 Jps
15192 TestDeadLock
19404
4684 RemoteMavenServer36

F:\自学课程\底层课程\Java并发编程\concurrent>jstack 15192//查看进程的详细信息

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.192-b12 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000002e13800 nid=0x428c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
//以下是t2线程的详细信息
"t2" #12 prio=5 os_prio=0 tid=0x000000001ed35800 nid=0x290 waiting for monitor entry [0x000000001f37f000]
   java.lang.Thread.State: BLOCKED (on object monitor)//阻塞状态
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
        - waiting to lock <0x000000076ec5d160> (a java.lang.Object)//正在等待的锁...
        - locked <0x000000076ec5d170> (a java.lang.Object)//已经获得的锁
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
//以下是t1线程的详细信息
"t1" #11 prio=5 os_prio=0 tid=0x000000001ed35000 nid=0xcd4 waiting for monitor entry [0x000000001f27f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
        - waiting to lock <0x000000076ec5d170> (a java.lang.Object)
        - locked <0x000000076ec5d160> (a java.lang.Object)
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

//省略部分输出

Found one Java-level deadlock: //发现一个Java级别的死锁
=============================
"t2":
  waiting to lock monitor 0x0000000002f0c0c8 (object 0x000000076ec5d160, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x0000000002f0b9e8 (object 0x000000076ec5d170, a java.lang.Object),
  which is held by "t2"

Java stack information for the threads listed above:
===================================================
"t2"://出现死锁的行号信息
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
        - waiting to lock <0x000000076ec5d160> (a java.lang.Object)
        - locked <0x000000076ec5d170> (a java.lang.Object)
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"t1":
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
        - waiting to lock <0x000000076ec5d170> (a java.lang.Object)
        - locked <0x000000076ec5d160> (a java.lang.Object)
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
  • 使用Jconsole工具

image-20220627153116799

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待。对于这种情况 linux 下可以通过 jps+jstack方式进行排查。

3.14.3 哲学家就餐问题

image-20220627155809520

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待
//就餐测试类
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("阿基米德", c1, c5).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) {
            // 尝试获得左手筷子
            synchronized (left) {
                // 尝试获得右手筷子
                synchronized (right) {
                    eat();
                }
            }
        }
    }

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

//筷子类
class Chopstick {
    String name;

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

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

执行不多会,就执行不下去了

12:33:15.575 [苏格拉底] c.Philosopher - eating...
12:33:15.575 [亚里士多德] c.Philosopher - eating...
12:33:16.580 [阿基米德] c.Philosopher - eating...
12:33:17.580 [阿基米德] c.Philosopher - eating...
// 卡在这里, 不向下运行

使用 jconsole 检测死锁,发现

-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
    
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况

3.14.4 活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

**解决办法:**让两个线程的执行时间交错,具体方案是通过增加随机睡眠时间

3.14.5 饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。

下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

image-20220627161919772

顺序加锁的解决方案(线程1,2获取锁的顺序是相同的)

image-20220627161943812

案例演示:

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线程时

image-20220703175719446

  • 当t1被interrupt打断时

image-20220703180224396

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

image-20220703180911272

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

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) 且超时

    image-20220703194112666

输出:超时后放弃获取锁

image-20220703193919450

  • 使用方式三:使用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("释放了锁");
    }
}

输出:

image-20220703195119864

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

//测试死锁
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();
    }
}

输出:

image-20220705151947795

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

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱编程的大李子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值