Java JUC(二) Synchronized 基本使用与核心原理

news2024/11/15 19:30:20

Java JUC(二) Synchronized 基本使用与核心原理

随着 Java 多线程开发的引入,程序的执行效率和速度都取得了极大的提升。但此时,多条线程如果同时对一个共享资源进行了非原子性操作则可能会诱发线程安全问题,而线程安全问题将会导致潜在的数据和行为错误。上述概念中包含了三个关键要素即:多线程共享资源(临界资源)非原子性操作,这也是线程安全问题产生的根本原因。因此,锁机制的出现就是为了确保多线程对共享资源的安全访问,控制线程之间的协作关系。接下来,本文将介绍最常用的 Synchronized 锁的基本使用与底层原理。

一. Synchronized 基本使用

synchronized关键字是Java中用于实现线程同步的一种常用机制,其加锁目标是类或实例对象。它的主要作用是确保在同一时刻,只有一个线程可以执行某个方法或代码块,从而避免因多个线程同时访问共享资源时所引发的数据不一致问题。其特点如下:

  • 可重入锁:synchronized是可重入锁,即线程可以重复获取已持有的synchronized锁;
  • 可见性与原子性:synchronized既能保证数据的可见性,也能保证修饰方法对数据操作的原子性,但相比volatile来说其不能禁止指令重排序;
  • 非公平锁: 获取锁的机会是随机或非顺序的;

1. 基本用法

1.1 修饰实例方法

获取当前对象实例的锁即this锁,线程想要执行被Synchronized关键字修饰的普通实例方法,必须先获取当前实例对象的锁资源。

public synchronized void lockInstance() {
    System.out.println("锁的是当前对象实例this");
}

注意: 当一个线程正在访问一个被synchronized修饰的实例方法时,即当一个线程获取了该对象的锁之后,其他线程将无法获取该对象的锁,所以无法访问该对象的其他被synchronized修饰的对象实例方法。但是如果有其他方法未被synchronized修饰,又或者其他被synchronized修饰的是静态方法,则这类方法是可以被其他线程所访问的。

1.2 修饰静态方法

获取当前类对象的锁即class锁,线程想要执行被Synchronized关键字修饰的静态方法,必须先获取当前类对象(唯一)的锁资源。

public synchronized static void lockClass() {
    System.out.println("锁的是当前类class");
}

注意: 静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥(如上述);因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问实例 synchronized 方法占用的锁是当前实例对象锁。

1.3 修饰代码块

synchronized修饰代码块(划分细粒度)时,可以对括号内指定的对象实例或类加锁,表示进入同步代码块前要先获得给定对象/类Class的锁资源。其格式如下:

  • synchronized(Object object) {...}

  • synchronized(Class.class) {...}

(1)实例对象锁(Object)

public void lockObjectWithBlock(Object obj) {
    synchronized (obj) {
        System.out.println("锁的是指定的对象实例obj");
    }
}
public void lockObjectWithBlock() {
    synchronized (this) {
        System.out.println("锁的是当前对象实例this");
    }
}

(2)类对象锁(class)

public void lockclassWithBlock() {
    synchronized (A.class) {
        System.out.println("锁的是指定类A.class");
    }
}

2. wait与notify

wait()notify() 相关方法是 Java 的 Object 顶级父类中的方法,它们基于底层monitor对象监视器实现(后面会说),因此调用线程必须拥有该对象的监视器(即对象锁)才能调用方法,即必须在同步块或同步方法(synchronized 中被调用。在应用中,它们常被用于实现多线程间的协作,处理线程间的通信问题。

TypeMethodDescription
voidwait()使当前(持锁)线程进入阻塞等待(进入WAITING状态),直到另一个线程为此对象调用notify方法或notifyAll方法唤醒。注意当前线程必须拥有此对象的监视器(锁)才能调用,并且调用后线程会释放monitor对象锁。在使用过程中可能会抛出以下两种异常:
- IllegalMonitorStateException:若当前线程未拥有对象的监视器锁;
- InterruptedException:若当前线程在唤醒之前被中断,并清除中断状态;
voidwait(long timeout)使当前(持锁)线程进入阻塞等待(进入TIMED_WAITING状态),直到另一个线程为此对象调用notify方法或notifyAll方法唤醒,或等待时间timeout(ms)过期;其他条件与wait方法保持一致。
voidnotify()唤醒正在等待该对象监视器锁的单个线程,若有多个线程在等待则唤醒哪个线程是不确定的(存在虚假唤醒问题);线程在被唤醒后不会立即获取锁,而是进入等待队列,待对象锁释放之后重新参与锁的竞争(→RUNNABLEBLOCKED)。注意当前线程同样必须拥有此对象的监视器(锁)才能调用此方法。
- IllegalMonitorStateException:若当前线程未拥有对象的监视器锁;
voidnotifyAll()唤醒正在等待该对象监视器锁的所有线程。其他条件与notify方法保持一致。

虚假唤醒问题:notify随机唤醒单个线程,notifyAll唤醒所有线程;在多线程环境下,二者都会导致某些未达到预期唤醒条件的线程提前苏醒,这就是虚假唤醒问题。要解决虚假唤醒问题,可以通过搭配while条件循环来判断,其示例如下:

/**
 * 场景模拟:奶茶店和咖啡店共用一个窗口(window)出餐,等待顾客点单...
 *  - 奶茶店(hasTeaWithMilk):顾客需要奶茶,则奶茶店开始工作;
 *  - 咖啡店(hasCoffee):顾客需要咖啡,则咖啡店开始工作;
 */
public class Test_02 {
    // 窗口锁
    static final Object window = new Object();
    // 奶茶点单标志
    static boolean hasTeaWithMilk = false;
    // 咖啡点单标志
    static boolean hasCoffee = false;
    public static void main(String[] args) throws InterruptedException {
        // 奶茶店监控线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (window){
                    System.out.println("[奶茶店] 查看顾客点单情况...");
                    while (!hasTeaWithMilk){
                        System.out.println("[奶茶店] 没有点奶茶,继续等待订单...");
                        try {
                            window.wait(); // 挂起等待,唤醒后从此继续执行(循环判断下次条件是否成立)
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("[奶茶店] 点单没?! " + hasTeaWithMilk);
                    if(hasTeaWithMilk){
                        System.out.println("[奶茶店] 开始工作...");
                    }else{
                        System.out.println("[奶茶店] 白被叫醒...");
                    }
                }
            }
        }).start();
        Thread.sleep(1000);
        // 咖啡店监控线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (window){
                    System.out.println("[咖啡店] 查看顾客点单情况...");
                    while (!hasCoffee){
                        System.out.println("[咖啡店] 没有点咖啡,继续等待订单...");
                        try {
                            window.wait(); // 挂起等待,唤醒后从此继续执行(循环判断下次条件是否成立)
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("[咖啡店] 点单没?! " + hasCoffee);
                    if(hasCoffee){
                        System.out.println("[咖啡店] 开始工作...");
                    }else{
                        System.out.println("[咖啡店] 白被叫醒...");
                    }
                }
            }
        }).start();
        Thread.sleep(1000);
        // 顾客点单线程: main
        synchronized (window){
            hasCoffee = true;
            System.out.println("[顾客] 点了咖啡!!");
            window.notifyAll(); //唤醒全部等待线程
        }
    }
}
[奶茶店] 查看顾客点单情况...
[奶茶店] 没有点奶茶,继续等待订单...
[咖啡店] 查看顾客点单情况...
[咖啡店] 没有点咖啡,继续等待订单...
[顾客] 点了咖啡!!
[咖啡店] 点单没?! true
[咖啡店] 开始工作...
[奶茶店] 没有点奶茶,继续等待订单...

二. Synchronized 核心原理

早期 Java 版本中,synchronized属于重量级锁,其直接关联到 monitor 监视器锁,依赖于底层操作系统的 Mutex Lock 来实现,线程之间的切换需要频繁从用户态转换到内核态,成本较高且效率较低。在Java 6之后,JVM 引入了偏向锁和轻量级锁两种锁机制,并进行了大量锁操作的优化(锁消除、锁粗化等),极大提高了锁的获取与释放效率。本节将着重介绍锁升级的实现,其他优化技术感兴趣的同学可以自行了解。注意: 本文涉及的JDK版本是1.8,JVM虚拟机以64位的HotSpot实现为准。

1. 对象内存布局

在 JVM 虚拟机中,Java 对象在内存中的布局可以分为 3 部分区域即:对象头实例数据对齐填充;其中:

  • 对象头: 存储 MarkWord(标记字段)和类型指针(对象指向其类元数据的指针,标识该对象是哪个类的实例);如果是数组对象,还会存储数组长度(ArrayLength);
  • 实例数据: 非静态成员数据的占用空间(包括父类的所有属性);
  • 对齐填充: 为了减少堆内存的碎片空间、提升IO性能,JVM 默认对每个对象做8byte的倍数填充;

在这里插入图片描述

在对象头部分,MarkWord(标记字段)用于存储对象运行时的基本信息,包括HashCodeGC分代年龄以及锁状态标志位等锁信息;而 synchronized 关键字的实现就与 MarkWord 的结构密不可分。64位 JVM 的Mark Word 组成示例如下:

在这里插入图片描述

2. 锁优化与锁升级

锁升级是结合实际经验针对不同竞争条件下的一种优化,synchronized 锁主要包括四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,随着线程竞争的激烈程度,锁会逐渐升级以应对不同的场景。注意:锁升级一般是单向的(由低级到高级),不可降级。

2.1 无锁状态

JDK1.6之后,JVM默认开启两个参数-XX:+UseBiasedLocking(表示启用偏向锁)、-XX:BiasedLockingStartupDelay=4000(表示JVM启动后延迟4000ms初始化匿名偏向锁);也就是说,在JVM启动后的前4000ms内创建的对象都是无锁状态,而匿名偏向锁初始化后创建的所有对象的对象头都为匿名偏向锁状态,其中匿名偏向锁是指锁状态为偏向锁时,偏向线程ID为空的特殊状态。注意该场景需要关注以下两点:

  • 为什么要有匿名偏向锁: 按照我个人的理解,偏向锁出现根本目的是为了减小在大部分场景下单个线程重复进入同步代码块时的开销,而偏向锁状态根据偏向线程ID字段是否为空又能天然表示当前是否存在偏向两个情况,无锁状态的存在此时多少显得没有什么意义。除此之外,提前进入匿名偏向锁可以简化锁升级过程、优化单线程访问下的同步开销;
  • 为什么要有启动延迟: JVM 启动时默认会进行大量sync操作,而偏向锁在面临同步和竞争时并不是都有利的,此时会发生大量的锁撤销和锁升级操作,极大降低JVM的启动效率;

综上所述,默认情况下JVM启动后4000ms内的普通无锁对象,以及4000ms后的匿名偏向锁对象都属于意义上的"无锁状态"。对于无锁状态的锁对象,如果有竞争发生则会直接进入轻量级锁;而匿名偏向锁对象则会尝试进行非匿名偏向锁的替换,也就是说只有匿名偏向的对象才能进入偏向锁模式

2.2 偏向锁状态

在大多数实际情况下(经验之谈),锁不仅很少存在多线程竞争,反而由同一线程重复获得的情况是最频繁的,因此为了减少先前单线程获取锁的代价与开销,从而引入了偏向锁机制。由上可知,偏向锁状态的应用场景是单线程访问临界区资源或进入同步代码块

偏向锁的核心思想是在偏向锁状态下,此时MarkWord结构中的偏向线程ID字段用于存储当前持有锁的单一线程,当线程请求获取锁时只需比较偏向线程ID字段是否与当前线程一致即可,这样同一线程重复获取锁时无需进行任何额外操作即可进入同步代码(记录重入次数),从而省去了大量有关锁申请的开销;注意:偏向锁申请后不会主动释放(即使线程退出了同步代码块甚至线程已终止),除非进入了偏向锁撤销/升级。 但是若线程请求获取锁时发现偏向线程不一致,则其处理过程如下:

  • S1: 判断锁是否仍处于偏向模式(101),若否则进入轻量级锁升级逻辑;
  • S2: 判断是否为匿名偏向锁或epoch过期,若是则尝试通过一次CAS进行偏向ID替换;
  • S3:CAS失败或偏向非当前线程,都说明出现了两个线程对锁的竞争,此时不管先前持有锁的线程是否已退出同步代码块或是否线程已终止,都会进入偏向锁的撤销和轻量级锁的升级逻辑(也有文章说此时会判断先前线程是否已退出同步代码块或已终止并尝试使用CAS替换以保持偏向锁状态,但我并不认同,实验如下);
//JOL库:用来查看对象头信息
import org.openjdk.jol.info.ClassLayout;
public class Test_01 {
    public static void main(String[] args) throws InterruptedException {
        // 等待JVM延迟启动
        Thread.sleep(5000);
        // 1.初始化匿名偏向锁对象 101
        Object obj = new Object();
        System.out.println("匿名偏向锁: ");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        // 2.单线程的偏向锁状态 101
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("t1 获得锁: ");
                    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 已退出同步代码块: ");
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            }
        });
        t1.start();
        // 3.偏向锁不释放 101
        t1.join();
        Thread.sleep(1000);
        System.out.println("t1 线程执行已结束: ");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        // 4.偏向锁升级轻量级锁 00
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("t2 获得锁: ");
                    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 已退出同步代码块: ");
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            }
        });
        t2.start();
        // 轻量级锁释放回到无锁状态 001
        t2.join();
        Thread.sleep(1000);
        System.out.println("t2 线程执行已结束: ");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}
/**
 * JOL输出的对象头信息:
 * 1.[object header: mark]: MarkWord(64位),默认输出是16进制的,需要转换成64位的2进制来看(后三位为锁标志位)
 * 2.[object header: class]: 类元指针
 * 3.[object alignment gap]: 对齐填充
 */
匿名偏向锁: 
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 获得锁: 
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000020cdf13d005 (biased: 0x000000008337c4f4; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 已退出同步代码块: 
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000020cdf13d005 (biased: 0x000000008337c4f4; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 线程执行已结束: 
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000020cdf13d005 (biased: 0x000000008337c4f4; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2 获得锁: 
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000c07ceff0b8 (thin lock: 0x000000c07ceff0b8)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2 已退出同步代码块: 
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2 线程执行已结束: 
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

最后,偏向锁并不都有利,其只适用于单个线程重入的场景,而对于多线程竞争的场景下只会增大开销。原因是:偏向锁的撤销需要进入safepoint(全局安全点),该过程开销较大。需要进入safepoint是由于,偏向锁的撤销需要到其他线程的栈帧中遍历寻找锁对象的lockRecord;而在非safepoint,栈帧是动态变化的,修改动态数据会引入更多的问题。目前看来,偏向锁存在的价值是为历史遗留的Collection类(如Vector和HashTable等)做优化,迟早药丸。因此,Java 15中默认不开启偏向锁。

2.3 轻量级锁状态

由经验数据可知:“对于绝大部分的锁,在整个同步周期内都不存在竞争”,因此轻量级锁的应用场景是多线程交替访问临界区资源或进入同步代码块。在轻量级锁状态下,作为锁的对象头中的MarkWord用于存储指向持有锁的线程栈中的锁记录(Lock Record)的指针,那么什么是LockRecordLockRecord是每个线程私有的数据结构,用于存储锁对象头中MardWord的拷贝(Displaced MarkWord)以及指向锁对象的指针,但一般LockRecord只有在轻量级线程状态才会真正发挥作用。

LockRecord的作用主要有两方面:一是保存元数据,即由于MarkWord的空间有限,随着对象状态的改变MarkWord的结构也会发生变化,这样原本存储在对象头里的一些信息(如HashCode、分代年龄等)就无法存储,为了保证数据不丢失,就会拷贝原MarkWord(无锁状态下)到线程栈中(称为Displaced Mark Word),并通过指针指向;二是实现轻量级锁的重入,即轻量级锁重入次数=栈中该锁对象的LockRecord数量,其中只有第一个无锁状态下的LockRecord会存储完整的Displaced Mark Word拷贝,其他重入LockRecordDisplaced Mark Word=NULL,只存指向锁对象的指针,用于标识重入次数。

举个例子

synchronized(obj){synchronized(obj){synchronized(obj){ ... }}}

该代码块会生成三个lockrecord,其中只有第一个lockrecord的Displaced Mark Word里有完整拷贝信息,其displaced_header低三位 = 001,即无锁状态;其他后面两个lockrecord的Displaced Mark Word = NULL,仅存储锁对象指针。

那么为什么轻量锁用lockrecord数量来统计重入次数?个人猜测是因为锁对象markword中放不下。

轻量级锁的核心思想是:在LockRecord拷贝完成后,线程通过CAS尝试将MarkWord的锁记录指针修改为指向自己(线程)的锁记录,然后将lockrecord的owner指向锁对象,若修改成功则当前线程将获得轻量级锁。进入轻量级锁有两种情况:

  • 无锁状态进入轻量级锁: 如上,使用CAS操作尝试将对象头的Markword更新为指向Lock Record的指针;
  • 偏向锁状态进入轻量级锁: 偏向锁撤销时,如果偏向线程仍在持有锁,则直接进入轻量级锁形式(safepoint无需CAS);

注意: 轻量级锁在退出同步代码块时,会主动释放锁并回到无锁状态,此时会检查markword中的lockrecord指针是否指向自己(获得锁的线程lockrecord),然后使用原子的CAS将Displaced Mark Word替换回对象头,如果成功则表示没有竞争发生,如果替换失败则会升级为重量级锁。

最后,关于轻量级锁进入重量级锁的时的行为,目前有两种主流观点,一是轻量级锁状态在竞争锁时会进行自旋,超过一定次数/自适应自旋则进入重量级锁升级;二是轻量级锁状态不会进行自旋,一旦发现多线程同时竞争就进入重量级锁升级,但反而在重量级锁初期会先尝试自适应自旋来竞争锁。两种观点的文章如下:

  • 轻量级锁状态自旋:浅析synchronized锁升级的原理与实现
  • 重量级锁状态自旋:Java锁与线程的那些事 (youzan.com)

但不管哪一种观点,两者都是在用户态进行自适应自旋,以尽可能减少同步操作带来的开销,没有太多本质上的区别。综上所述,轻量级锁适用于线程的交替执行场景:线程A进入轻量级锁,退出同步代码块并释放锁,会将锁对象恢复为无锁状态;线程B再进入锁,发现为无锁状态,会CAS尝试获取该锁对象的轻量级锁。

2.4 重量级锁状态

重量级锁的应用场景是多个线程同时访问临界区资源或进入同步代码块。在重量级锁状态下,作为锁的对象头中的MarkWord存储的是指向监视器monitor对象的指针;在HotSpot虚拟机中,monitor是由ObjectMonitor实现的,其主要数据结构如下:

ObjectMonitor() {
    _header       = NULL; //markOop对象头
    _count        = 0; //记录个数
    _waiters      = 0, //等待线程数
    _recursions   = 0; //重入次数
    _object       = NULL; //监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
    _owner        = NULL;  //指向获得ObjectMonitor对象的线程或基础锁
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ; 
    _Responsible  = NULL;
    _succ         = NULL;
    _cxq          = NULL;
    FreeNext      = NULL;
    _EntryList    = NULL; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ; // _owner is (Thread *) vs SP/BasicLock
    _previous_owner_tid = 0; // 监视器前一个拥有者线程的ID
}

重量级锁的核心思想是:给每个锁对象分配并绑定一个底层的monitor监视器对象。在其具体实现中,一个ObjectMonitor对象包括两个同步队列(_cxq 和 _EntryList) ,以及一个等待队列 _WaitSet。cxq、EntryList 、WaitSet都是由ObjectWaiter构成的链表结构(每个等待锁的线程都会被封装成ObjectWaiter对象)。重量级锁竞争时,请求线程会首先尝试获取锁,若加锁失败则当前节点加入cxq队列中阻塞等待(BLOCKED);当线程释放锁时(包括执行完同步代码块或调用了wait方法),则会从cxq或EntryList队列中按照策略(由QMode参数决定)选择一个线程unpark唤醒,重新参与锁的竞争(非公平锁,不一定成功)。当线程获取到对象的monitor后会进入Owner区域,并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count+1,表示获锁成功,从而继续执行后续的同步代码。

如果线程获得锁后调用Object#wait方法,则会将线程加入到WaitSet中,从而进入WAITING或TIMED_WAITING状态阻塞(该过程会释放锁并通过底层的park方法挂起线程);当被Object#notify唤醒后,会将WaitSet列表中的一个ObjectWaiter节点从WaitSet移动到cxq或EntryList队列中去,重新等待锁的等待竞争(notify并不意味着立即获取锁,仍需进入队列重新竞争)。需要注意的是,当调用一个锁对象的wait或notify方法时,若当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁,因为这两个方法都依赖于monitor实现。

综上所述,重量级锁会将线程放进等待队列,并交由操作系统进行调度,涉及用户态和内核态的转变,因此效率很低(相当于单线程执行)。除此之外,我们还需要注意一下在线程生命周期转换部分:BLOCKED状态的线程一定处于entryList或cxq中阻塞,而处于WAITINGTIMED WAITING的线程,可能是由于执行了sleep或park进入该状态(不一定在waitSet中)。

三. 实战应用

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

在这里插入图片描述

保护性暂停模式(Guarded Suspension)是一种并发设计模式,常用于实现线程间对通信结果的同步等待,属于同步模式。保护性暂停模式的核心思想是以GuardedObject作为中间锁对象,通过wait&notify方法实现线程的等待与唤醒,JDK中joinFuture的底层实现就是基于该设计模式。在我来看,该模式最大的作用就是实现解耦。带有超时时间的保护性暂停模式实现如下:

  • 未超时获取到结果: 被唤醒后由于result!=null,退出循环并返回结果;
  • 超时未获取到结果: 超时唤醒后发现waitTime==0break退出循环并结束;
  • 虚假唤醒: 被唤醒后由于result==null,继续等待剩余waitTime=timeout-passedTime时间;
// 测试类
public class Test_03 {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1 等待结果...");
                Object result = guardedObject.get(3000);
                System.out.println("t1 获取结果 = " + result);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 设置结果...");
                guardedObject.complete(null); // 模拟虚假唤醒
                System.out.println("t2 设置完成...");
            }
        }).start();
    }
}

//保护性暂停对象: 同步等待过程的解耦
class GuardedObject{
    // 目标结果
    private Object result = null;

    /**
     * 获取结果方法
     * @param timeout 最大等待时间(ms)
     */
    public Object get(long timeout){
        synchronized (this) {
            long begin = System.currentTimeMillis(); // 等待开始时间
            long passedTime = 0; // 已经等待时间
            while (result == null){
                long waitTime = timeout - passedTime; // 解决虚假唤醒问题
                if(waitTime <= 0){
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                passedTime = System.currentTimeMillis() - begin;
            }
            return result;
        }
    }

    /**
     * 设置结果方法
     * @param result 结果
     */
    public void complete(Object result) {
        synchronized (this) {
            this.result = result;
            this.notifyAll(); // 唤醒所有等待线程
        }
    }

}

2. 异步之生产者/消费者模式

在这里插入图片描述

以消息队列为例,生产者/消费者模式用于实现线程之间多对多的资源平衡问题,并且支持异步消费/生产。其特点如下:

  • 当生产者向消息队列中放入消息时,向其他等待的消费者发送可消费通知;当消息队列满后,生产者停止生产并等待消费。
  • 当消费者从消息队列中消费消息时,向其他等待的生产者发送可生产通知;当消息队列空后,消费者停止消费并等待生产。
// 测试类
public class test_04 {
    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(2);

        for(int i = 0;i < 3;i++){
            int id = i;
            new Thread(()->{
                messageQueue.put(new Message(id,"消息"+id));
            },"生产者"+i).start();
        }

        new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Message message = messageQueue.take();
            }
        },"消费者").start();
    }
}
// 消息类: 不可变数据
final class Message{
    private int id;
    private Object value;

    public Message(int id, Object value){
        this.id = id;
        this.value = value;
    }

    public int getId(){
        return id;
    }

    public Object getValue(){
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}
// 消息队列类
class MessageQueue{
    // 存储消息的集合
    private LinkedList<Message> valueList = new LinkedList<>();
    // 队列容量
    private int capacity;

    public MessageQueue(int capacity){
        this.capacity = capacity;
    }
    // 获取/消费消息方法
    public Message take(){
        synchronized (valueList){
            // 检查队列是否为空,为空则等待生产
            while(valueList.isEmpty()){
                try {
                    System.out.println(Thread.currentThread().getName() + " 队列为空,消费者线程等待...");
                    valueList.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 消费头部消息
            Message message = valueList.removeFirst();
            System.out.println(Thread.currentThread().getName() + " 已消费消息: " + message);
            // 通知生产者线程
            valueList.notifyAll();
            return message;
        }
    }
    // 存入/生产消息方法
    public void put(Message message){
        synchronized (valueList){
            // 检查队列是否已满,已满则等待消费
            while(valueList.size() == capacity){
                try {
                    System.out.println(Thread.currentThread().getName() + " 队列已满,生产者线程等待...");
                    valueList.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 生产消息插入队尾
            valueList.addLast(message);
            System.out.println(Thread.currentThread().getName() + " 已生产消息: " + message);
            // 通知消费者线程
            valueList.notifyAll();
        }
    }
}

生产者0 已生产消息: Message{id=0, value=消息0}
生产者2 已生产消息: Message{id=2, value=消息2}
生产者1 队列已满,生产者线程等待...
消费者 已消费消息: Message{id=0, value=消息0}
生产者1 已生产消息: Message{id=1, value=消息1}
消费者 已消费消息: Message{id=2, value=消息2}
消费者 已消费消息: Message{id=1, value=消息1}
消费者 队列为空,消费者线程等待...

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

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

相关文章

前端报错401 【已解决】

前端报错401 【已解决】 在前端开发中&#xff0c;HTTP状态码401&#xff08;Unauthorized&#xff09;是一个常见的错误&#xff0c;它表明用户试图访问受保护的资源&#xff0c;但未能提供有效的身份验证信息。这个错误不仅关乎用户体验&#xff0c;也直接关系到应用的安全性…

JAVA开源项目 学科竞赛管理系统 计算机毕业设计

本文项目编号 T 047 &#xff0c;文末自助获取源码 \color{red}{T047&#xff0c;文末自助获取源码} T047&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

修牛蹄视频哪里找?修牛蹄的解压视频素材网站分享

在现代农业和畜牧业中&#xff0c;牛蹄修剪技术的重要性不言而喻&#xff0c;不仅直接关联到牲畜的健康&#xff0c;也对农场的整体经济收益产生巨大影响。对于新手畜牧工作者而言&#xff0c;挑选出优秀的学习资源尤为关键。今天&#xff0c;我将为大家推荐几个提供优质牛蹄修…

烤羊肉串引来的思考——命令模式

文章目录 烤羊肉串引来的思考——命令模式吃烤羊肉串&#xff01;烧烤摊vs.烧烤店紧耦合设计命令模式松耦合设计进一步改进命令模式命令模式的作用 烤羊肉串引来的思考——命令模式 吃烤羊肉串&#xff01; 时间&#xff1a;6月23日17点  地点&#xff1a;小区门口  人物…

企业EMS -能源管理系统-能源在线监测平台

一、介绍 基于SpringCloud的能管管理系统-能源管理平台源码-能源在线监测平台-双碳平台源码-SpringCloud全家桶-能管管理系统源码 二、软件架构 二、功能介绍 三、数字大屏展示 四、数据采集原理 五、软件截图

在不受支持的 Mac 上安装 macOS Sequoia (OpenCore Legacy Patcher v2.0.1)

在不受支持的 Mac 上安装 macOS Sequoia (OpenCore Legacy Patcher v2.0.1) Install macOS on unsupported Macs 请访问原文链接&#xff1a;https://sysin.org/blog/install-macos-on-unsupported-mac/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主…

吉客云与金蝶云星空对接集成分页查询货品信息连通[标准]

吉客云与金蝶云星空对接集成分页查询货品信息连通[标准][付款单新增]-v1(付款单) 对接系统&#xff1a;吉客云 “吉客云”是一站式企业数字化解决方案系统&#xff0c;可实现业务、财务、办公、人事等一体化管理。相对于传统多套软件系统的集成方案&#xff0c;“吉客云”具有业…

剖析:基于 RDMA 的多机数据分发和接收场景

在基于 RDMA 的多机数据分发和接收场景中&#xff0c;数据的传输主要依赖于以下几个步骤和角色&#xff1a; 机器 A&#xff08;发送方&#xff09;&#xff1a;通过 RDMA 将数据直接写入远程机器的内存中。机器 B&#xff08;接收方&#xff09;&#xff1a;接收数据&#xf…

关于 NLP 应用方向与深度训练的核心流程

文章目录 主流应用方向核心流程&#xff08;5步&#xff09;1.选定语言模型结构2.收集标注数据3.forward 正向传播4.backward 反向传播5.使用模型预测真实场景 主流应用方向 文本分类文本匹配序列标注生成式任务 核心流程&#xff08;5步&#xff09; 基本流程实现的先后顺序…

ShardingSphere 分库分表

中间件 常用中间件 MyCat 是基于 Proxy&#xff0c;它复写了 MySQL 协议&#xff0c;将 Mycat Server 伪装成⼀个 MySQL 数据库客户端所有的jdbc请求都必须要先交给MyCat&#xff0c;再有 MyCat转发到具体的真实服务器缺点是效率偏低&#xff0c;中间包装了⼀层代码⽆侵⼊性…

解决Android Studio 右上角Gradle不显示task

解决Android Studio 右上角Gradle不显示task_gradle中没有build task-CSDN博客 不正常的情况下 正常的情况下 解决方案 依次点击&#xff1a;File -> Settings -> Experimental -> 取消勾选 “Do not build Gradle task list during Gradle sync” 同步项目即可

[数据集][目标检测]基于yolov5增强数据集算法mosaic来扩充自己的数据集自动生成增强图片和对应标注无需重新标注

【算法介绍】 YOLOv5最引人注目的增强技术之一是马赛克增强&#xff0c;它将四张不同的图像拼接成一张图像。 思路&#xff1a;首先&#xff0c;从数据集中随机选择四张图像&#xff0c;然后将它们缩放、随机裁剪&#xff0c;并按马赛克模式拼接在一起。这种方式允许模型看到…

【逐行注释】MATLAB的程序,对比EKF(扩展卡尔曼滤波)和PF(粒子滤波)的滤波效果,附下载链接

文章目录 总述部分源代码运行结果扩展性 总述 本代码使用 M A T L A B MATLAB MATLAB实现了扩展卡尔曼滤波&#xff08; E K F EKF EKF&#xff09;和粒子滤波&#xff08; P F PF PF&#xff09;在状态估计中的对比分析。 主要功能包括&#xff1a; 参数设置&#xff1a;初始…

Android Studio 开发快速获取开发版和发布版SHA1和MD5

本文讲解Android Studio 开发中如何快速获取开发版和发布版SHA1和MD5。 一、获取开发版: 点击Android Studio右上角Gradle按钮,打开Gradle视图 找到项目-Tasks-signingReport 双击即可AndroidStudio底部 Run面板获取开发版SHA1和MD5 二、获取发布版:

《深入解析 Java 中的 ThreadLocal》

ThreadLocal 1.概述 ThreadLocal被称为线程局部变量&#xff0c;用于在线程中保存数据。由于在ThreadLocal中保存的数据仅属于当前线程&#xff0c;所以该变量对其他线程而言是隔离的&#xff0c;也就是说该变量是当前线程独有的变量。 ThreadLocal用于在同一个线程间&#…

STM32CubeIDE | 使用HAL库的ADC读取内部传感器温度

1、cubemx配置 1.1、系统配置 1.2、GPIO配置 PB2设置为“GPIO_Output” user label设置为“LED” 1.3、串口配置 模式选择为“Asynchronous”&#xff0c;其他默认 1.4、时钟树配置 全部保持默认 2、ADC配置 通道选择“Temperature Sensor Channel”&#xff0c;其他默认 …

基于STM32的Zeta型数控电源设计

本设计基于STM32F103C6T6为主控芯片&#xff0c;基于Zeta型DC/DC电源的拓扑结构设计一种数控电源。系统包含单片机主控模块、Zeta型升降压模块、驱动模块、电流采样模块、电压采样模块、OLED显示模块、电源模块及按键模块。用电流采样模块采集电流&#xff0c;电压采样模块采集…

Skyvern:基于LLM和CV的开源RPA

Skyvern 使用 LLM 和计算机视觉来自动化基于浏览器的工作流程。它提供了一个简单的 API 端点&#xff0c;可以完全自动化大量网站上的手动工作流程&#xff0c;从而取代脆弱或不可靠的自动化解决方案。 传统的浏览器自动化方法需要为网站编写自定义脚本&#xff0c;通常依赖于…

基于小安派AiPi-Eyes-Rx的N合1触摸屏游戏

基于小安派AiPi-Eyes-Rx的N合1触摸屏游戏 目前存在的游戏&#xff1a; 植物大战僵尸&#xff1a;demos/pvz羊了个羊&#xff1a;demos/yang消消乐&#xff1a;demos/xiaoxiaole华容道&#xff1a;demos/huarongdao PVZ功能展示可见&#xff1a; 羊了个羊&#xff1a; 消消…

在多态的方法调用中为什么会出现“左边编译左边运行”的现象?多态创建的对象到底是谁属于父类还是子类?通过深扒集合remove方法调用理解其原理

目录 “左边编译左边运行”的两个原因&#xff1a; 什么是“编译看左边&#xff0c;运行看右边”&#xff1f; 为什么会出现“左边编译左边运行”现象&#xff1f; 1. 子类没有重写父类的方法 2. 重载与重写的混淆&#xff08;重难点&#xff09; 问题&#xff1a;编译器是…