java线程共享模型之管程(synchronized原理、wait-notify、park方法)

news/2024/12/21 17:24:04/

文章目录

  • 前言
    • 一、 变量的线程安全分析
      • 1.1 成员变量与静态变量是否线程安全?
      • 1.2 局部变量是否线程安全?
      • 1.3 局部变量线程安全分析
      • 具体举例:
        • 1. 局部变量引用的对象没有逃离方法作用域 :
        • 2. 局部变量引用的对象逃离了方法作用域 :
      • 1.4 常见线程安全类
        • 多个方法组合调用 :
        • 关键点:
        • 不可变类线程安全
    • 二、 Monitor概念
      • 2.1 Java对象头
      • 2.2 Monitor(锁)
      • 2.3 synchronized原理(1)
      • 2.4 synchronized原理(2)
        • ①、轻量级锁
        • ②、锁膨胀
        • ③、自旋优化
        • ④、偏向锁
          • (1)偏向状态
          • (2)撤销
          • (3)批量重偏向
          • (4)批量撤销
        • ⑤、锁消除
    • 三、 wait notify概念
      • 3.1 基本概念
      • 3.2 api介绍
    • 四、 wait notify正确使用方法
      • 4.1 sleep(long n) 和 wait(long n) 区别
      • 4.2 step 1
      • 4.3 step2
      • 4.4 step3 - 4
      • 4.5 step5
      • 4.6 wait - notify正确模板格式
    • 五、park&unpark
      • 5.1 基本使用
      • 5.2 特点
      • 5.3 原理

前言


本章主要整理的synchronized的原理,其中设计对象头中monitor的知识,其中,waitSet涉及wait - notify方法,然后,重点刨析了synchronized中的好几种锁对应的流程,在最后,顺便整理了一下park&unpark方法。


一、 变量的线程安全分析

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

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

1.2 局部变量是否线程安全?

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

1.3 局部变量线程安全分析

  • 局部变量

在这里插入图片描述

在这里插入图片描述

  • 局部变量引用的对象

局部变量引用的对象是否线程安全
如果一个局部变量引用的对象没有逃离方法作用域,即这个对象只在当前方法内使用,且不会被其他线程访问或持有,那么它是线程安全的。
如果该对象逃离了方法作用域(例如被返回,或者作为共享数据传递给了外部),那么它可能会被多个线程访问和修改,从而导致线程安全问题。

具体举例:

1. 局部变量引用的对象没有逃离方法作用域 :

在这种情况下,对象在方法内部使用完后就消失了,因此不涉及线程安全问题。

java">class ThreadSafeLocal {public void process() {String str = "Hello";  // 局部变量,线程安全str = str + " World";  // 字符串是不可变的,操作是线程安全的System.out.println(str);  // 每个线程有自己的局部副本}
}

在这个例子中,str 是局部变量,每个线程调用 process() 时,都会有自己的 str 变量副本。并且 str 引用的 String 是不可变的,内部操作不会影响其他线程。因此,线程是安全的。

2. 局部变量引用的对象逃离了方法作用域 :

如果局部变量引用的对象被传递到方法外部,或者被多个线程共享访问,那么这个对象可能会出现线程安全问题。

java">class SharedObject {private StringBuilder sb = new StringBuilder();public StringBuilder getSb() {return sb;  // sb 被返回到方法外部,可能被多个线程访问}
}class ThreadUnsafeLocal {public void process() {SharedObject sharedObj = new SharedObject();StringBuilder sb = sharedObj.getSb();  // sb 被传递到外部sb.append(" World");  // 多线程环境下会发生竞争条件System.out.println(sb.toString());}
}

在这个例子中,sb 是局部变量,但它引用的 StringBuilder 对象是从 SharedObject 返回的,并且可能会被多个线程共享访问。StringBuilder 是可变的,因此多个线程同时对它进行操作时,会发生竞态条件,导致数据错误。

1.4 常见线程安全类


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

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

  • 它们每个方法是原子的
  • 但注意它们多个方法的组合不是原子的。
多个方法组合调用 :

假设我们有一个 Counter 类,它包含两个方法:increment()getCount()increment() 会增加计数器的值,而 getCount() 会返回当前的计数值。现在我们想通过 increment()getCount() 的组合来增加计数器的值并获取最新的计数。

如果没有同步机制,多个线程同时调用 increment()getCount() 方法时,可能会导致结果不一致,因为这些方法的组合操作(即获取计数值并更新)并不是原子的。

java">class Counter {private int count = 0;public void increment() {count++;  // 不是原子的}public int getCount() {return count;  // 也是线程安全的,但它只读取,不会修改}public void incrementAndGet() {increment();System.out.println(getCount());  // 方法组合不是原子的}
}public class ThreadUnsafeExample {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();// 创建两个线程,它们同时调用 incrementAndGetThread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.incrementAndGet();  // 增加计数并打印}});Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.incrementAndGet();  // 增加计数并打印}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + counter.getCount());  // 可能不会是 2000}
}

incrementAndGet():该方法组合了两个操作:首先调用 increment(),然后调用 getCount()。即使每个方法内部是线程安全的(getCount() 只是读取数据,没有修改),方法的组合操作仍然不是线程安全的。因为在 increment() 执行时,如果有多个线程同时调用这个组合方法,它们会竞争修改 count 的值,导致错误的最终结果。

关键点:
  • 方法内部是原子操作:每个方法(如 getCount())单独执行时是线程安全的。
  • 多个方法的组合:当多个方法依赖共享资源(例如 count)并且组合执行时,没有适当的同步机制,它们的组合操作就不是原子的,容易出现竞态条件,导致线程安全问题。
不可变类线程安全
  • 例如String,它在改变的时候会被重新复制一份,不会对原来的对象进行修改,因此线程安全

二、 Monitor概念

2.1 Java对象头

  • 在32位虚拟机上 :

在这里插入图片描述

在这里插入图片描述

  • 64位虚拟机则是在32位的基础上翻倍即可。

2.2 Monitor(锁)

Monitor被翻译为 监视器管程

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

在这里插入图片描述

  • 刚开始Monitor中的Owner为null
  • 当Thread2执行时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是之前 获得过锁,但是条件不满足进入WAITTING状态的线程。

在这里插入图片描述

2.3 synchronized原理(1)

java">static final Object lock = new Object();
static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter ++;}
}

对应的字节码为 :

在这里插入图片描述

2.4 synchronized原理(2)

①、轻量级锁

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

②、锁膨胀

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

③、自旋优化

在这里插入图片描述

在这里插入图片描述

④、偏向锁

在这里插入图片描述

(1)偏向状态

在这里插入图片描述

在这里插入图片描述

(2)撤销

在这里插入图片描述


在这里插入图片描述

实现 :

java">Dog d = new Dog();new Thread(() ->{log.debug(ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintable());}log.debug(ClassLayout.parseInstance(d).toPrintable());synchronized (TestBiased.class) {TestBiased.class.notify();}}, "t1").start();new Thread(() ->{synchronized (TestBiased.class) {try {TestBiased.class.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}log.debug(ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintable());}log.debug(ClassLayout.parseInstance(d).toPrintable());
}, "t2").start();

运行结果

在这里插入图片描述

1. 初始状态(线程 t1

20:48:31.674 c.TestBiased [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
  • 这是对象 d 的初始状态。

  • 对象头部解释

    • 低三位 101:表示 无锁状态(JVM 默认未加锁的对象会显示为 101)。
    • 剩余部分:未使用,具体值根据 JVM 的实现可能是对象分代相关的标识。

在这一时刻,d 尚未被加锁。


2. 第一次加锁(线程 t1

20:48:31.677 c.TestBiased [t1] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
  • t1 中执行了 synchronized (d),此时线程对对象 d 加锁。

  • 对象头部解释

    • 对象头部分的中间位发生变化,其中存储的是 线程 ID偏向锁信息
    • 偏向锁标志位:仍然显示为 101,这表明对象处于 偏向锁状态
    • 偏向锁意味着该对象被特定的线程持有锁(t1 持有),而未升级为轻量级锁或重量级锁。

3. 释放锁后(线程 t1

20:48:31.677 c.TestBiased [t1] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
  • t1 中锁被释放,但对象的头部没有明显变化。
  • 偏向锁的特性是线程释放锁时,偏向锁状态不会立即被撤销。这是因为 JVM 试图优化加锁性能,在后续没有竞争的情况下,可以直接重新偏向到同一个线程。

4. 第二个线程初始读取状态(线程 t2

20:48:31.677 c.TestBiased [t2] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
  • 线程 t2 唤醒后读取了对象 d 的状态。
  • 对象仍处于偏向锁状态,偏向锁仍然指向线程 t1,但 t2 尚未加锁。

5. 第二个线程加锁后(线程 t2

20:48:31.678 c.TestBiased [t2] - 00000000 00000000 00100000 01010101 11110011 00100000 00000000 00100000
  • 线程 t2 对对象 d 加锁。

  • 对象头部解释

    • 偏向锁被撤销,锁升级为 轻量级锁重量级锁
    • 显示了不同于偏向锁的信息,表示 t2 持有了对象的锁。
    • 具体升级为轻量级锁还是重量级锁,取决于 JVM 的实现和锁竞争的激烈程度。

6. 第二个线程释放锁后(线程 t2

20:48:31.678 c.TestBiased [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
  • 线程 t2 释放锁。

  • 对象头部解释

    • 回到了无锁状态(101)。
    • 对象头中保存的锁相关信息被清空。
(3)批量重偏向

在这里插入图片描述

(4)批量撤销

在这里插入图片描述

⑤、锁消除

下面表示没有用锁消除优化,上面是用锁优化的情况。

在这里插入图片描述

三、 wait notify概念

3.1 基本概念

在这里插入图片描述

3.2 api介绍

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

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

java">private static final Object obj = new Object();
public static void main(String[] args) {new Thread(() -> {synchronized (obj) {log.debug("线程开始执行...");try {obj.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("其他代码...");}}, "t1").start();new Thread(() -> {synchronized (obj) {log.debug("线程开始执行...");try {obj.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("其他代码...");}}, "t2").start();sleep(2);log.debug("唤醒其它线程:");synchronized (obj) {//obj.notify();obj.notifyAll();}
}

结果 :

notify 的结果 :

在这里插入图片描述

notifyAll 的结果 :

在这里插入图片描述

四、 wait notify正确使用方法

4.1 sleep(long n) 和 wait(long n) 区别

  • sleep是Thread方法,而wait是Object方法

  • sleep不需要强制和sychronized配合使用,但是wait需要和synchronized一起用

  • sleep在睡眠的同时,不会释放对象锁,但wait的时候会释放对象锁。

  • 它们状态是一样的,都是TIMED_WAITTING

4.2 step 1

错误示范 :

java">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("没烟,先歇会!");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();
}

结果 :

在这里插入图片描述

这种方法的问题所在。

  • 是小南睡眠期间,线程阻塞,其它人都得等着。这就导致了任务运行的效率不高。
  • 小南线程必须睡够两秒,就算烟提前送过来,也无法醒来
  • 加了synchronized(room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main没加synchronized就好像main是翻窗户进来。
  • 解决方法 : 使用wait - notify方法。

4.3 step2

只需要改成使用wait方法

java">new Thread(() -> {synchronized (room) {log.debug("有烟没?[{}]", hasCigarette);if (!hasCigarette) {log.debug("没烟,先歇会!");try {room.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}log.debug("有烟没?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以开始干活了");}}
}, "小南").start();
java">new Thread(() -> {// 这里能不能加 synchronized (room)?synchronized (room) {hasCigarette = true;log.debug("烟到了噢!");room.notifyAll();}
}, "送烟的").start();

结果 :

在这里插入图片描述

  • 解决了其它干活线程的阻塞的问题
  • 但如果有其它线程也在等待条件呢?

4.4 step3 - 4

java">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();}

运行结果 :

notify :

在这里插入图片描述

  • 此时,造成了虚假唤醒的情况,原本想要小女继续干活,结果成了唤醒小南,但是小南继续运行的条件不满足,导致了虚假唤醒

notifyAll :

在这里插入图片描述

  • 使用notifyAll 就可以都唤醒了。小女正常了,但是会导致小南没干成活,我们在step5中继续看。

4.5 step5

java">if (!hasTakeout) {log.debug("没外卖,先歇会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}
}

改成 :

java">while (!hasTakeout) {log.debug("没外卖,先歇会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}
}

运行结果 :

在这里插入图片描述

  • 这样被唤醒的时候,是符合被唤醒条件小女继续执行,而小南可以重新进入waitSet中等待。

4.6 wait - notify正确模板格式

java">synchronized(lock) {while(条件判断) {lock.wati();}// 干活
}// 另一个线程
synchronized(lock) {lock.notifyAll();
}

五、park&unpark

5.1 基本使用

它们都是LockSupport中的方法 :

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

park跟wait-notify类似,但是有一个重要区别,如下 :

java">public static void main(String[] args) {Thread t1 = new Thread(() -> {log.debug("start...");sleep(2);log.debug("park...");LockSupport.park();log.debug("resume...");}, "t1");t1.start();sleep(1);log.debug("unpark...");LockSupport.unpark(t1);
}

运行结果 :

在这里插入图片描述

特点就是,如果在调用park方法之前调用过unpark方法,那么后续就可以恢复线程继续运行。

5.2 特点

在这里插入图片描述

5.3 原理

先park再unpark

在这里插入图片描述

在这里插入图片描述

先unpark再park

在这里插入图片描述


http://www.ppmy.cn/news/1556976.html

相关文章

C++设计模式:组合模式(公司架构案例)

组合模式是一种非常有用的设计模式&#xff0c;用于解决**“部分-整体”**问题。它允许我们用树形结构来表示对象的层次结构&#xff0c;并且让客户端可以统一地操作单个对象和组合对象。 组合模式的核心思想 什么是组合模式&#xff1f; 组合模式的目的是将对象组织成树形结…

多音轨视频使用FFmpeg删除不要音轨方法

近期给孩子找宫崎骏动画&#xff0c;但是有很多是多音轨视频但是默认的都是日语&#xff0c;电视上看没办法所以只能下载后删除音轨文件只保留中文。 方法分两步&#xff0c;先安装FFmpeg在转文件即可。 第一步FFmpeg安装 FFmpeg是一个开源项目&#xff0c;包含了处理视频的…

HTMLCSS:酷炫的3D开关控件

这段代码创建了一个具有 3D 效果的开关控件&#xff0c;当用户点击滑块时&#xff0c;滑块会移动到开关的另一侧&#xff0c;同时改变背景颜色&#xff0c;模拟开关的开启和关闭状态。动画效果增加了页面的互动性和视觉吸引力。 演示效果 HTML&CSS <!DOCTYPE html>…

React 工具和库面试题(一)

1. 如何在 React 项目中使用 Hooks 从服务端获取数据&#xff1f; 在 React 中&#xff0c;我们通常使用 useEffect Hook 来进行副作用操作&#xff0c;比如从服务端获取数据&#xff0c;结合 useState 来管理数据状态。 基本步骤&#xff1a; 使用 useEffect 来执行异步操作…

BERT模型入门(2)BERT的工作原理

文章目录 如名称所示&#xff0c;BERT&#xff08;来自Transformer的双向编码器表示&#xff09;是基于Transformer模型。我们可以将BERT视为只有编码器部分的Transformer。 在上一个主题《Transformer入门》中&#xff0c;我们了解到将句子作为输入喂给Transformer的编码器&a…

MLM: 掩码语言模型的预训练任务

MLM: 掩码语言模型的预训练任务 掩码语言模型&#xff08;Masked Language Model, MLM&#xff09;是一种用于训练语言模型的预训练任务&#xff0c;其核心目标是帮助模型理解和预测语言中的上下文关系。以下是对这一概念的详细说明&#xff1a; 基本定义&#xff1a; MLM是一…

JAVA进制转换-对不同位数的转换方法

JAVA进制转换-对不同位数的转换方法 实例结果代码补叙 实例 第一个输入参数设为被转换的数值&#xff0c;第二个输入参数设为源来的位数&#xff0c;第三个输入参数设为目标的位数。 /*** 位数转换* args[0] 被转换值* args[1] 源位数* args[2] 目标位数*/public static vo…

Python 助力 DBA:高效批量管理数据库服务器的多线程解决方案-多库查询汇总工具实现

批量数据库服务器连接测试与数据汇总&#xff1a;Python实现方案 作为数据库服务器运维人员&#xff0c;我们经常需要面对大量服务器的连接测试和数据汇总工作。本文将介绍一个使用Python实现的高效解决方案&#xff0c;可以帮助我们快速完成这些任务。 需求概述 从配置文件…