并发编程——3.共享模型之管程

news2024/9/25 7:20:44

目录

  • 3.共享模型之管程
    • 3.1.共享带来的问题
      • 3.1.1.Java中的体现
      • 3.1.2.问题分析
      • 3.1.3.临界区 (Critical Section)
      • 3.1.4.竞态条件 (Race Condition)
    • 3.2.synchronized 解决方案
    • 3.3.方法上的 synchronized
    • 3.4.变量的线程安全分析
      • 3.4.1.成员变量和静态变量是否线程安全?
      • 3.4.2.局部变量是否线程安全?
      • 3.4.3.局部变量线程安全分析
      • 3.4.4.常见线程安全类
    • 3.5.Monitor
      • 3.5.1.Java 对象头
      • 3.5.2. Monitor(锁)原理
    • 3.6.wait/notify
      • 3.6.1.API 介绍
      • 3.6.2.sleep(long n) 和 wait(long n) 的区别
      • 3.6.3.案例
    • 3.7.同步模式之保护性暂停
      • 3.7.1.定义
      • 3.7.2.实现
      • 3.7.3.带超时版 GuardedObject
    • 3.8.Park & Unpark
      • 3.8.1.基本使用
      • 3.8.2.特点
    • 3.9. 重新理解线程状态转换
    • 3.10.多把锁
    • 3.11.活跃性
      • 3.11.1.死锁
      • 3.11.2.定位死锁
      • 3.11.3.活锁
      • 3.11.4.饥饿
    • 3.12.ReentrantLock
      • 3.12.1.可重入
      • 3.12.2.可打断
      • 3.12.3.锁超时
      • 3.13.4.公平锁
      • 3.13.5.条件变量
    • 3.13.同步模式之固定运行顺序
      • 3.13.1. wait notify 版
      • 3.13.2.Park Unpark 版
    • 3.14.同步模式之交替执行
      • 3.14.1.wait notify 版
      • 3.14.2.Lock 条件变量版
      • 3.14.3.Park Unpark 版

本文笔记整理来自黑马视频https://www.bilibili.com/video/BV16J411h7Rd/?p=50,相关资料可在视频评论区进行获取。

3.共享模型之管程

3.1.共享带来的问题

3.1.1.Java中的体现

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test17")
public class Test17 {
    
    static int counter = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        }, "t1");
    
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter--;
            }
        }, "t2");
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("counter: {}", counter);
    }
}

某次输出结果如下:

10:29:34 [main] c.Test - counter: 2560

Process finished with exit code 0

3.1.2.问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析。例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 	// 准备常量1
iadd 		// 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i 	// 获取静态变量i的值
iconst_1	 	// 准备常量1
isub 			// 自减
putstatic i		// 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

在这里插入图片描述

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

在这里插入图片描述

但多线程下这 8 行代码可能交错运行:
① 出现负数的情况

在这里插入图片描述

② 出现正数的情况

在这里插入图片描述

3.1.3.临界区 (Critical Section)

  • 一个程序运行多个线程本身是没有问题的;
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题;
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题;
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区:

static int counter = 0;

static void increment()
// 临界区
{
    counter++;
}

static void decrement()
// 临界区
{
    counter--;
}

3.1.4.竞态条件 (Race Condition)

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

3.2.synchronized 解决方案

(1)应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock;
  • 非阻塞式的解决方案:原子变量;

下面使用阻塞式的解决方案(即 synchronized)来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意:虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码;同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。

(2)语法

synchronized(对象) // 线程 1,线程 2(blocked)
{
	临界区
}

修改上述代码:

Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
        synchronized (lock) {
            counter++;
        }
    }
}, "t1");

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
        synchronized (lock) {
            counter--;
        }
    }
}, "t2");

输出结果如下:

11:00:42 [main] c.Test - counter: 0

Process finished with exit code 0

(3)分析

在这里插入图片描述

我们可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人;
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码;
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了;
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入;
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码;

用图来表示:

在这里插入图片描述

(4)思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。为了加深理解,请思考下面的问题:

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性;
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象;
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象;

(5)面向对象改进
把需要保护的共享变量放入一个类。

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test17")
public class Test17 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.increment();
            }
        }, "t1");
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.decrement();
            }
        }, "t2");
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("counter: {}", room.getCounter());
    }
}

class Room {
    private int counter = 0;
    
    public void increment() {
        synchronized (this) {
            counter++;
        }
    }
    
    public void decrement() {
        synchronized (this) {
            counter--;
        }
    }
    
    public int getCounter() {
        synchronized (this) {
            return counter;
        }
    }
}

3.3.方法上的 synchronized

class Test{
    public synchronized void test() {
        
    }
}
//等价于
class Test{
    public void test() {
        synchronized(this) {
            
        }
    }
}
class Test{
    public synchronized static void test() {
    }
}
//等价于
class Test{
    public static void test() {
        synchronized(Test.class) {
            
        }
    }
}

不加 synchronized 的方法:不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)。

3.4.变量的线程安全分析

3.4.1.成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全;
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:
    • 如果只有读操作,则线程安全;
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全;

3.4.2.局部变量是否线程安全?

  • 局部变量是线程安全的;
  • 但局部变量引用的对象则未必:
    • 如果该对象没有逃离方法的作用访问,它是线程安全的;
    • 如果该对象逃离方法的作用范围,需要考虑线程安全;

3.4.3.局部变量线程安全分析

public static void test1() {
    int i = 10;
    i++;
}

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

在这里插入图片描述

而局部变量的引用稍有不同。先看一个成员变量的例子:

public class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            
            // } 临界区
        }
    }
    
    private void method2() {
        list.add("1");
    }
    
    private void method3() {
        list.remove(0);
    }
}

执行

static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
    ThreadUnsafe test = new ThreadUnsafe();
    for (int i = 0; i < THREAD_NUMBER; i++) {
        new Thread(() -> {
            test.method1(LOOP_NUMBER);
        }, "Thread" + i).start();
    }
}

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
		 at java.util.ArrayList.rangeCheck(ArrayList.java:657)
		 at java.util.ArrayList.remove(ArrayList.java:496)
		 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
		 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
		 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
		 at java.lang.Thread.run(Thread.java:748)

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量;
  • method3 与 method2 分析相同;

在这里插入图片描述

将 list 修改为局部变量:

public class ThreadSafe {
    public void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2(list);
            method3(list);
            
            // } 临界区
        }
    }
    
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

那么就不会有上述问题了,分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享;
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象;
  • method3 的参数分析与 method2 相同;

在这里插入图片描述

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3;
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
public class ThreadSafe {
    public void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2(list);
            method3(list);
            
            // } 临界区
        }
    }
    
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】;

3.4.4.常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:

Hashtable table = new Hashtable();
new Thread(()->{
    table.put("key", "value1");
}).start();
new Thread(()->{
    table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的;
  • 但注意它们多个方法的组合不是原子的,见后面分析;

(1)线程安全类方法的组合
分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
	table.put("key", value);
}

在这里插入图片描述

(2)不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?通过分析源码可知,这些方法都是生成了新的字符串对象,而并未改变原始的字符串。

3.5.Monitor

3.5.1.Java 对象头

以 32 位虚拟机为例:
(1)普通对象

|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|

(2)数组对象

|---------------------------------------------------------------------------------|
| 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 |
|-------------------------------------------------------|--------------------|

(3)64 位虚拟机 Mark Word

|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|

3.5.2. Monitor(锁)原理

Monitor 被翻译为监视器管程,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,Monitor 结构如下:

在这里插入图片描述

  • 刚开始 Monitor 中 Owner 为 null;
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner;
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED;
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析;

注意:synchronized 必须是进入同一个对象的 monitor 才有上述的效果,不加 synchronized 的对象不会关联监视器,不遵从以上规则。

3.6.wait/notify

3.6.1.API 介绍

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

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

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    
    final static Object obj = new Object();
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码");
            }
        }, "t1").start();
    
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码");
            }
        }, "t2").start();
        
        //主线程在 2s 后执行
        TimeUnit.SECONDS.sleep(2);
        log.debug("唤醒 obj 上的其它线程");
        synchronized (obj) {
            //随机唤醒一个线程
            obj.notify();
            //唤醒所有线程
            //obj.notifyAll();
        }
    }
}

notify 的一种结果如下:

12:26:08 [t1] c.TestWaitNotify - 执行
12:26:08 [t2] c.TestWaitNotify - 执行
12:26:10 [main] c.TestWaitNotify - 唤醒 obj 上的其它线程
12:26:10 [t1] c.TestWaitNotify - 其它代码

notifyAll 的一种结果如下:

12:27:20 [t1] c.TestWaitNotify - 执行
12:27:20 [t2] c.TestWaitNotify - 执行
12:27:22 [main] c.TestWaitNotify - 唤醒 obj 上的其它线程
12:27:22 [t2] c.TestWaitNotify - 其它代码
12:27:22 [t1] c.TestWaitNotify - 其它代码

Process finished with exit code 0

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

3.6.2.sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法;
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用;
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁;
  4. 使用它们时,线程的状态均为 TIMED_WAITING;

3.6.3.案例

(1)案例代码如下:

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test19")
public class Test19 {
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    
    public static void main(String[] args) throws InterruptedException {
        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();
    
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notifyAll();
            }
        }, "送外卖的").start();
    }
}

输出如下:

11:07:17 [小南] c.Test19 - 有烟没?[false]
11:07:17 [小南] c.Test19 - 没烟,先歇会!
11:07:17 [小女] c.Test19 - 外卖送到没?[false]
11:07:17 [小女] c.Test19 - 没外卖,先歇会!
11:07:18 [送外卖的] c.Test19 - 外卖到了噢!
11:07:18 [小女] c.Test19 - 外卖送到没?[true]
11:07:18 [小女] c.Test19 - 可以开始干活了
11:07:18 [小南] c.Test19 - 没烟,先歇会!

(2)分析:

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为虚假唤醒;解决方法:改为 notifyAll;
  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了;解决方法:用 while + wait,当条件不成立,再次 wait;

(3)wait/notify使用模板总结:

synchronized(lock) {
	while(条件不成立) {
		lock.wait();
	}
	// 干活
}
//另一个线程
synchronized(lock) {
	lock.notifyAll();
}

3.7.同步模式之保护性暂停

3.7.1.定义

保护性暂停即 Guarded Suspension,用在一个线程等待另一个线程的执行结果。要点:

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject;
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者);
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式;
  • 因为要等待另一方的结果,因此归类到同步模式;

在这里插入图片描述

3.7.2.实现

package cn.itcast.pattern;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

public class Downloader {
    public static List<String> download() throws IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL("https://www.baidu.com/").openConnection();
        ArrayList<String> lines = new ArrayList<>();
        try (BufferedReader reader =
                     new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return lines;
    }
}
package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.List;

import static cn.itcast.pattern.Downloader.download;

@Slf4j(topic = "c.GuardedObject")
public class GuardedObject {
    private Object response;
    private final Object lock = new Object();
    
    public Object get() {
        synchronized (lock) {
            // 条件不满足则等待
            while (response == null) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }
    
    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            lock.notifyAll();
        }
    }
    
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            try {
                // 子线程执行下载
                List<String> response = download();
                log.debug("download complete...");
                guardedObject.complete(response);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        log.debug("waiting...");
        // 主线程阻塞等待
        Object response = guardedObject.get();
        log.debug("get response: [{}] lines", ((List<String>) response).size());
    }
}

输出结果如下:


15:48:29 [main] c.GuardedObject - waiting...
15:48:33 [t1] c.GuardedObject - download complete...
15:48:33 [main] c.GuardedObject - get response: [3] lines

Process finished with exit code 0

3.7.3.带超时版 GuardedObject

如果要控制超时时间呢?

public Object get(long timeout) {
    synchronized (lock) {
        // 开始时间
        long begin = System.currentTimeMillis();
        //经历的时间
        long passedTime = 0;
        // 条件不满足则等待
        while (response == null) {
            //这一轮循环应该等待的时间
            long waitTime = timeout - passedTime;
            //经历的时间超过了最大等待时间时,退出循环
            if (waitTime <= 0) {
                break;
            }
            try {
                lock.wait(waitTime);    //注意虚假唤醒问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //计算经历时间
            passedTime = System.currentTimeMillis() - begin;
        }
        return response;
    }
}

3.8.Park & Unpark

3.8.1.基本使用

(1)它们是 LockSupport 类中的方法:

// 暂停当前线程
LockSupport.park();

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

(2)先 park 再 unpark:

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        },"t1");
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

输出如下:

11:47:01 [t1] c.TestParkUnpark - start...
11:47:02 [t1] c.TestParkUnpark - park...
11:47:03 [main] c.TestParkUnpark - unpark...
11:47:03 [t1] c.TestParkUnpark - resume...

Process finished with exit code 0

(3)先 unpark 再 park:

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        },"t1");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

输出如下:

11:47:55 [t1] c.TestParkUnpark - start...
11:47:56 [main] c.TestParkUnpark - unpark...
11:47:57 [t1] c.TestParkUnpark - park...
11:47:57 [t1] c.TestParkUnpark - resume...

Process finished with exit code 0

3.8.2.特点

与 Object 的 wait & notify 相比:

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

3.9. 重新理解线程状态转换

在这里插入图片描述

假设有线程 Thread t。

情况 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 线程从 WAITING --> RUNNABLE;
    • 竞争锁失败,t 线程从 WAITING --> BLOCKED;

情况 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.10.多把锁

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

class BigRoom {
    public void sleep() throws InterruptedException {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            TimeUnit.SECONDS.sleep(2);
        }
    }
    public void study() throws InterruptedException {
        synchronized (this) {
            log.debug("study 1 小时");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
    bigRoom.compute();
},"小南").start();
new Thread(() -> {
    bigRoom.sleep();
},"小女").start();

(2)改进

import java.util.concurrent.TimeUnit;

class BigRoom {
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    public void sleep() throws InterruptedException {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            TimeUnit.SECONDS.sleep(2);
        }
    }
    public void study() throws InterruptedException {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

将锁的粒度细分:

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

3.11.活跃性

3.11.1.死锁

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

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");
        
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
                
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

输出结果如下:

11:06:38 [t1] c.TestDeadLock - lock A
11:06:38 [t2] c.TestDeadLock - lock B

3.11.2.定位死锁

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

在这里插入图片描述

在这里插入图片描述

(2)避免死锁要注意加锁顺序,另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查。

3.11.3.活锁

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

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@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) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

3.11.4.饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。下面讲一下遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题:

在这里插入图片描述

顺序加锁的解决方案

在这里插入图片描述

3.12.ReentrantLock

(1)相对于 synchronized 它具备如下特点:

  • 可中断;
  • 可以设置超时时间;
  • 可以设置为公平锁;
  • 支持多个条件变量;

(2)与 synchronized 一样,都支持可重入,基本语法如下:

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

3.12.1.可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "c.Test22")
public class Test22 {
    private static ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        lock.lock();
        try {
            log.debug("enter main");
            m1();
        } finally {
            lock.unlock();
        }
    }
    
    public static void m1() {
        lock.lock();
        try {
            log.debug("enter m1");
            m2();
        } finally {
            lock.unlock();
        }
    }
    
    public static void m2() {
        lock.lock();
        try {
            log.debug("enter m2");
        } finally {
            lock.unlock();
        }
    }
}

输出结果如下:

16:19:07 [main] c.Test22 - enter main
16:19:07 [main] c.Test22 - enter m1
16:19:07 [main] c.Test22 - enter m2

Process finished with exit code 0

3.12.2.可打断

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "c.Test22")
public class Test22 {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断");
                return;
            }
            try {
                log.debug("获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
            log.debug("执行打断");
        } finally {
            lock.unlock();
        }
    }
}

输出结果如下:

16:54:58 [main] c.Test22 - 获得了锁
16:54:58 [t1] c.Test22 - 启动...
16:54:59 [main] c.Test22 - 执行打断
16:54:59 [t1] c.Test22 - 等锁的过程中被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at cn.itcast.test.Test22.lambda$main$0(Test22.java:15)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断。

3.12.3.锁超时

(1)立刻失败

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "c.Test22")
public class Test22 {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        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();
        try {
            TimeUnit.SECONDS.sleep(2);
        } finally {
            lock.unlock();
        }
    }
}

输出结果如下:

19:00:17 [main] c.Test22 - 获得了锁
19:00:17 [t1] c.Test22 - 启动...
19:00:17 [t1] c.Test22 - 获取立刻失败,返回

Process finished with exit code 0

(2)超时失败

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "c.Test22")
public class Test22 {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("获取等待 1s 后失败,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.debug("获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } finally {
            lock.unlock();
        }
    }
}

输出结果如下:

19:02:24 [main] c.Test22 - 获得了锁
19:02:24 [t1] c.Test22 - 启动...
19:02:25 [t1] c.Test22 - 获取等待 1s 后失败,返回

Process finished with exit code 0

3.13.4.公平锁

ReentrantLock 默认是不公平的。公平锁一般没有必要,会降低并发度,后面分析原理时会讲解。

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

3.13.5.条件变量

(1)synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待。ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:

  • synchronized 是那些不满足条件的线程都在一间休息室等消息;
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒;

(2)使用要点:

  • await 前需要获得锁;
  • await 执行后,会释放锁,进入 conditionObject 等待;
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁;
  • 竞争 lock 锁成功后,从 await 后继续执行;

(3)案例如下:

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "c.Test23")
public class Test23 {
    static ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitbreakfastQueue = lock.newCondition();
    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasCigrette) {
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的烟");
            } finally {
                lock.unlock();
            }
        }).start();
    
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasBreakfast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的早餐");
            } finally {
                lock.unlock();
            }
        }).start();
    
        TimeUnit.SECONDS.sleep(1);
        sendBreakfast();
        TimeUnit.SECONDS.sleep(1);
        sendCigarette();
    }
    
    private static void sendCigarette() {
        lock.lock();
        try {
            log.debug("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }
    
    }
    
    private static void sendBreakfast() {
        lock.lock();
        try {
            log.debug("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }
}

输出结果如下:

12:30:52 [main] c.Test23 - 送早餐来了
12:30:52 [Thread-1] c.Test23 - 等到了它的早餐
12:30:53 [main] c.Test23 - 送烟来了
12:30:53 [Thread-0] c.Test23 - 等到了它的烟

Process finished with exit code 0

3.13.同步模式之固定运行顺序

固定运行顺序,比如,必须先 2 后 1 打印。

3.13.1. wait notify 版

package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TestSequentialControl")
public class TestSequentialControl {
    
    //用来同步的对象
    static Object obj = new Object();
    // t2 运行标记,代表 t2 是否已经执行过
    static boolean t2runed = false;
    
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                while (!t2runed) {
                    // t1 先等一会儿
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            log.debug("1");
        }, "t1");
    
        Thread t2 = new Thread(() -> {
            log.debug("2");
            synchronized (obj) {
                //修改运行标记
                t2runed = true;
                //通知 obj 上等待的线程(可能有多个,因此需要使用 notifyAll)
                obj.notifyAll();
            }
        }, "t2");
        
        t1.start();
        t2.start();
    }
}

输出结果如下:

14:30:30 [t2] c.TestSequentialControl - 2
14:30:30 [t1] c.TestSequentialControl - 1

Process finished with exit code 0

3.13.2.Park Unpark 版

(1)可以看到,实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait;
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题;
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个;

(2)可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

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

输出结果如下:

15:05:18 [t2] c.TestSequentialControl - 2
15:05:18 [t1] c.TestSequentialControl - 1

Process finished with exit code 0

park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』。

3.14.同步模式之交替执行

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现?

3.14.1.wait notify 版

package cn.itcast.pattern;

public class TestAlternateExecution {
    public static void main(String[] args) {
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        new Thread(() -> {
            syncWaitNotify.print(1, 2, "a");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(2, 3, "b");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(3, 1, "c");
        }).start();
    }
}

class SyncWaitNotify {
    private int flag;
    private int loopNumber;
    
    public SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
    
    public void print(int waitFlag, int nextFlag, String str) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (this.flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}

结果输出如下:

abcabcabcabcabc
Process finished with exit code 0

3.14.2.Lock 条件变量版

package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class TestAlternateExecution {
    public static void main(String[] args) {
        AwaitSignal as = new AwaitSignal(5);
        Condition aWaitSet = as.newCondition();
        Condition bWaitSet = as.newCondition();
        Condition cWaitSet = as.newCondition();
        new Thread(() -> {
            as.print("a", aWaitSet, bWaitSet);
        }).start();
        new Thread(() -> {
            as.print("b", bWaitSet, cWaitSet);
        }).start();
        new Thread(() -> {
            as.print("c", cWaitSet, aWaitSet);
        }).start();
        as.start(aWaitSet);
    }
}

@Slf4j(topic = "c.AwaitSignal")
class AwaitSignal extends ReentrantLock {
    //循环次数
    private int loopNumber;
    
    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    
    public void start(Condition first) {
        this.lock();
        try {
            log.debug("start");
            first.signal();
        } finally {
            this.unlock();
        }
    }
    
    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }
}

输出结果如下:

15:58:14 [main] c.AwaitSignal - start
abcabcabcabcabc
Process finished with exit code 0

3.14.3.Park Unpark 版

package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

public class TestAlternateExecution {
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
        ParkUnpark pu = new ParkUnpark(5);
        t1 = new Thread(() -> {
            pu.print("a", t2);
        });
        t2 = new Thread(() -> {
            pu.print("b", t3);
        });
        t3 = new Thread(() -> {
            pu.print("c", t1);
        });
        t1.start();
        t2.start();
        t3.start();
        LockSupport.unpark(t1);
    }
}

class ParkUnpark {
    private int loopNumber;
    
    public ParkUnpark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    
    public void print(String str, Thread next) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(next);
        }
    }
}

输出结果如下:

abcabcabcabcabc
Process finished with exit code 0

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/130129.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot+VUE前后端分离项目学习笔记 - 【07 SpringBoot实现增删改查】

增删改查代码编写 UserController.java package com.zj.demo.controller;import com.zj.demo.entity.User; import com.zj.demo.mapper.UserMapper; import com.zj.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.sp…

CVPR 2017|Deep Feature Flow for Video Recognition论文复现(pytorch版)

&#x1f3c6;引言&#xff1a;深度卷积神经网络在图像识别任务中取得了巨大的成功。然而&#xff0c;将最先进的图像识别网络转移到视频上并非易事&#xff0c;因为每帧评估速度太慢且负担不起。我们提出了一种快速准确的视频识别框架——深度特征流DFF。它只在稀疏关键帧上运…

数据结构与算法_五大算法之--回溯算法

1 回溯算法 回溯算法具有通用性&#xff0c;但是算法的效率不高&#xff0c;通常可以通过剪枝等操作提高算法的效率。 算法思想&#xff1a; 在包含问题的所有解空间树中&#xff0c;按照深度优先搜索的策略&#xff0c;从根节点出发&#xff0c;深度搜索解空间树。当搜索到某…

APSIM作物生长模型学习

由于研究需要&#xff0c;将对APSIM模型使用进行一定学习&#xff0c;特做此笔记&#xff0c;也供该模型的初学者共同进步。 首先是版本选择&#xff0c;这个模型发展较长&#xff0c;有经典的classic版本和次世代版本&#xff0c;而经过实际验证&#xff0c;次世代版本和经典版…

RHCSA 第六天笔记

网络配置 1&#xff0c;ip 命令 ip a 2,修改配置文件&#xff08;不推荐&#xff09; 3&#xff0c;nmcli命令 4&#xff0c;nmtui命令 5&#xff0c;cockpit 网络接口是指网络中的计算机或网络设备与其他设备实现通讯的进出口。这里&#xff0c;主要是指计算机的网络接口即网…

学习笔记之Vue组件化编程(二)

Vue组件化编程&#xff08;二&#xff09;Vue组件化编程一、模块与组件&#xff0c;模块化与组件化1.1 对组件的理解1.2 模块1.3 组件1.4 模块化1.5 组件化&#xff08;二&#xff09;Vue组件化编程 一、模块与组件&#xff0c;模块化与组件化 1.1 对组件的理解 在传统式编写…

Centos7下mysql8.0读写分离的配置

1.前言 1.关于读写分离的原理&#xff0c;这里不做太多赘述。主要从服务器去读取主服务的binlog日志&#xff0c;完成数据同步的过程。 这里我在mac开启了2个虚拟机&#xff0c;ip分别为192.168.31.109 ,192.168.31.208系统为centos 2.配置主从分离之前&#xff0c;需要安装…

第二十五讲:OSPF路由协议邻居认证配置

在相同OSPF区域的路由器上启用身份验证的功能&#xff0c;只有经过身份验证的同一区域的路由器才能互相通告路由信息。这样做不但可以增加网络安全性&#xff0c;对OSPF重新配置时&#xff0c;不同口令可以配置在新口令和旧口令的路由器上&#xff0c;防止它们在一个共享的公共…

android血量条的制作

最近&#xff0c;项目中需要用到血量条&#xff0c;想到血量条这东西&#xff0c;在游戏中经常见到。那么&#xff0c;再android开发中如何制作血量条呢&#xff1f;这里本人想到了两种方法&#xff0c;在网上找到一种最优方案。 方法一&#xff1a;用多张相同的图片拼凑而成的…

Docker安装nginx以及nginx-gui控制面板

一、安装nginx 1、搜索镜像 docker search nginx2、拉取镜像 docker pull nginx3、创建Nginx挂载配置文件 # 创建挂载目录 mkdir -p /install/nginx/conf mkdir -p /install/nginx/log mkdir -p /install/nginx/html# 生成容器 # 将容器nginx.conf文件复制到宿主机 # 将容器…

公网远程连接本地socket服务【内网穿透】

文章目录1. 配置本地socket服务2. 本地socket服务暴露至公网2.1 创建隧道映射9999端口2.2 获取公网地址3. 公网连接本地socket服务端1. 配置本地socket服务 Java 服务端demo环境 jdk1.8框架:springbootmaven开发工具:IDEA 在pom文件引入第三包封装的netty框架maven坐标 <…

【MyBatis】安装 + 框架搭建 + 优化 + 增删改查(全程一条龙服务讲解~)

目录 前言 一、准备工作 1.1、下载MyBatis 1.2、数据库设计 二、搭建框架 2.1、创建Maven项目 2.2、jar包、引入依赖 2.3、创建MyBatis核心配置文件 2.4、映射文件 2.5、通过junit测试功能 2.6、框架优化 三、增删改查优化 四、小结——注意事项 前言 本篇全程从0…

软件测试工程师的发展道路

最近看到一些测试朋友&#xff0c;对测试未来比较迷茫&#xff0c;不知该如何前行&#xff0c;无方向感。目前来看&#xff0c;业界目前存在一个普遍的矛盾&#xff0c;一方面很多人会觉得测试没有发展前途&#xff0c;另一方面&#xff0c;又有非常多的企业急需专业的测试人员…

React学习05-React Router 5

React Router 5 相关理解 SPA 单页Web应用&#xff08;single page web application&#xff0c;SPA&#xff09;。整个应用只有一个完整的页面。点击页面中的链接不会刷新页面&#xff0c;只会做页面的局部更新。数据都需要通过ajax请求获取, 并在前端异步展现。 路由 什么…

全球十大数据安全事件

2021年&#xff0c;数据隐私泄露事件频发&#xff0c;涉及面广&#xff0c;影响力大&#xff0c;企业因此陷入数据保护合规与社会舆情压力的双重危机。近日&#xff0c;有国外媒体梳理了2021年十大数据泄密事件&#xff0c;并对事件进行了点评分析&#xff0c;可供读者参考。据…

第二十九讲:神州路由器DHCP协议的配置

实验拓扑图如下所示 操作步骤&#xff1a; 步骤1&#xff1a;按照图1&#xff0c;正确连接拓扑结构图。 步骤2&#xff1a;为路由器设置主机名称和配置接口IP地址。 Router>enable &#xff01;进入特权模式 Router #config &a…

MySQL高级【SQL性能分析】

目录 1&#xff1a;SQL性能分析 1.1&#xff1a;SQL执行频率 1.2&#xff1a;慢查询日志 1.3&#xff1a;profile详情 1.4&#xff1a;explain 1&#xff1a;SQL性能分析 1.1&#xff1a;SQL执行频率 MySQL 客户端连接成功后&#xff0c;通过 show [session|global] sta…

2022年的5G行业:“5G+”很火,5G网络迟迟未能普及

作者 | 曾响铃 文 | 响铃说 2022年&#xff0c;5G行业依旧是如火如荼地发展&#xff0c;5G技术继续深刻地改变着我们的生活与生产&#xff0c;影响社会经济发展的方方面面。 回顾过去的一年&#xff0c;5G行业有看点&#xff0c;也有疑虑。 5G网络已基本覆盖全国。截至 202…

PaddleNLP开源基于UIE的情感分析,解决小样本难题,助力客户意见洞察与舆情分析!

情感分析&#xff08;Sentiment Analysis&#xff09;是近年来国内外研究的热点&#xff0c;旨在对带有情感色彩的主观性文本进行分析、处理、归纳和推理。情感分析具有广泛的应用场景&#xff0c;可被应用于消费决策、舆情分析、个性化推荐等领域。 如上图所示&#xff0c;情…

移位操作符、位操作符,原码、反码、补码

整数的二进制的表达形式有3种。原码反码补码下面我们举一个例子吧十进制的2原码&#xff1a;00000000000000000000000000000010&#xff08;常见的形式&#xff09;反码&#xff1a;00000000000000000000000000000010补码&#xff1a;00000000000000000000000000000010小结 正整…