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

文章目录

  • 前言
    • [一、 变量的线程安全分析](#一、 变量的线程安全分析)
      • [1.1 成员变量与静态变量是否线程安全?](#1.1 成员变量与静态变量是否线程安全?)
      • [1.2 局部变量是否线程安全?](#1.2 局部变量是否线程安全?)
      • [1.3 局部变量线程安全分析](#1.3 局部变量线程安全分析)
      • 具体举例:
        • [1. 局部变量引用的对象没有逃离方法作用域 :](#1. 局部变量引用的对象没有逃离方法作用域 :)
        • [2. 局部变量引用的对象逃离了方法作用域 :](#2. 局部变量引用的对象逃离了方法作用域 :)
      • [1.4 常见线程安全类](#1.4 常见线程安全类)
    • [二、 Monitor概念](#二、 Monitor概念)
    • [三、 wait notify概念](#三、 wait notify概念)
      • [3.1 基本概念](#3.1 基本概念)
      • [3.2 api介绍](#3.2 api介绍)
    • [四、 wait notify正确使用方法](#四、 wait notify正确使用方法)
      • [4.1 sleep(long n) 和 wait(long n) 区别](#4.1 sleep(long n) 和 wait(long n) 区别)
      • [4.2 step 1](#4.2 step 1)
      • [4.3 step2](#4.3 step2)
      • [4.4 step3 - 4](#4.4 step3 - 4)
      • [4.5 step5](#4.5 step5)
      • [4.6 wait - notify正确模板格式](#4.6 wait - notify正确模板格式)
    • 五、park&unpark
      • [5.1 基本使用](#5.1 基本使用)
      • [5.2 特点](#5.2 特点)
      • [5.3 原理](#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();

        // 创建两个线程,它们同时调用 incrementAndGet
        Thread 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

相关推荐
CodeClimb5 分钟前
【华为OD-E卷-寻找关键钥匙 100分(python、java、c++、js、c)】
java·python·华为od
CodeClimb5 分钟前
【华为OD-E卷-通信误码 100分(python、java、c++、js、c)】
java·python·华为od
Seven_cm11 分钟前
JDK21执行java -jar xxx.jar 文件时 “An unexpected error occurred” 问题处理
java·开发语言·jar
奔跑草-12 分钟前
【服务器】MyBatis是如何在java中使用并进行分页的?
java·服务器·mybatis
In 202925 分钟前
堆【Lecode_HOT100】
java·算法·排序算法
一起学习计算机27 分钟前
29、基于springboot的网上购物商城系统研发
java·spring boot·后端
CodeClimb28 分钟前
【华为OD-E卷-最多提取子串数目 100分(python、java、c++、js、c)】
java·python·华为od
lisacumt31 分钟前
[java] 简单的熔断器scala语言案例
java·开发语言·scala
CodeClimb32 分钟前
【华为OD-E卷-箱子之字形摆放 100分(python、java、c++、js、c)】
java·python·华为od
Qzer_40732 分钟前
JVM中的方法绑定机制
java·开发语言·jvm