文章目录

  • JUC P1 进程,线程,管程 基础+代码
    • 0. 简单概念
      • 0.1 进程和线程
      • 0.2 并行和并发
      • 0.3 同步和异步
    • 1. 创建线程的方法
    • 2. 查看进程和线程的方法
    • 3. 线程运行
      • 3.1 栈与栈帧
      • 3.2 线程上下文切换
      • 3.3 Java Thread 常见方法
        • 3.3.1 start() 和 run()
        • 3.3.2 sleep() 和 yield()
        • 3.3.3 任务优先级
        • 3.3.4 sleep() 应用
        • 3.3.5 join()
        • 3.3.6 interrupt()
        • 3.3.7 过时方法
        • 3.3.8 守护线程
      • 3.4 练习
    • 4. 共享模型之管程
      • 4.1 共享问题
      • 4.2 synchronized 解决方案
        • 语法
      • 4.3 变量的线程安全分析
      • 4.4 练习
      • 4.5 Monitor 管程
        • 4.5.1 对象头
        • 4.5.2 Monitor 管程
        • 4.5.3 轻量级锁
        • 4.5.4 重量级锁
        • 4.5.5 偏向锁(Java 15 弃用)
          • 偏向状态
          • 为什么废弃偏向锁?
        • 4.5.6 锁消除
      • 4.6 wait() / notify()
        • 4.6.1 sleep() 和 wait() 的区别
        • 4.6.2 最佳使用方法
      • 4.7 多线程设计模式
        • 4.7.1 保护性暂停
          • 扩展
        • 4.7.2 生产者/消费者(异步)
      • 4.8 park() / unpark()
      • 4.9 死锁
        • 定位死锁
        • 哲学家就餐问题
        • 活锁
        • 饥饿

JUC P1 进程,线程,管程 基础+代码

教程:https://www.bilibili.com/video/BV16J411h7Rd

0. 简单概念

0.1 进程和线程

线程:

  • 计算机调度的最小单元
  • 更加轻量,加一个线程只需要分配很少的存储空间,可以多个线程共享同一个进程的空间
  • 线程同步:互斥锁

进程:

  • 计算机分配资源的最小单元
  • 一个进程可以有多个线程,至少有一个线程
  • fork 创建子进程,当发生写操作时会复制父进程一块内存
  • 进程间通信:管道,消息队列,Socket,信号量

0.2 并行和并发

并行:多个线程同时执行,一手画圆,一手画方

并发:每个线程分配一定的时间执行,一会画圆,一会画方

0.3 同步和异步

同步:需要等待返回结果,才能继续运行

异步:不需要等待返回结果,就能继续运行

异步的小例子,主线程不需要等待另外一个线程执行结束就可以直接向下执行:

public static void main(String[] args) throws ClassNotFoundException {new Thread(() -> {try {long start = System.currentTimeMillis();Thread.sleep(2000);System.out.println("我执行完了, 执行时间: " + (System.currentTimeMillis() - start) + " ms");} catch (InterruptedException e) {throw new RuntimeException(e);}}).start();System.out.println("hello");
}

1. 创建线程的方法

参考我之前写的:Java 创建线程的三种方式

这里补充一些细节:

  • 使用 FutureTask 实例获取返回值的时候 task.get() 会使当前线程阻塞,一直等到结果返回才继续往下执行。
public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {log.debug("线程执行");Thread.sleep(2000);return 200;}});new Thread(task, "线程1").start();log.debug("{}", task.get());log.debug("执行结束!");
}

简化版:

public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> task = new FutureTask<Integer>(() -> {log.debug("线程执行");Thread.sleep(2000);return 200;});new Thread(task, "线程1").start();log.debug("{}", task.get());log.debug("执行结束!");
}

2. 查看进程和线程的方法

Linux:

  • ps -ef 查看所有进程信息
  • kill <pid>,杀死进程
  • top 实时查看进程
  • top -Hp <pid> 查看进程中的线程

Java:

  • jps 查看 java 进程
  • jstack <pid> 查看某个时刻某个 java 进程中的所有线程详情
  • jconsole 使用图形化界面查看进程情况

3. 线程运行

3.1 栈与栈帧

每个线程启动 JVM 会分配给该线程一块虚拟机栈内存,每个线程只能有一个活动栈帧,对应当前执行的方法。

3.2 线程上下文切换

切换时机:

  • 线程 CPU 时间片结束
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep(),yield(),wait(),join(),park(),synchronized,lock 等方法。

上下文切换需要记录当前线程的状态,JVM 实现此方法的就是程序计数器

线程不是越多越好,频繁的上下文切换也会导致性能下降。

3.3 Java Thread 常见方法

  • start():启动一个新线程,让一个线程进入就绪,不一定会立刻运行(CPU 时间片还没分配)
  • run():线程调用 start() 后会自动调用该方法,不能手动调用,手动调用就相当于一个普通方法来使用
  • join():等待线程运行结束
  • join(long n):等待线程运行结束,最多等待 n 毫秒
  • setPriority(int):设置优先级,默认为 5,最高为 10
  • getState(): 获取线程状态,Java 中共有 6 种状态
public enum State {NEW, // 新建RUNNABLE, // 可运行线程的线程状态。处于可运行状态的线程正在JVM中执行BLOCKED, // 阻塞WAITING, // 等待TIMED_WAITING, // 限时等待TERMINATED; // 终止
}

Java 中的 RUNNABLE 对应多种 OS 中的状态:

  • isInterrupted():是否被打断,不清除打断标记
  • isAlive(): 线程是否存活
  • interrupt():打断线程,若被打断的线程正在 sleep,wait,join,则会导致被打断的线程抛出 InterruptException,并清除打断标记
  • interrupted():判断当前线程是否被打断,清除打断标记
  • sleep(long n) :当前线程休眠 n 毫秒,让出 cpu 的时间片给其他线程
  • yield():提示线程调度器让出当前线程对 CPU 的使用,主要为了测试和调试

3.3.1 start() 和 run()

public static void main(String[] args) {FutureTask<Integer> task = new FutureTask<>(() -> {log.debug("t1 线程执行完毕!");return 200;});new Thread(task, "t1").run();log.debug("main 执行完毕!");
}

  • 主动调用 run() 就相当于调用一个普通方法一样,不会以多线程的方式去执行。

  • 正确的方式应该调用 start() 方法,然后让系统自动调用 run() 方法。

start() 不能被调用两次,会抛出 IllegalThreadStateException

public static void main(String[] args) {FutureTask<Integer> task = new FutureTask<>(() -> {log.debug("t1 线程执行完毕!");return 200;});Thread t1 = new Thread(task, "t1");t1.start();t1.start();log.debug("main 执行完毕!");
}

  • 当线程创建后没有调用 start() 之前是 NEW 状态
  • 调用 start() 之后是 RUNNABLE 状态。

3.3.2 sleep() 和 yield()

sleep

  1. 调用 sleep 会让线程从 正在运行 进入 TIMED_WAITING
  2. 其他线程使用 interrupt() 方法打断正在睡眠的线程会抛出 InterruptedException
  3. 睡眠结束后线程未必会立即得到执行
public static void main(String[] args) throws InterruptedException {FutureTask<Integer> task = new FutureTask<>(() -> {try {log.debug("t1 进入睡眠...");Thread.sleep(2000);} catch (Exception e) {log.debug("t1 醒来");e.printStackTrace();return 201;}return 200;});Thread t1 = new Thread(task, "t1");t1.start();Thread.sleep(1000);t1.interrupt();log.debug("main 执行完毕!");
}


4. ★建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

TimeUnit.SECONDS.sleep(2); // 表示当前线程睡 2 秒

yield

  1. 调用 yield 会让线程从 正在运行 进入 RUNNABLE,然后调度执行其他同等优先级的线程,若没有同等优先级的线程,则不能保证让当前线程暂停
  2. 具体实现依赖 OS 的任务调度器

3.3.3 任务优先级

  • 线程优先级只是提示调度器优先调度该线程,任务调度器可以选择忽略
  • Java 中线程默认为 5,最小为 1,最大为 10
  • 若 CPU 繁忙,优先级高的线程会获得更多的时间片
  • 若 CPU 空闲,优先级几乎没什么用

3.3.4 sleep() 应用

防止 while(true) 导致 CPU 空转,浪费资源。可以使用 yield 或者 sleep 来让出 CPU 的使用权给其他程序,即使死循环 sleep(1) 也能大大减少 CPU 的利用。

  • 也可以使用 wait 或者条件变量达到类似的效果(需要加锁,一般适用于同步的场景)
  • sleep 适用于无需锁同步的场景

3.3.5 join()

在当前线程中调用另外一个线程的 join() 方法,表示等待另外一个线程结束后,当前线程才继续往下执行。

@Slf4j
public class InitTest {static int a = 0;public static void main(String[] args) throws InterruptedException {FutureTask<Integer> task = new FutureTask<>(() -> {log.debug("t1 线程开始执行...");TimeUnit.SECONDS.sleep(1);a = 10;return 0;});Thread t1 = new Thread(task, "t1");t1.start();// 调用 join 方法同步等待 t1 线程执行结束t1.join();log.debug("a 的值为: {}", a);}
}


join(long n) 可以进行有时间限制的等待。

3.3.6 interrupt()

若以异常的方式打断 wait(), sleep(), join(),会把打断标记置为 false

@Slf4j
public class InitTest {public static void main(String[] args) throws InterruptedException {FutureTask<Integer> task = new FutureTask<>(() -> {try {log.debug("t1 进入睡眠...");TimeUnit.SECONDS.sleep(2);} catch (Exception e) {log.debug("t1 醒来");e.printStackTrace();}return 200;});Thread t1 = new Thread(task, "t1");t1.start();TimeUnit.SECONDS.sleep(1);t1.interrupt();log.debug("是否打断: {}", t1.isInterrupted());}
}

正常打断(可以用于正常停止线程):

@Slf4j
public class InitTest {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (true) {boolean interrupted = Thread.currentThread().isInterrupted();if (interrupted) {log.debug("线程 t1 被打断了, 退出循环");break;}}}, "t1");t1.start();TimeUnit.SECONDS.sleep(1);t1.interrupt();log.debug("t1 线程是否被打断: {}", t1.isInterrupted());}
}

两阶段停止
在一个线程 T1 中如何 “优雅” 停止一个 T2?

  • 错误思路1:用 stop() 方法(已弃用),如果 T2 锁住共享资源,那么 T2 被杀死后将无法释放锁
  • 错误思路2:System.exit(int) 方法停止线程,该方法会让整个程序都停止

设计一个后台系统监控程序:

/*** 后台监控程序, 两阶段提交*/
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {MonitorThread monitorThread = new MonitorThread();monitorThread.start();TimeUnit.SECONDS.sleep(5);monitorThread.stop();}
}@Slf4j(topic = "c.MonitorThread")
class MonitorThread {private Thread monitor;// 启动监控public void start() {monitor = new Thread(() -> {while (true) {Thread thread = Thread.currentThread();if (thread.isInterrupted()) {log.debug("准备退出......");break;}try {TimeUnit.SECONDS.sleep(2); // case 1 :该处被打断, 打断标记还是 falselog.debug("执行监控记录");          // case 2 : 该处被打断, 打断标记置为 true} catch (InterruptedException e) {log.debug("睡眠时间被打断, 清除打断标记");e.printStackTrace();thread.interrupt();  // case 2 : 重新设置打断标记}}});monitor.start();}// 停止监控public void stop() {monitor.interrupt();}
}


打断 park() 中的线程park() 方法若被打断,再次 park() 将执行失效,如果想再次 park() 可以使用 Thread.interrupted() 清除标记:

@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("park......");LockSupport.park();log.debug("unpark.....");log.debug("当前线程是否被打断: {}", Thread.currentThread().isInterrupted());LockSupport.park(); // 若被打断就无法再次 parklog.debug("unpark.....");});t1.start();TimeUnit.SECONDS.sleep(1);t1.interrupt();}
}

3.3.7 过时方法

以下方法不推荐使用,容易破坏同步代码块,造成线程死锁:

  • stop():停止线程运行
  • suspend() :挂起(暂停)线程运行
  • resume():恢复线程运行

3.3.8 守护线程

所有非守护线程运行结束,即使守护线程的代码没执行完,也会强制结束:

@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {break;}}log.debug("守护线程结束!");});// 设置 t1 为守护线程t1.setDaemon(true);t1.start();TimeUnit.SECONDS.sleep(1);log.debug("主线程结束");}
}

常见守护线程:

  • 垃圾回收器
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待他们处理完当前的请求

Note:
一个进程可以有多个守护线程
t1.setDaemon(true);t1.start(); 不能更换顺序,否则抛出 IllegalThreadStateException

操作系统层面的阻塞状态(比如读文件的时候)在 Java 中是 RUNNABLE 状态。

3.4 练习

烧水泡茶:

@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {long start = System.currentTimeMillis();Thread t1 = new Thread(() -> {try {log.debug("洗水壶.... 1 min");TimeUnit.SECONDS.sleep(1);log.debug("烧开水.... 15 min");TimeUnit.SECONDS.sleep(15);} catch (InterruptedException e) {throw new RuntimeException(e);}}, "t1");Thread t2 = new Thread(() -> {try {log.debug("洗茶壶, 洗茶杯, 拿茶叶.... 4min");TimeUnit.SECONDS.sleep(4);t1.join();log.debug("泡茶...");log.debug("总共用时: {} ms", System.currentTimeMillis() - start);} catch (InterruptedException e) {throw new RuntimeException(e);}}, "t2");t1.start();t2.start();}
}

该代码的缺陷

  • t2 线程需要等待 t1 线程烧水结束才能泡茶,假如反过来要实现 t1 线程拿 t2 线程的茶叶泡茶呢?怎么办?
  • 在接下来的内容中会对该代码进行优化。

4. 共享模型之管程

  • 共享问题
  • synchronized
  • 线程安全分析
  • Monitor
  • wait/notify
  • 线程状态转换
  • 活跃性
  • Lock

4.1 共享问题

  • 一个程序运行多个线程本身没问题

  • 问题出现在多个线程访问共享资源

    • 多个线程只读共享资源也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作时,称这段代码块为临界区(Critical Section)

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

4.2 synchronized 解决方案

为了实现临界区互斥访问,主要有:

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

本节主要讲 synchronized,俗称对象锁

  • 采用互斥的方式让同一时刻最多只能有一个线程持有对象锁,其他线程获取对象锁会进行阻塞。

Note:
互斥保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区的代码
同步是由于线程执行先后不同,顺序不同,需要一个线程等待其他线程运行到某个点

语法

synchronized (object) {// 临界区
}

面向对象的写法:

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

方法上的 synchronized

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

4.3 变量的线程安全分析

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

  • 若没共享或者被共享但是只读,则线程安全
  • 若被共享且有读写操作,则要考虑线程安全问题

成员变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象未必:
    • 若对象没有逃离方法,则线程安全
    • 若对象逃离了方法的,则需要考虑线程安全问题

常见的线程安全类:

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

这里所说的线程安全类指的是单个方法是线程安全的,但是如果方法组合不一定是线程安全的:

Hashtable<Object, Object> hashtable = new Hashtable<>();
if (hashtable.get("key") == null) {hashtable.put("key", 111);
}

Note:
自定义的类,类中没有成员变量,那么该类是线程安全的

4.4 练习

转账问题:

@Slf4j(topic = "c.InitTest")
public class InitTest {static Random random = new Random();// 随机转账private static int randomAccount() {return random.nextInt(100) + 1;}public static void main(String[] args) throws InterruptedException {Account a = new Account(1000);Account b = new Account(1000);Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {a.transfer(b, randomAccount());}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {b.transfer(a, randomAccount());}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("转账 2000 次后的总金额 : {}", a.getMoney() + b.getMoney());}
}class Account {private int money;public Account(int money) {this.money = money;}public void setMoney(int money) {this.money = money;}public int getMoney() {return money;}// 给 target 账户转账public void transfer(Account target, int amount) {if (this.money >= amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}}}

这样的话转账过程中会出现线程安全问题,钱可能越转越多,也可能越转越少:

如果直接在 transfer 方法上加 synchronized,这样解决不了问题的,因为每次锁的都是当前对象,两个线程每次操作的都是两个对象,因此可以直接锁 Account 类:

public void transfer(Account target, int amount) {synchronized (Account.class) {if (this.money >= amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}}
}

4.5 Monitor 管程

4.5.1 对象头

32 位虚拟机:

通过 Klass Word 可以找到对象从属的 class 类对象。

普通对象:

数组对象:

Mark Word 结构(不同状态下,里面的结构也不同):

Note:

  • 可以看到 age 占 4 位,因此 -XX:MaxTenuringThreShold 可以设置的最大分代年龄就是 15。
  • 类中的方法,是不占内存的,只有调用的时候才会分配栈帧。
  • Integer 对象占的内存:int 大小 + 对象头长度 = 12

64 位:

4.5.2 Monitor 管程

Note:
我的理解:管程就是一个“城管”,管理各个线程之间状态转换的数据结构。

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

  • 被锁住的临界区代码块一次只能有一个线程作为 Owner,EntryList 存储阻塞的线程,WaitSet 存储等待的线程。
  • Thread-2 结束后,会唤醒 EntryList 中等待的线程来竞争锁。
  • 同步代码块通过 monitorentermonitorexit 实现,如果过程中发生异常,系统会进行解锁然后抛出异常。

Note:
Monitor 锁是 OS 提供的,因此程序性能更低

4.5.3 轻量级锁

一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),那么就用轻量级锁进行优化。语法仍然使用 synchronized

锁重入的时候会进行 CAS 操作,将对象头中的 Mark Word 替换为指向锁记录的指针

public static void main(String[] args) {fun1();
}public static void fun1() {synchronized (lock) {// synchronized 默认拥有可重入的特性, 一个线程多次获得锁不会导致死锁log.debug("方法 1 执行...");fun2();}
}public static void fun2() {synchronized (lock) {log.debug("方法 2 执行...");}
}

Note:
锁重入指的是一个线程不会因为多次获取锁导致死锁。

4.5.4 重量级锁

在一个线程尝试对对象加轻量级锁的过程中,CAS 竞争操作不成功,也就是此时有其他线程已经为该对象加了轻量级锁(有竞争),这时候需要先进行自旋操作,重复获取锁,若一直失败,则进行锁膨胀,将轻量级锁变为重量级锁,进入阻塞状态。

会直接锁膨胀,变成重量级锁,当重量级锁竞争失败才会进行自旋,自旋一直失败才进入阻塞状态。

Note:
自旋操作存疑!!!:https://www.nowcoder.com/discuss/604631

另外一个线程执行完毕, CAS 替换 Mark Word 会失败,因为锁已经变成重量级锁,因此释放锁,让阻塞的线程加入。

Note:
自旋操作 是为了防止上下文切换,进入阻塞状态,自旋会消耗 CPU,在单核机下自旋就是浪费,多核 CPU 下自旋操作才能发挥作用。
自旋操作是自适应的,默认开启。

4.5.5 偏向锁(Java 15 弃用)

为了解决轻量级锁每次锁重入都需要进行 CAS 操作,Java 6 引入了偏向锁进行优化:

  • 只有第一次使用 CAS 将线程 ID 记录到对象的 Mark Word 和栈帧中,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,只要不发生竞争,这个对象就归该线程所有。

适用于:只有一个线程多次访问同步代码块的场景。

偏向状态

引入依赖:

<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version>
</dependency>
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) {Dog dog = new Dog();log.debug(ClassLayout.parseInstance(dog).toPrintable());}
}class Dog {}

  • 偏向锁是默认开启的,但是需要等应用程序启动后几秒才能激活,若想避免延迟可以设置 -XX:BiasedLockingStartupDelay=0
  • 若确定应用程序中的线程通常都处于竞争状态,可以通过 -XX:-UseBiasedLocking=false,程序默认进入轻量级锁状态

在 Java 15,上面这两个选项已经被弃用了!!!
Java HotSpot(TM) 64-Bit Server VM warning: Option BiasedLockingStartupDelay was deprecated in version 15.0 and will likely be removed in a future release.

为什么废弃偏向锁?

参考:https://zhuanlan.zhihu.com/p/365454004

  • 对于使用了新类库的 Java 应用来说,偏向锁带来的收益已不如过去那么明显,而且在当下多线程应用越来越普遍的情况下,偏向锁带来的锁升级操作反而会影响应用的性能
  • 偏向锁为同步系统引入了许多复杂的代码,并且对 HotSpot 的其他组件产生了影响。这种复杂性已经成为理解代码的障碍,也阻碍了对同步系统进行重构

Note:
偏向锁一直存在是因为兼容以前的 Hasttable 和 Vector 这种老古董性能差的类库,从 Java 15 之后不准备去兼容了,因为已经有了像 ConcurrentHashMap 性能更好的类库。

现在上锁直接就是 thin lock,轻量级锁:

Dog dog = new Dog();
synchronized (dog) {log.debug(ClassLayout.parseInstance(dog).toPrintable());
}

  • 看 16 进制数的最后一位,0 表示轻量级锁

Note:
看 Mark Word 最后两位所表示的锁信息:
00 轻量级锁
01 无锁
10 重量级锁
11 GC 清除标记

4.5.6 锁消除

JVM 中叫同步省略

简单讲就是如果一个对象只能被一个线程访问,就没必要上锁了,即使你上了锁,即时编译阶段会进行优化,把锁去掉。

可以通过 -XX:-EliminateLocks 关闭该功能。

4.6 wait() / notify()

  • Owner 线程发现条件不满足,调用 wait() 方法,即可进入 WaitSet 变为 WATING 状态
  • BLOCKED 和 WATING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁的时候唤醒
  • WATING 线程会在 Owner 线程调用 notify() 或者 notifyAll() 时唤醒,但是醒后不意味着立刻能获得锁,需要进入 EntryList 重新竞争

API:

  • wait(),notify(),notifyAll() 都是 Object 的函数
  • 调用的条件是:必须先获得此对象的锁,才能调用这几个方法
static Object lock = new Object();public static void main(String[] args) throws InterruptedException {lock.wait();
}// 抛出异常:IllegalMonitorStateException: current thread is not owner
static Object lock = new Object();public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (lock) {log.debug("执行...");try {lock.wait(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("其他代码...");}}, "t1").start();new Thread(() -> {synchronized (lock) {log.debug("执行...");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("其他代码...");}}, "t2").start();TimeUnit.SECONDS.sleep(2);log.debug("主线程唤醒 wait 中的对象");synchronized (lock) {//            lock.notify(); // 随机唤醒一个lock.notifyAll(); // 唤醒全部}
}

4.6.1 sleep() 和 wait() 的区别

  1. sleep() 是 Thread 的方法,wait() 是 Object 类的方法
  2. sleep() 不需要强制和 synchronized 配合使用,wait() 必须synchronized 配合使用
  3. sleep() 不会释放锁,wait() 会释放锁

共同点:

  • 状态都是 WATING 和 TIMED_WATING

4.6.2 最佳使用方法

为什么用 while 不用 if ?:

  • 如果一个线程被唤醒,但是还不满足条件,此时用 if 的话就会出错
synchronized (lock){while (条件判断不成立) {lock.wait();}// 执行代码
}// 唤醒线程
synchronized (lock){lock.notifyAll();
}

4.7 多线程设计模式

4.7.1 保护性暂停

Guarded Suspension,用于一个线程等待另外一个线程的执行结果:

借鉴 join() 的源码实现:

@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {GuardedObject guardedObject = new GuardedObject();// 等待结果new Thread(() -> {log.debug("{}", guardedObject.get(3000));}, "t1").start();// 通知 t1new Thread(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}guardedObject.complete("To: t1, From: t2");}, "t2").start();}
}class GuardedObject {// 结果private Object response;// 获取结果public Object get(long millis) {synchronized (this) {final long startTime = System.nanoTime();long delay = millis;do {try {wait(delay);} catch (InterruptedException e) {throw new RuntimeException(e);}} while (response == null && (delay = millis - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);// 细节:每次需要等待的时间 = 总等待时间 - (当前时间 - 起始时间)}}// 产生结果public void complete(Object response) {synchronized (this) {this.response = response;this.notifyAll();}}
}

  • JDK 中,join() 的实现、Future 的实现采用的就是该模式

join() 的源码参考:

public final synchronized void join(final long millis)throws InterruptedException {if (millis > 0) {if (isAlive()) {final long startTime = System.nanoTime();long delay = millis;do {wait(delay);} while (isAlive() && (delay = millis -TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);}} else if (millis == 0) {while (isAlive()) {wait(0);}} else {throw new IllegalArgumentException("timeout value is negative");}}
扩展

在多个类中使用 GuardedObject 需要定义多个,很不方便,因此可以设置一个队列解耦,并且支持多个任务的管理:

实现:

@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {// 启动三个收信人for (int i = 0; i < 3; i++) {new People().start();}TimeUnit.SECONDS.sleep(1);// 启动三个邮差,根据收件人的信箱 id 送信Mailboxes.getIds().forEach(id -> {new Postman(id, "内容" + id).start();});}
}@Slf4j(topic = "c.People")
class People extends Thread{@Overridepublic void run() {// 收信GuardedObject guardedObject = Mailboxes.createGuardedObject();try {log.debug("开始收信, 信箱 id:{}", guardedObject.getId());Object response = guardedObject.get(5000);log.debug("收信 Id: {}, 内容: {}", guardedObject.getId(), response);} catch (InterruptedException e) {throw new RuntimeException(e);}}
}@Slf4j(topic = "c.Postman")
class Postman extends Thread{private final int id;private final String mail;public Postman(int id, String mail) {this.id = id;this.mail = mail;}@Overridepublic void run() {GuardedObject guardedObject = Mailboxes.getGuardedObject(id);log.debug("送信箱 id:{}, 内容: {}", guardedObject.getId(), mail);guardedObject.complete(mail);}
}abstract class Mailboxes {private static int id = 1;private static final Map<Integer, GuardedObject> boxes = new ConcurrentHashMap<>();// 产生唯一 idpublic static synchronized int generateId() {return id++;}// 根据 id 获取 GuardedObject 对象public static GuardedObject getGuardedObject(int id) {return boxes.remove(id);}// 产生 GuardedObject 对象public static GuardedObject createGuardedObject() {GuardedObject go = new GuardedObject(generateId());boxes.put(go.getId(), go);return go;}public static Set<Integer> getIds() {return boxes.keySet();}
}class GuardedObject {// 设置唯一标识private final int id;// 结果private Object response;public GuardedObject(int id) {this.id = id;}// 获取结果public Object get(long millis) throws InterruptedException {synchronized (this) {final long startTime = System.nanoTime();long delay = millis;do {wait(delay);} while (response == null && (delay = millis - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);}// 直到有结果的时候返回结果, 否则一直 waitreturn response;}// 产生结果public void complete(Object response) {synchronized (this) {this.response = response;this.notifyAll();}}public int getId() {return this.id;}
}

4.7.2 生产者/消费者(异步)

  • 不需要生产结果和消费结果的线程一一对应
  • 消息队列是有容量限制的队列,空时不会再消耗数据
  • JDK 中的各种阻塞队列,采用的就是该模式

实现:

@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {MessageQueue queue = new MessageQueue(10);for (int i = 0; i < 3; i++) {int id = i;new Thread(() -> {try {queue.put(new Message(id, "值" + id));} catch (InterruptedException e) {throw new RuntimeException(e);}}, "生产者" + i).start();}new Thread(() -> {try {while (true) {TimeUnit.SECONDS.sleep(1);Message take = queue.take();}} catch (InterruptedException e) {throw new RuntimeException(e);}}, "消费者").start();}
}@Slf4j(topic = "c.MessageQueue")
class MessageQueue {// 消息队列集合private final LinkedList<Message> list = new LinkedList<>();// 容量private final int capacity;public MessageQueue(int capacity) {this.capacity = capacity;}// 获取消息public Message take() throws InterruptedException {// 检查对象是否为空synchronized (list) {while (list.isEmpty()) {log.debug("队列空...消费者请等待");list.wait();}Message msg = list.removeFirst();log.debug("取出消息 :" + msg);list.notifyAll();return msg;}}// 存入消息public void put(Message msg) throws InterruptedException {synchronized (list) {// 检查对象是否满while (list.size() == capacity) {log.debug("队列满...生产者请等待");list.wait();}log.debug("存入消息 :" + msg);list.addLast(msg);list.notifyAll();}}
}record Message(int id, Object value) {}

4.8 park() / unpark()

它们是 LockSupport 类中的方法:

  • park() :暂停当前线程
  • unpark():恢复某个线程的运行,(多次调用只执行一次)
  • unpark() 可以在 park() 之前调用,且有效

而且 unpark() 可以以线程为单位进行唤醒,更加精确

public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("start...");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("park");LockSupport.park();log.debug("resume...");});t1.start();TimeUnit.SECONDS.sleep(2);log.debug("unpark");LockSupport.unpark(t1);
}

unpark() 早于 park() 的情况:

public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("start...");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("park");LockSupport.park();log.debug("resume...");});t1.start();TimeUnit.SECONDS.sleep(1);log.debug("unpark");LockSupport.unpark(t1);
}

4.9 死锁

t1 线程获得 A 锁,又想去获得 B 锁;
t2 线程获得 B 锁,又想去获得 A 锁;

这样就导致了死锁。

定位死锁

jps, jstack <pid> 会看到每个线程的状态,并且死锁的位置会被列出

哲学家就餐问题

  • 五个哲学家只做两件事情,思考和吃饭
  • 吃饭要用两根筷子吃,桌子上只有 5 根筷子,每位哲学家左右手各有一根筷子
  • 筷子被身边人拿到,自己就得等待

这样等一会就有可能发生死锁:

@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) {Chopstick c1 = new Chopstick("c1");Chopstick c2 = new Chopstick("c2");Chopstick c3 = new Chopstick("c3");Chopstick c4 = new Chopstick("c4");Chopstick c5 = new Chopstick("c5");new Philosopher("张三", c1, c2).start();new Philosopher("尼古拉斯·赵四", c2, c3).start();new Philosopher("王二麻子", c3, c4).start();new Philosopher("职业法师·刘海柱", c4, c5).start();new Philosopher("IKUN", c5, c1).start();}
}@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {private final Chopstick left;private final Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}private void eat() {log.debug("eating...");try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}}@Overridepublic void run() {while (true) {synchronized (left) {synchronized (right) {eat();}}}}
}record Chopstick(String name) {}

五个线程都进入死锁状态了。

活锁

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

@Slf4j(topic = "c.InitTest")
public class InitTest {static final Object lock = new Object();static volatile int count = 10;public static void main(String[] args) {new Thread(() -> {while (count > 0) {try {TimeUnit.MILLISECONDS.sleep(1);count--;log.debug("count: {}", count);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "t1").start();new Thread(() -> {while (count < 20) {try {TimeUnit.MILLISECONDS.sleep(1);count++;log.debug("count: {}", count);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "t2").start();}
}

两个线程谁都无法结束。

饥饿

一个线程优先级太低,导致一直得不到 CPU 的调度执行,也不能结束。

哲学家就餐问题修改条件,把:

new Philosopher("IKUN", c5, c1).start();

改为

new Philosopher("IKUN", c1, c5).start();

其他进程一直得不到执行,其他线程饥饿。

JUC P1 进程,线程,管程 基础+代码相关推荐

  1. Linux的进程/线程/协程系列4:进程知识深入总结:上篇

    Linux的进程/线程/协程系列4:进程/线程相关知识总结 前言 本篇摘要: 1. 进程基础知识 1.1 串行/并行与并发 1.2 临界资源与共享资源 1.3 同步/异步与互斥 1.4 进程控制原语 ...

  2. linux的进程/线程/协程系列5:协程的发展复兴与实现现状

    协程的发展复兴与实现现状 前言 本篇摘要: 1. 协同制的发展史 1.1 协同工作制的提出 1.2 自顶向下,无需协同 1.3 协同式思想的应用 2. 协程的复兴 2.1 高并发带来的问题 2.2 制 ...

  3. linux的进程/线程/协程系列3:查看linux内核源码——vim+ctags/find+grep

    linux的进程/线程/协程系列3:查看linux内核源码--vim+ctags/find+grep 前言 摘要: 1. 下载linux内核源码 2. 打标签方法:vim+ctags 2.1 安装vi ...

  4. linux的进程/线程/协程系列1:进程到协程的演化

    linux的进程/线程/协程系列1:进程到协程的演化 前言 摘要: 1. 一些历史:批处理时代 2. 现代操作系统启动过程 3. 进程(process)的出现 4. 线程(thread)与线程池 5. ...

  5. 简要说明__python3中的进程/线程/协程

    多任务可以充分利用系统资源,极大提升程序运行效率,多任务的实现往往与 多线程,多进程,多协程有关 稳定性: 进程 > 线程 > 协程 系统资源占用量:进程 > 线程 > 协程 ...

  6. Python之进程+线程+协程(异步、selectors模块、阻塞、非阻塞IO)

    文章目录 一.IO多路复用 二.selectors模块 本篇文字是关于IO多路复用的更深入一步的总结,上一篇 Python之进程+线程+协程(事件驱动模型.IO多路复用.select与epoll)对I ...

  7. 进程 线程 协程 各自的概念以及三者的对比分析

    文章目录 1 进程 2 线程 3 进程和线程的区别和联系 3.1 区别 3.2 联系 4 举例说明进程和线程的区别 5 进程/线程之间的亲缘性 6 协程 线程(执行一个函数)和协程的区别和联系 协程和 ...

  8. java基础巩固-宇宙第一AiYWM:为了维持生计,四大基础之OS_Part_1整起(进程线程协程并发并行、进程线程切换进程间通信、死锁\进程调度策略、分段分页、交换空间、OS三大调度机制)

    PART0:OS,这货到底是个啥? OS,是个啥? OS的结构们: 存储器: 存储器的层次结构: 内存:我们的程序和数据都是存储在内存,我们的程序和数据都是存储在内存,每一个字节都对应一个内存地址.内 ...

  9. 4.19 python 网络编程和操作系统部分(TCP/UDP/操作系统概念/进程/线程/协程) 学习笔记

    文章目录 1 网络编程概念 1)基本概念 2)应用-最简单的网络通信 2 TCP协议和UDP协议进阶(网络编程) 1)TCP协议和UDP协议基于socket模块实现 2)粘包现象 3)文件上传和下载代 ...

最新文章

  1. hdu 1286 找新朋友 (容斥原理 || 欧拉函数)
  2. 服务器存档修改,云服务器存档修改器
  3. numpy 归一化_NumPy 数据归一化、可视化
  4. 历史上的今天:首条海底光缆开通;VeriSign 收购 Network Solutions;计算机图形学先驱诞生...
  5. css3弧形跑道效果_【Tableau 图表】你是不是真的需要一个跑道图呢?
  6. 第十周Java学习总结
  7. ad中pcb双面板怎么设置_html中表格tr的td单元格怎么设置宽度属性
  8. php函数获取数据库中的表格,初步了解PHP获取数据库表信息函数_PHP教程
  9. python rsi_使用python与rsi进行算法交易
  10. Java仓储物流项目_基于jsp的物流仓库管理系统-JavaEE实现物流仓库管理系统 - java项目源码...
  11. 计算机体系结构实验1——计算机性能评测
  12. 【工具类】数据脱敏工具类
  13. 键盘上特殊符号的中英文名称
  14. 电商时代得流量者得天下,思域流量要怎么做
  15. 学生个人网页设计作品 学生个人网页模板简单个人主页成品 个人网页制作 HTML学生个人网站作业设计
  16. Acwing:COW(DP+状态机 Python)
  17. @Before, @BeforeClass, @BeforeEach 和 @BeforeAll之间的不同
  18. 4.Python复杂数据类型之字典
  19. 前端——》手机H5页面九宫格抽奖(含概率及奖品配置)
  20. js 根据时间戳格式化为24小时的日期形式

热门文章

  1. 物联网开源数据库分析归纳
  2. C++ 多线程 如何避免死锁
  3. bim综合免费插件:Revit中运用报告参数的方法
  4. 基于JS的金铲铲工资结算系统的设计与开发
  5. Linux操作系统桌面环境GNOME和KDE的切换
  6. 【Oracle 数据库】奶妈式教程 day09 子查询
  7. 只售卖一种可乐的自动售货机
  8. 视频教程-micropython基础入门(esp32/esp8266单片机开发)-物联网技术
  9. Gif合成透明PNG变成黑色背景GIF问题解决
  10. 数据解读 | 这届年轻人为什么开始在B站看刑法了?