从 JVM 中深入探究 Synchronized

智能气象站:ESP32驱动的心知天气数据可视化实践 本文详细介绍了基于ESP32微控制器和心知天气API构建智能气象站的完整实践。通过Arduino开发环境,实现天气数据的获取、JSON解析与OLED屏可视化展示,涵盖硬件连接、API配置、低功耗优化及部署问题排查,为物联网开发者提供实用指南。 阅读详情

开篇语

Synchronized,Java 友好的提供了的一个关键字,它让开发者可以快速的实现同步。它就像一个星星,远远看去就是一个小小的点。但是走近一看,却是一个庞大的蛋糕。而这篇文章就是要将这个巨大的蛋糕切开,吃进肚子里面去。

Synchronized 使用

在 Java 中,如果要实现同步,Java 提供了一个关键词 synchronized 来让开发人员可以快速实现同步代码块。

publicclassTest{

    publicstaticvoidmain(String[] args){
        Object o = new Object();

        Thread thread1 = new Thread(() -> {
            synchronized (o){
                System.out.println("获取锁成功");
            }
        }).start();
    }
}

线程 thread1 获取对象 o 的锁,并且输出一句话 “获取锁成功”。

publicclassTest{

    privateint i = 0;

    publicsynchronizedvoidset(int i){
        this.i = i;
    }

    publicsynchronizedstatic String get(){
        return"静态方法";
    }

    publicvoidput(){
        synchronized (this){
            System.out.println("同步代码块");
        }
    }
}

synchronized 关键字除了可以用于代码块,还可以用于方法上。用于实例方法上时,线程执行该方法之前,会自动获取该对象锁,获取到对象锁之后才会继续执行实例方法中的代码;用于静态方法上时,线程执行该方法之前,会自动获取该对象所属类的锁,获取到类锁之后才会继续执行静态方法中的代码。用于代码块上时,可以传入任意对象作为锁,并且可以控制锁的粒度。

synchronized 实现原理

下面是 Test 类的字节码文件

public class Test
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // Test
  super_class: #8                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 4, attributes: 1
Constant pool:
   #1 = Methodref          #8.#27         // java/lang/Object."<init>":()V#2 = Fieldref           #7.#28         // Test.i:I#3 = String             #29            // 静态方法#4 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;#5 = String             #32            // 同步代码块#6 = Methodref          #33.#34        // java/io/PrintStream.println:(Ljava/lang/String;)V#7 = Class              #35            // Test#8 = Class              #36            // java/lang/Object#9 = Utf8               i#10 = Utf8               I#11 = Utf8               <init>#12 = Utf8               ()V#13 = Utf8               Code#14 = Utf8               LineNumberTable#15 = Utf8               LocalVariableTable#16 = Utf8               this#17 = Utf8               LTest;#18 = Utf8               set#19 = Utf8               (I)V#20 = Utf8               get#21 = Utf8               ()Ljava/lang/String;#22 = Utf8               put#23 = Utf8               StackMapTable#24 = Class              #37            // java/lang/Throwable#25 = Utf8               SourceFile#26 = Utf8               Test.java#27 = NameAndType        #11:#12        // "<init>":()V#28 = NameAndType        #9:#10         // i:I#29 = Utf8               静态方法#30 = Class              #38            // java/lang/System#31 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;#32 = Utf8               同步代码块#33 = Class              #41            // java/io/PrintStream#34 = NameAndType        #42:#43        // println:(Ljava/lang/String;)V#35 = Utf8               Test#36 = Utf8               java/lang/Object#37 = Utf8               java/lang/Throwable#38 = Utf8               java/lang/System#39 = Utf8               out#40 = Utf8               Ljava/io/PrintStream;#41 = Utf8               java/io/PrintStream#42 = Utf8               println#43 = Utf8               (Ljava/lang/String;)V
{
  public Test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=10: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field i:I9: return
      LineNumberTable:
        line 5: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0100this   LTest;

  public synchronized void set(int);
    descriptor: (I)V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=20: aload_0
         1: iload_1
         2: putfield      #2                  // Field i:I5: return
      LineNumberTable:
        line 10: 0
        line 11: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            060this   LTest;
            061     i   I

  public static synchronized java.lang.String get();
    descriptor: ()Ljava/lang/String;
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=1, locals=0, args_size=00: ldc           #3                  // String 静态方法2: areturn
      LineNumberTable:
        line 14: 0

  public void put();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=10: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;7: ldc           #5                  // String 同步代码块9: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V12: aload_1
        13: monitorexit
        14: goto2217: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             41417   any
            172017   any
      LineNumberTable:
        line 18: 0
        line 19: 4
        line 20: 12
        line 21: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0230this   LTest;
      StackMapTable: number_of_entries = 2
        frame_type = 255/* full_frame */
          offset_delta = 17
          locals = [ class Test, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250/* chop */
          offset_delta = 4
}

我们通过查看字节码可以发现,synchronized 关键字作用在实例方法和静态方法上时,JVM 是通过 ACC_SYNCHRONIZED 这个标志来实现同步的。而作用在代码块时,而且通过指令 monitorenter 和 monitorexit 来实现同步的。monitorenter 是获取锁的指令,monitorexit 则是释放锁的指令。

对象头

通过上文我们已经知道,Java 要实现同步,需要通过获取对象锁。那么在 JVM中,是如何知道哪个线程已经获取到了锁呢?

要解释这个问题,我们首先需要了解一个对象的存储分布由以下三部分组成:

  • 对象头(Header) :由 Mark Word 和 Klass Pointer 组成

  • 实例数据(Instance Data) :对象的成员变量及数据

  • 对齐填充(Padding) :对齐填充的字节

Mark Word ****记录了对象运行时的数据:

  1. identity_hashcode:哈希码,只要获取了才会有

  1. age:GC分代年龄

  1. biased_lock: 1表示偏向锁,0表示非偏向锁

  1. lock 锁状态 :01 无锁/偏向锁;00 轻量级锁;10 重量级锁;11 GC 标志

  1. 偏向线程 ID

128bit (对象头)

状态

64bit Mark Word

64bit Klass Poiter

unused:25

identity_hashcode:31

unused:1

age:4

biased_lock:1

lock:2

无锁

threadId:54

epoch:2

unused:1

age:4

biased_lock:1

lock:2

偏向锁

ptr_to_lock_record:62

lock:2

轻量级锁

ptr_to_heavyweight_monitor:62

lock:2

重量级锁

lock:2

GC 标记

当线程获取对象锁的时候,需要先通过对象头中的 Mark Word 判断对象锁是否已经被其他线程获取,如果没有,那么线程需要往对象头中写入一些标记数据,用于表示这个对象锁已经被我获取了,其他线程无法再获取到。如果对象锁已经被其他线程获取了,那么线程就需要进入到等待队列中,直到持有锁的线程释放了锁,它才有机会继续获取锁。

当一个线程拥有了锁之后,它便可以多次进入。当然,在这个线程释放锁的时候,那么也需要执行相同次数的释放动作。比如,一个线程先后3次获得了锁,那么它也需要释放3次,其他线程才可以继续访问。这也说明使用 synchronized 获取的锁,都是可重入锁。

字节序

我们知道了对象头的内存结构之后,我们还需要了解一个很重要的概念:字节序。它表示每一个字节之间的数据在内存中是如何存放的?如果不理解这个概念,那么在之后打印出对象头时,也会无法跟上述展示的对象头内存结构相互对应上。

字节序:大于一个字节的数据在内存中的存放顺序。

注意!注意!注意!这里使用了大于,也就是说一个字节内的数据,它的顺序是固定的。
  • 大端序(BIG_ENDIAN):高位字节排在内存的低地址处,低位字节排在内存的高地址处。符合人类的读写顺序

  • 小端序(LITTLE_ENDIAN):高位字节排在内存的高地址处,低位字节排在内存的低地址处。符合计算机的读取顺序

我们来举个例子:

有一个十六进制的数字:0x123456789。

使用大端序阅读:高位字节在前,低位字节在后。

内存地址

1

2

3

4

5

十六进制

0x01

0x23

0x45

0x67

0x89

二进制

00000001

00100011

01000101

01100111

10001001

使用小端序阅读:低位字节在前,高位字节在后。

内存地址

1

2

3

4

5

十六进制

0x89

0x67

0x45

0x23

0x01

二进制

10001001

01100111

01000101

00100011

00000001

既然大端序符合人类的阅读习惯,那么统一使用大端序不就好了吗?为什么还要搞出一个小端序来呢?

这是因为计算机都是先从低位开始处理的,这样处理效率比较高,所以计算机内部都是使用小端序。其实计算机也不知道什么是大端序,什么是小端序,它只会按顺序读取字节,先读第一个字节,再读第二个字节。

Java 中的字节序

我们可以通过下面这一段代码打印出 Java 的字节序:

publicclassByteOrderPrinter {publicstaticvoidmain(String[] args){
        System.out.println(ByteOrder.nativeOrder());
    }
}

打印的结果为: LITTLE_ENDIAN。

因此,我们可以知道 Java 中的字节序为小端字节序。

如何阅读对象头

在理解了字节序之后,我们来看看如何阅读对象头。

首先,我们使用一个第三方类库 jol-core,我使用的是 0.10 版本,帮助我们打印出对象头的数据。

我们可以通过下面这一段代码打印出 Java 的对象头:

publicclassObjectHeaderPrinter{

    publicstaticvoidmain(String[] args)throws InterruptedException {

        Test test = new Test();

        System.out.println("=====打印匿名偏向锁对象头=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        synchronized (test){
            System.out.println("=====打印偏向锁对象头=====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }
    }

}

打印结果如下:

=====打印匿名偏向锁/无锁对象头=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           05000000 (00000101000000000000000000000000) (5)
      44        (object header)                           00000000 (00000000000000000000000000000000) (0)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====打印偏向锁对象头=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           05 a0 804b (00000101101000001000000001001011) (1266720773)
      44        (object header)                           01000000 (00000001000000000000000000000000) (1)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

我们把对象头的内存结构和对象头单独拿出来对照着解释一下:

128bit (对象头)

状态

64bit Mark Word

64bit Klass Poiter

unused:25

identity_hashcode:31

unused:1

age:4

biased_lock:1

lock:2

匿名偏向锁/无锁

threadId:54

epoch:2

unused:1

age:4

biased_lock:1

lock:2

偏向锁

ptr_to_lock_record:62

lock:2

轻量级锁

ptr_to_heavyweight_monitor:62

lock:2

重量级锁

lock:2

GC 标记

// 匿名偏向锁/无锁// 我们给每个字节都标上序号。
                a        b        c        d
05000000 (00000101000000000000000000000000) (5)
                e        f        g        h
00000000 (00000000000000000000000000000000) (0)
                i        j        k         l
506a 0600 (01010000011010100000011000000000) (420432)

unused:25 位,它实际上的字节应该是:hgf + e 的最高位。

identity_hashcode:31 位,它实际上的字节应该是:e 的低 7 位 + dcb。

unused:1位,它实际上的字节应该是:a 的最高位。

age:4位,它实际上的字节应该是:a的第 4-7 位

biased_lock:1位,它实际上的字节应该是:a的第 3 位

lock:2位,它实际上的字节应该是:a的低 2 位。

unused:25

identity_hashcode:31

unused:1

age:4

biased_lock:1

lock:2

hgf + e的最高位

e 的低 7 位 + dcb

a 的最高位

a的第 4-7 位

a的第 3 位

a的低 2 位

00000000 00000000 00000000 0

0000000 00000000 00000000 00000000

0

0000

1

01

我们再来看一个加了偏向锁的对象头:

// 偏向锁
                a        b        c        d
05900013 (00000101100100000000000000010011) (318803973)
                e        f        g        h
01000000 (00000001000000000000000000000000) (1)
                i        j        k        l
506a 0600 (01010000011010100000011000000000) (420432)

threadId:54

epoch:2

unused:1

age:4

biased_lock:1

lock:2

hgfedc + b 的高 6 位

b的低 2 位

a 的最高位

a的第 4-7 位

a的第 3 位

a的低 2 位

00000000 00000000 00000000 00000001 00010011 00000000 100100

00

0

0000

1

01

偏向锁

偏向锁是 Java 为了提高获取锁的效率和降低获取锁的代价,而进行的一个优化。因为 Java 团队发现大多数的锁都只被一个线程获取。基于这种情况,就可以认为锁都只被一个线程获取,那么就不会存在多个线程竞争的条件,因此就可以不需要真正的去获取一个完整的锁。只需要在对象头中写入获取锁的线程 ID,用于表示该对象锁已经被该线程获取。

获取偏向锁,只要修改对象头的标记就可以表示线程已经获取了锁,大大降低了获取锁的代价。

当线程获取对象的偏向锁时,它的对象头:

threadId:54

epoch:2

unused:1

age:4

biased_lock:1

lock:2

threadId:获取了偏向锁的线程 ID

epoch:用于保存偏向时间戳

age:对象 GC 年龄

biased_lock:偏向锁标记,此时为 1

lock:锁标记,此时为 10

获取偏向锁

线程获取对象锁时,首先检查对象锁是否支持偏向锁,即检查 biased_lock 是否为 1;如果为 1,那么将会检查threadId 是否为 null,如果为 null,将会通过 CAS 操作将自己的线程 ID 写入到对象头中。如果成功写入了线程 ID,那么该线程就获取到了对象的偏向锁,可以继续执行后面的同步代码。

只有匿名偏向的对象才能进入偏向锁模式,即该对象还没有偏向任何一个线程(不是绝对的,存在批量重偏向的情况)。

释放偏向锁

线程是不会主动释放偏向锁的。只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁。

释放偏向锁需要在全局安全点进行。释放的步骤如下:

  1. 暂停拥有偏向锁的线程,判断是否处于同步代码块中,如果处于,则进行偏向撤销,并升级为轻量级锁。

  1. 如果不处于,则恢复为无锁状态。

由此可以知道,偏向锁天然是可重入的。

偏向撤销

偏向撤销主要发生在多个线程存在竞争,不再偏向于任何一个线程了。也就是说偏向撤销之后,将不会再使用偏向锁。具体操作就是将 Mark Work 中的 biased_lock 由 1 设置为 0 。 偏向撤销需要到达全局安全点才可以撤销,因为它需要修改对象头,并从栈中获取数据。因此偏向撤销也会存在较大的资源消耗。

想要撤销偏向锁,还不能对持有偏向锁的线程有影响,所以就要等待持有偏向锁的线程到达一个 safepoint 安全点,在这个安全点会挂起获得偏向锁的线程。

  1. 如果原持有偏向锁的线程依然还在同步代码块中,那么就会将偏向锁升级为轻量级锁。

  1. 如果原持有偏向锁的线程已经死亡,或者已经退出了同步代码块,那么直接撤销偏向锁状态即可。

对象的偏向锁被撤销之后,对象在未来将不会偏向于任何一个线程。

批量重偏向

我们可以想象,如果有 100 个对象都偏向于一个线程,此时如果有另外一个线程来获取这些对象的锁,那么这 100 个对象都会发生偏向撤销,而这 100 次偏向撤销都需要在全局安全点下进行,这样就会产生大量的性能消耗。

批量重偏向就是建立在撤销偏向会对性能产生较大影响情况下的一种优化措施。当 JVM 知道有大量对象的偏向锁撤销时,它就知道此时这些对象都不会偏向于原线程,所以会将对象重新偏向于新的线程,从而减少偏向撤销的次数。

当一个类的大量对象被同一个线程 T1 获取了偏向锁,也就是大量对象先偏向于该线程 T1。T1 同步结束后,另一个线程 T2 对这些同一类型的对象进行同步操作,就会让这些对象重新偏向于线程 T2。

在了解批量重偏向前,我们需要先了解一点其他知识:
JVM 会给对象的类对象 class 赋予两个属性,一个是偏向撤销计数器,一个是 epoch 值。

我们先来看一个例子:

import org.openjdk.jol.info.ClassLayout;

import java.util.ArrayList;
import java.util.List;

/**
*  @author  liuhaidong
*  @date  2023/1/6 15:06
*/publicclassReBiasTest{

    publicstaticvoidmain(String[] args)throws InterruptedException {

         //延时产生可偏向对象//默认4秒之后才能进入偏向模式,可以通过参数-XX:BiasedLockingStartupDelay=0设置
        Thread.sleep(5000);

        //创造100个偏向线程t1的偏向锁
        List<Test> listA = new ArrayList<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                Test a = new Test();
                synchronized (a) {
                    listA.add(a);
                }
            }
            try {
                //为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();

        //睡眠3s钟保证线程t1创建对象完成
        Thread.sleep(3000);
        System.out.println("打印t1线程,list中第20个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));

        //创建线程t2竞争线程t1中已经退出同步块的锁
        Thread t2 = new Thread(() -> {
            //这里面只循环了30次!!!for (int i = 0; i < 30; i++) {
                Test a = listA.get(i);
                synchronized (a) {
                    //分别打印第19次和第20次偏向锁重偏向结果if (i == 18 || i == 19) {
                        System.out.println("第" + (i + 1) + "次偏向结果");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                    if (i == 10) {
                        // 该对象已经是轻量级锁,无法降级,因此只能是轻量级锁
                        System.out.println("第" + (i + 1) + "次偏向结果");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

        Thread.sleep(3000);
        System.out.println("打印list中第11个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
        System.out.println("打印list中第26个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
        System.out.println("打印list中第41个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable()));
    }
}
在 JDK8 中,-XX:BiasedLockingStartupDelay 的默认值是 4000;在 JDK11 中,-XX:BiasedLockingStartupDelay 的默认值是 0
  1. t1 执行完后,100 个对象都会偏向于 t1。

  1. t2 执行完毕之后,其中前 19 个对象都会撤销偏向锁,此时类中的偏向撤销计数器为19。但当撤销到第 20 个的时候,偏向撤销计数器为 20,此时达到 -XX:BiasedLockingBulkRebiasThreshold=20 的条件,于是将类中的 epoch 值 +1,并在此时找到所有处于同步代码块的对象,并将其 epoch 值等于类对象的 epoch 值。然后进行批量重偏向操作,从第 20 个对象开始,将会比较对象的 epoch 值是否等于类对象的 epoch 值,如果不等于,那么直接使用 CAS 替换掉 Mark Word 中的程 ID 为当前线程的 ID。

  • 结论:

  1. 前 19 个对象撤销了偏向锁,即 Mark Word 中的 biased_lock 为 0,如果有线程来获取锁,那么先获取轻量级锁。

  1. 第 20 - 30 个对象,依然为偏向锁,偏向于线程 t2。

  1. 第 31 - 100 个对象,依然为偏向锁,偏向于线程 t1。

tech.youzan.com/javasuo-yu-…

暂时无法在飞书文档外展示此内容

批量撤销偏向

当偏向锁撤销的数量达到 40 时,就会发生批量撤销。但是,这是在一个时间范围内达到 40 才会发生,这个时间范围通过 -XX:BiasedLockingDecayTime设置,默认值为 25 秒。

也就是在发生批量偏向的 25 秒内,如果偏向锁撤销的数量达到了 40 ,那么就会发生批量撤销,将该类下的所有对象都进行撤销偏向,包括后续创建的对象。如果在发生批量偏向的 25 秒内没有达到 40 ,就会重置偏向锁撤销数量,将偏向锁撤销数量重置为 20。

Hashcode 去哪了

我们通过 Mark Word 知道,在无锁状态下,如果调用对象的 hashcode() 方法,就会在 Mark Word 中记录对象的 Hashcode 值,在下一次调用 hashcode() 方法时,就可以直接通过 Mark Word 来得知,而不需要再次计算,以此来保证 Hashcode 的一致性。

但是获取了锁之后,就会修改 Mark Word 中的值,那么之前记录下来的 Hashcode 值去哪里了呢?

Lock Record

在解答这个问题之前,我们需要先知道一个东西:Lock Record。

当字节码解释器执行 monitorenter 字节码轻度锁住一个对象时,就会在获取锁的线程栈上显式或者隐式分配一个 Lock Record。换句话说,就是在获取轻量级锁时,会在线程栈上分配一个 Lock Record。这个 Lock Record 说直白一点就是栈上的一块空间,主要用于存储相关信息。

Lock Record 只要有三个作用:

  1. 持有 Displaced Word(就是对象的 Mark Word)和一些元信息用于识别哪个对象被锁住了。

  1. 解释器使用 Lock Record 来检测非法的锁状态

  1. 隐式地充当锁重入机制的计数器

那么这个 Lock Record 跟 Hashcode 有什么关系呢?

场景 1

我们先来看第一个场景:先获取对象的 hashcode,然后再获取对象的锁。

import org.openjdk.jol.info.ClassLayout;

publicclassTestObject{
    publicstaticvoidmain(String[] args){
        Test test = new Test();

        // 步骤 1
        System.out.println("=====获取 hashcode 之前=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        test.hashCode();

        // 步骤 2
        System.out.println("=====获取 hashcode 之后=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        // 步骤 3synchronized (test){
            System.out.println("=====获取锁之后=====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }

        // 步骤 4
        System.out.println("=====释放锁之后=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

运行结果:

=====获取 hashcode 之前=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           05000000 (00000101000000000000000000000000) (5)
      44        (object header)                           00000000 (00000000000000000000000000000000) (0)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====获取 hashcode 之后=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           010c 978b (00000001000011001001011110001011) (-1953035263)
      44        (object header)                           76000000 (01110110000000000000000000000000) (118)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====获取锁之后=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           902a 906b (10010000001010101001000001101011) (1804610192)
      44        (object header)                           01000000 (00000001000000000000000000000000) (1)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====释放锁之后=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           010c 978b (00000001000011001001011110001011) (-1953035263)
      44        (object header)                           76000000 (01110110000000000000000000000000) (118)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • 步骤一:未获取对象的 hashcode 值之前,对象处于匿名偏向锁状态。锁标记为:101

  • 步骤二:获取对象的 hashcode 之后,对象的偏向状态被撤销,处于无锁状态。锁标记为:001。对象头中也存储了 hashcode 值,hashcode 值为 0111011 10001011 10010111 00001100。

  • 步骤三:获取锁之后,对象处于轻量级锁状态。锁标记为:00。其余 62 位为指向 Lock Record 的指针。从这里我们可以看到,Mark Word 中已经没有 hashcode 了。整块 Mark Word 的内容已经被复制到 Lock Word 中。

  • 步骤四:释放锁之后,对象处于无锁状态。锁标记为:001。在 Mark Word 中也可以看到之前生成的 hashcode。与步骤二中的 Mark Word 一模一样。这是因为在释放锁之后,JVM 会将 Lock Record 中的值复制回 Mark Word 中,并删除 Lock Record。

结论:

  1. 当对象生成 hashcode 之后,会撤销偏向,并将 hashcode 记录在 Mark Word 中。

  1. 非偏向的对象获取锁时,会先在栈中生成一个 Lock Record。并将对象的 Mark Word 复制到 Lock Record 中。

场景2

我们现在来看第二个场景:先获取对象的锁,然后在同步代码块中生成 hashcode。

import org.openjdk.jol.info.ClassLayout;

publicclassHashCode2{
    publicstaticvoidmain(String[] args){
        Test test = new Test();

        // 步骤一
        System.out.println("=====获取锁之前=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        synchronized (test){
            // 步骤二
            System.out.println("=====获取锁之后,获取hashcode之前=====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());

            // 步骤三
            test.hashCode();
            System.out.println("=====获取锁之后,获取hashcode之后=====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }

        // 步骤四
        System.out.println("=====释放锁之后=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

运行结果:

=====获取锁之前=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           05000000 (00000101000000000000000000000000) (5)
      44        (object header)                           00000000 (00000000000000000000000000000000) (0)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====获取锁之后,获取hashcode之前=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           0590803a (00000101100100001000000000111010) (981504005)
      44        (object header)                           01000000 (00000001000000000000000000000000) (1)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====获取锁之后,获取hashcode之后=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           02 e8 832a (00000010111010001000001100101010) (713287682)
      44        (object header)                           01000000 (00000001000000000000000000000000) (1)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====释放锁之后=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           02 e8 832a (00000010111010001000001100101010) (713287682)
      44        (object header)                           01000000 (00000001000000000000000000000000) (1)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • 步骤一:未获取对象的 hashcode 值之前,对象处于匿名偏向锁状态。锁标记为:101

  • 步骤二:进入同步代码块,线程获取了偏向锁。锁标记:101

  • 步骤三:对象生成 hashcode,此时锁标记:10,直接从偏向锁升级为重量级锁。 其余 62 位为指向 objectMonitor 的指针。

与轻量级锁存在同样的问题,hashcode 会存放在哪里?每一个对象在 JVM 中都有一个 objectMonitor 对象,而 Mark Word 就存储在 objectMonitor 对象的 header 属性中。

轻量级锁

轻量级锁解决的场景是:任意两个线程交替获取锁的情况。主要依靠 CAS 操作,相比较于使用重量级锁,可以减少锁资源的消耗。

获取轻量级锁

使用轻量级锁的情况有以下几种:

  1. 禁用偏向锁。

  1. 偏向锁失效,升级为轻量级锁。

禁用偏向锁导致升级

在启动 Java 程序时,如果添加了 JVM 参数 -XX:-UseBiasedLocking , 那么在后续的运行中,就不再使用偏向锁 。

偏向锁失效,升级为轻量级锁

如果对象发生偏向撤销时:

  1. 首先会检查持有偏向锁的线程是否已经死亡,如果死亡,则直接升级为轻量级锁,否则,执行步骤2

  1. 查看持有偏向锁的线程是否在同步代码块中,如果在,则将偏向锁升级为轻量级锁,否则,执行步骤3

  1. 修改 Mark Word 为非偏向模式,设置为无锁状态。

加锁过程

当线程获取轻量级锁时,首先会在线程栈中创建一个 Lock Record 的内存空间,然后拷贝 Mark Word 中的数据到 Lock Record 中。JVM 中将有数据的 Lock Record 叫做 Displated Mark Word。

Lock Record 在栈中的内存结构:

暂时无法在飞书文档外展示此内容

当数据复制成功之后,JVM 将会使用 CAS 尝试修改 Mark Word 中的数据为指向线程栈中 Displated Mark Word 的指针,并将 Lock Record 中的 owner 指针指向 Mark Word。

如果这两步操作都更新成功了,那么则表示该线程获得轻量级锁成功,设置 Mark Word 中的 lock 字段为 00,表示当前对象为轻量级锁状态。同步,线程可以执行同步代码块。

如果更新操作失败了,那么 JVM 将会检查 Mark Word 是否指向当前线程的栈帧:

  • 如果是,则表示当前线程已经获取了轻量级锁,会在栈帧中添加一个新的 Lock Record,这个新 Lock Record 中的 Displated Mark Word 为 null,owner 指向对象。这样的目的是为了统计重入的锁数量,因此,在栈中会有一个 Lock Record 的列表。完成这一步之后就可以直接执行同步代码块。

暂时无法在飞书文档外展示此内容

  • 如果不是,那么表示轻量级锁发生竞争,后续将会膨胀为重量级锁。

释放轻量级锁

释放轻量级锁时,会在栈中由低到高,获取 Lock Record。查询到 Lock Record 中的 Displated Mark Word 为 null 时,则表示,该锁是重入的,只需要将 owner 设置为 null 即可,表示已经释放了这个锁。如果 Displated Mark Word 不为 null,则需要通过 CAS 将 Displated Mark Word 拷贝至对象头的 Mark Word 中,然后将 owner 的指针设置为 null,最后修改 Mark Word 的 lock 字段为 01 无锁状态。

重量级锁

重量级锁解锁的场景是:多个线程相互竞争同一个锁。主要通过 park() 和 unpark()方法,结合队列来完成。相较于轻量级锁和偏向锁,需要切换内核态和用户态环境,因此获取锁的过程会消耗较多的资源。

获取重量级锁

使用重量级锁的情况有两种:

  1. 在持有偏向锁的情况下,直接获取对象的 hashcode,将会直接升级为重量级锁。

  1. 在轻量级锁的情况下,存在竞争,膨胀为重量级锁。

获取 hashcode,升级为重量级锁

import org.openjdk.jol.info.ClassLayout;

publicclassHashCode2{
    publicstaticvoidmain(String[] args){
        Test test = new Test();

        // 步骤一
        System.out.println("=====获取锁之前=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        synchronized (test){
            // 步骤二
            System.out.println("=====获取锁之后,获取hashcode之前=====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());

            // 步骤三
            test.hashCode();
            System.out.println("=====获取锁之后,获取hashcode之后=====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }
    }
}

执行后的结果

=====获取锁之前=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           05000000 (00000101000000000000000000000000) (5)
      44        (object header)                           00000000 (00000000000000000000000000000000) (0)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====获取锁之后,获取hashcode之前=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           0590803a (00000101100100001000000000111010) (981504005)
      44        (object header)                           01000000 (00000001000000000000000000000000) (1)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=====获取锁之后,获取hashcode之后=====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      04        (object header)                           02 e8 832a (00000010111010001000001100101010) (713287682)
      44        (object header)                           01000000 (00000001000000000000000000000000) (1)
      84        (object header)                           506a 0600 (01010000011010100000011000000000) (420432)
     124int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

我们直接在偏向锁的同步代码块中执行 hashcode(),会发现偏向锁直接膨胀为重量级锁了。我们可以看到 lock 字段为 10。

这里有一个疑问,为什么不是升级为轻量级锁呢?轻量级锁也可以在 Lock Record 中存储生成的 hashcode。而膨胀为更为消耗资源的重量级锁。

轻量级锁膨胀为重量级锁

当处于轻量级锁的时候,说明锁已经不再偏向于任何一个线程,但是也没有发生竞争,可以依靠 CAS 获取到轻量级锁。但是当出现 CAS 获取锁失败时,就会直接膨胀为重量级锁。

这里需要注意,只会 CAS 一次,只要一次失败就会直接膨胀为重量级锁,而不是达到自旋次数或者自旋时间才膨胀。

膨胀过程

在膨胀过程中,会有几种标记来表示锁的状态:

  • Inflated:膨胀已完成

  • Stack-locked:轻量级锁

  • INFLATING:膨胀中

  • Neutral:无锁

膨胀步骤:

  1. 检查是否已经为重量级锁,如果是直接返回。

  1. 检查是否处于膨胀中的状态,如果是,循环检测状态。检测出膨胀中的状态是因为有其他线程正在进行膨胀,因为需要等待膨胀完成之后,才能继续执行。

  1. 检查是否为轻量级锁,如果是,则执行以下步骤:

  1. 创建一个 ObjectMonitor 对象。

  1. 通过 CAS 设置 Mark Word 为全 0,用以表示 INFLATING 状态。如果失败,则从步骤 1 重新开始执行。

  1. 将 Mark Word 设置到 ObjectMonitor 对象中。

  1. 设置 owner 属性为 Lock Record

  1. 设置 Mark Word 值

  1. 返回

  1. 判定为无锁状态,执行以下步骤:

  1. 创建一个 ObjectMonitor 对象。

  1. 通过 CAS 直接设置 Mark Word 值。

  1. 返回

竞争锁过程

我们要理解如何获取重量级锁,需要先了解 ObjectMonitor 对象。顾名思义,这是一个对象监视器。在 Java 中,每个对象都有一个与之对应的 ObjectMonitor 。ObjectMonitor 内部有几个重要的字段:

  • cxq:存放被阻塞的线程

  • EntryList:存放被阻塞的线程,在释放锁时使用

  • WaitSet:获得锁的线程,如果调用 wait() 方法,那么线程会被存放在此处,这是一个双向循环链表

  • onwer:持有锁的线程

cxq,EntryList 均为 ObjectWaiter 类型的单链表。

获取锁过程

  1. 通过 CAS 设置 onwer 为当前线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread,如果成功,则表示获得锁。否则执行步骤 2

  1. 判断当前线程与获取锁线程是否一致,如果一致,则表示获得锁(锁重入)。否则执行步骤 3

  1. 判断当前线程是否为之前持有轻量级锁的线程,如果是,直接设置 onwer 为当前线程,表示获得锁。否则执行步骤 4

  1. 以上步骤都失败,则尝试一轮自旋来获取锁。如果未获取锁,则执行步骤 5

  1. 使用阻塞和唤醒来控制线程竞争锁

  1. 通过 CAS 设置 owner 为当前线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果成功,则表示获得锁。否则执行步骤 b

  1. 通过 CAS 设置 owner 为当前线程(尝试获取锁)CAS 的原值为 DEFLATER_MARKER,新值为 current_thread。如果成功,则表示获得锁。否则执行步骤c。(DEFLATER_MARKER 是一个锁降级的标记,后续会讲解。)

  1. 以上步骤都失败,则尝试一轮自旋来获取锁。如果未获取锁,则执行步骤 d。

  1. 为当前线程创建一个 ObjectWaiter 类型的 node 节点。步骤 i 和 ii 是一个循环,直到一个成功才会跳出这个循环。

  1. 通过 cas 插入 cxq 的头部,如果插入失败,则执行步骤 ii

  1. 通过 CAS 设置 owner 为当前线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果失败,则执行 i。

  1. 通过 CAS 设置 owner 为当前线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果成功,则表示获得锁。否则执行步骤 f。(该步骤往下开始是一个循环,直到获取到锁为止)

  1. 通过 park(),将线程阻塞。

  1. 线程被唤醒后

  1. 通过 CAS 设置 owner 为当前线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果成功,则表示获得锁。否则执行步骤 ii

  1. 通过 CAS 设置 owner 为当前线程(尝试获取锁)CAS 的原值为 DEFLATER_MARKER,新值为 current_thread。如果成功,则表示获得锁。否则执行 iii

  1. 尝试一轮自旋来获取锁。如果未获取锁,则跳转回步骤 e 执行。

自适应自旋锁主要是用于重量级锁中,降低阻塞线程概率。而不是用于轻量级锁,这里大家要多多注意。

释放重量级锁

释放锁过程

  1. 判断 _owner 字段是否等于 current_thread。如果等于则判断当前线程是否为持有轻量级锁的线程,如果是的话,表示该线程还没有执行 enter()方法,因此,直接设置 _owner 字段为 current_thread。

  1. 判断 _recursions,如果大于0,则表示锁重入,直接返回即可,不需要执行后续解锁代码。

  1. 设置 _owner 字段为 NULL,解锁成功,后续线程可以正常获取到锁。

  1. 唤醒其他正在被阻塞的线程。在执行以下操作之前需要使用该线程重新获取锁。如果获取锁失败,则表示锁已经被其他线程获取,直接返回,不再唤醒其他线程。(为什么还要获取到锁才可以唤醒其他线程呢?因为唤醒线程时,需要将 cxq 中的节点转移到 EntryList 中,涉及到链表的移动,如果多线程执行,将会出错。)

  1. 如何 _EntryList 非空,那么取 _EntryList 中的第一个元素,将该元素下的线程唤醒。否则执行步骤 b。

  1. 将 _cxq 设置为空,并将 _cxq 的元素按照原顺序放入 _EntryList 中。然后取 _EntryList 中的第一个元素,将该元素下的线程唤醒。

  1. 线程唤醒

  1. 设置 _owner 字段为 NULL,解锁成功,让后续线程可以正常获取到锁。

  1. 然后调用 unpark() 方法,唤醒线程。

wait(),notify(),notifyAll()

我们需要知道一个前提,在处理 wait 方法时,必须使用重量级锁。因此,wait 方法会导致锁升级。

我们先来看一个例子:

publicclassWaitTest {static final Object lock = new Object();

    publicstaticvoidmain(String[] args){

        new Thread(() -> {
            synchronized (lock){
                log("get lock");
                try {
                    log("wait lock");
                    lock.wait();
                } catch (InterruptedException e) {
                    thrownew RuntimeException(e);
                }
                log("get lock again");
                log("release lock");
            }
        }, "thread-A").start();

        sleep(1000);

        new Thread(() -> {
            synchronized (lock){
                log("get lock");
                createThread("thread-C");
                sleep(2000);
                log("start notify");
                lock.notify();
                log("release lock");
            }
        }, "thread-B").start();
    }

    publicstaticvoidcreateThread(String threadName){
        new Thread(() -> {
            synchronized (lock){
                log("get lock");
                log("release lock");
            }
        }, threadName).start();
    }

    privatestaticvoidsleep(long sleepVal){
        try{
            Thread.sleep(sleepVal);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    privatestaticvoidlog(String desc){
        System.out.println(Thread.currentThread().getName() + " : " + desc);
    }
}

最后打印的结果:

thread-A : get lock
thread-A : wait lock
thread-B : get lock
thread-B : start notify
thread-B : release lock
thread-A : get lock again
thread-A : release lock
thread-C : get lock
thread-C : release lock
  1. 线程 A 首先获取到锁,然后通过 wait() 方法,将锁释放,并且等待通知。

  1. 睡眠 1 S,这里是确保线程 A 可以顺利完成所有操作。

  1. 因为 A 释放了锁,所以线程 B 可以获取到锁。然后创建了线程 C。

  1. 因为线程 B 睡眠了 2S,依然持有锁,所以线程 C 无法获取到锁,只能继续等待。

  1. 线程 B 调用 notify() 方法,线程 A 被唤醒,开始竞争锁。

  1. 线程 A 和线程 C 竞争锁。

但是根据打印结果,无论执行多少次,都是线程 A 先获取锁。

第一个问题:为什么都是线程 A 先获取锁,而不是线程 C 先获取锁?

第二个问题:为什么 wait 方法并没有生成 monitorenter 指令,也可以获取到锁?

第三个问题:执行 wait 之后,线程去哪里了?它的状态是什么?

为了解答这些问题,我们需要深入到源码中去。但是这里就不放源码了,我只讲一下关键步骤:

wait()

  1. 膨胀为重量级锁

  1. 为 current_thread 创建 ObjectWaiter 类型的 node 节点

  1. 将 node 放入 _waitSet 中

  1. 释放锁

  1. 通过 park() 阻塞 current_thread。

notify()

  1. 检查 _waitSet 是否为 null,如果为 null,直接返回

  1. 获取 _waitSet 的第一个元素 node,并将其从链表中移除。

  1. 此时,存在三个策略:默认使用 policy = 2

  1. 插入到 EntryList 的头部(policy = 1)

  1. 插入到 EntryList 的尾部(policy = 0)

  1. 插入到 cxq 的 头部(policy = 2)

  1. 将 node 插入到 cxq 的头部。

notifyAll()

  1. 循环检测 _waitSet 是否不为空

  1. 如果不为空,则执行 notify() 的步骤。

  1. 否则返回

第一个问题:执行 wait 之后,线程去哪里了?它的状态是什么?

线程 A 调用 wait() 方法后,线程 A 就被 park 了,并被放入到 _waitSet 中。此时他的状态就是 WAITING。如果它从 _waitSet 移除,并被放入到 cxq 之后,那么他的状态就会变为 BLOCKED。如果它竞争到锁,那么他的状态就会变为 RUNNABLE 。

第二个问题:为什么 wait 方法并没有生成 monitorenter 指令,也可以获取到锁?

线程 A 调用 wait() 方法后,线程 A 被放入到 _waitSet 中。直到有其他线程调用 notify() 之后,线程 A 从 _waitSet 移除,并放入到 cxq 中。

第三个问题:为什么都是线程 A 先获取锁,而不是线程 C 先获取锁?

线程 A 调用 wait() 方法后,线程 A 被放入到 _waitSet 中。线程 B 获取锁,然后创建了线程 C,线程 C 竞争锁失败,被放入到 cxq 中。然后 B 调用 notify() 方法后,线程 A 从 _waitSet 移除,放入到 cxq 的头部。因此目前 cxq 的链表结构为:A -> C -> null。接着线程 B 释放锁,会将 cxq 中的元素按照原顺序放入到 EntryList 中,因此目前 cxq 链表结构为:null;EntryList 链表结构为:A -> C -> null。然后唤醒 EntryList 中的第一个线程。

所以,每次都是线程 A 先获取锁。

深入了解torch框架 大国的游戏,政府支持到位,是未来;AI : Artificial Intelligence,旨在研究、开发能够模拟、延伸和扩展人类智能的理论、方法、技术及应用系统,是一种拥有自主学习和推理能力的技术。它模仿人类大脑某些功能,包括感知、学习、理解、决策和问题解决。AI本质​ NLP(说话,听)、CV(眼睛)、自动驾驶、机器人(肢体动作)、大模型三要素:数据、网络、算力① 神经网络:找到合适的数学公式;② 训练:用已有数据训练网络,目标是求最优解;③ 推理:用模型预测新样本;PyTorch,简称Torch 阅读详情

相关推荐

QCPColorMap实现瀑布图

QCPColorMap实现真正的瀑布图

pf1873252141的博客 5273

synchronized 各种锁状态

偏向锁的论文中给出的,偏向锁定下的锁状态转换: 可能发生的所有状态转换: 对象可能的初始锁状态只有 可偏向 和 无锁 两种。 注释:可偏向状态,使用原始论文里的表达就是 the biasable but unbiased state。又叫 匿名偏向状态,后面统一使用 可偏向状态表达。 可偏向 只能变成 偏向。偏向 可以批量重偏向为 可偏向,可以撤销偏向为 无锁,或者撤销偏向为 轻量级锁。 无锁 只能变成 轻量级锁。 轻量级锁 可以解锁后变成 无锁,可以发生锁争用后膨胀成 重量级锁。 状态转换图示: 状态

L__ear的博客 653

C#连接数据库报Authentication to host ‘127.0.0.1‘ for user ‘root‘ using method ‘caching_sha2_password‘ fail

所以导致的,除了上边的办法,还可以把这个加密规则修改一下,这个的修改方式有很多,具体原因是因为MySQL8.0以前的加密规则是。

weixin_42209881的博客 4130

正确运用synchronized和二次判断 实现多线程安全

正确运用synchronized和二次判断 实现多线程安全,做出高效二符合预期的程序,特别是多个线程跑一个对象的时候,如下图所示:  测试代码如下: 特别注意if(shutdownRequested){ *部分不同的写法。 不然就会输出与逻辑不符的现象: 如: runner—-false—-我没有关闭。。。 runner—-true—-我没有关闭。。。 runner—-true—-我关闭...

weixin_33774615的博客 290

从采集到播放:RV1106+H264全链路开发避坑指南(含FFmpeg转码技巧)

本文详细解析了基于Rockchip RV1106芯片构建H.264视频从采集到播放全链路的开发实践。内容涵盖RKMPI框架设计、VI/VENC模块配置、内存泄漏排查及FFmpeg高效转码技巧,旨在帮助嵌入式开发者规避常见陷阱,实现稳定高效的低延迟视频处理流水线。

weixin_29189377的博客 662

保姆级教程:STK 11.0 从下载到安装成功(附Matlab互联配置与常见问题解决)

本文提供STK 11.0的完整安装指南,包括下载、安装步骤、Matlab互联配置及常见问题解决方案。详细介绍了系统要求核查、分步安装流程、关键文件替换和许可证配置,确保用户能够顺利完成STK 11.0的部署并实现与Matlab的无缝对接。

weixin_30698297的博客 346

synchronized判断锁使用==还是equals?

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_33852020的博客 312

独家开源|RK3588 平台 YOLOv5 Android 源码级解析:NPU 量化、多线程与目标跟踪实战

本文将带你完成一个完整的AI视觉项目:在搭载RK3588芯片的Android开发板上运行实时目标检测与跟踪应用。实战目标:成功运行官方YOLOV5摄像头实时检测案例技术深度:深入理解RKNN开发框架和源码实现核心技能:掌握NPU推理、多线程并发、目标跟踪等关键技术。

m0_71589036的博客 194

使用synchronized,如何判断是不是同一个锁。

先来看一下 == 和 equals 的区别:java中默认的equals方法实现如下:public boolean equals(Object obj) { return (this == obj); }值类型,用==判断相等性。对象引用的话,==判断引用指向的对象是否是同一个。==是判断两个变量或实例是不是指向同一个内存空间。 equals是判断两个变量或实例所指向的内存空间的值是不是相...

yaotai8135的博客 2577

2026华为OD机考新系统真题 + 双机位C卷 + 往年真题 - 题库目录分值说明 + 考点说明+在线OJ

2026年4月1日起,华为OD机试正式切换至新系统模式!采用力扣模式,无需自己写输入! 难度大幅度降低。正在备考华为OD机试的同学,优先刷新系统机试真题,可大幅提升通过率。 新系统考题考点与旧版一致,无需重新适应。本专栏所有算法题均配有详细思路解析,并提供C语言、JS、C++、Python、Go、Java多语言源码实现。订阅后私信即可开通专属OJ刷题权限,高效备考一次上岸!

算法大师! 45万+

超分辨率重建:ESPCN与SRGAN技术对比

超分辨率重建:ESPCN与SRGAN技术对比​ 人工智能,计算机视觉,大模型,AI,超分辨率重建是一种通过技术手段,将低分辨率图像转换为高分辨率图像的过程。它的目的是恢复低分辨率图像中丢失的细节信息,使图像具有更高的清晰度和视觉质量。

不踩坑,不知道坑有多深;不总结,踩过的坑白踩。 7万+

JVM synchronized 详细探究

首先,同步在JVM中是通过monitor进入和monitor退出来实现的。 1、synchronized 方法   对于synchronized方法,同步作为方法调用和返回的一部分被隐式执行,并不是使用monitorenter和monitorexit来实现方法同步。而是通过运行时常量池中方法对应的ACC_SYNCHRONIZED标志来实现同步。 1.1 synchronized 方法怎么被识别? ...

初心江湖路的博客 302

【RT-DETR实战】RT-DETR实战手记(200):端侧实时目标检测,下一步往哪儿走?

昨天深夜调一个模型部署,在树莓派4B上跑RT-DETR-nano,帧率死活卡在8FPS上不去。量化也做了,算子也优化了,内存对齐也检查了,可就是离实时检测的底线还差一截。盯着终端里跳动的数字,突然意识到——我们可能正在接近当前技术路径的瓶颈。端侧实时目标检测的下一个突破口,恐怕不在现有框架的修修补补里。

企鹅码农的专属播客,分享10多年的实战案例和经验解读,包含YOLO、RT-DETR、AI等各个工作领域。 187

JVM锁实现探究2:synchronized深探

这里我们来聊聊synchronized,以及wait(),notify()的实现原理。 在深入介绍synchronized原理之前,先介绍两种不同的锁实现。 阻塞锁 我们平时说的锁都是通过阻塞线程来实现的:当出现锁竞争时,只有获得锁的线程能够继续执行,竞争失败的线程会由running状态进入blocking状态,并被登记在目标锁相关的一个等待队列中,当前一个线程退出临界区,释放锁后,会将等待

xiaomin1991222的专栏 422

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

JVM锁实现探究:synchronized初探

原文: http://www.majin163.com/2014/03/17/synchronized1/ http://www.majin163.com/2014/03/17/synchronized2/ JAVA是一门极易入门的语言,这一点尤其表现在JAVA中对象锁的使用和多线程编程上。所谓对象锁,就是可以直接在JAVA的任意Object加锁(synchronized),也可以在通过任...

SAN_YUN的专栏 383

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

JVM锁实现探究1:synchronized初探

转自:http://www.majin163.com/2014/03/17/synchronized1/   引言 JAVA是一门极易入门的语言,这一点尤其表现在JAVA中对象锁的使用和多线程编程上。所谓对象锁,就是可以直接在JAVA的任意Object加 锁(synchronized),也可以在通过任意Object进行线程的阻塞(Object.wait())和唤醒(Object.notif...

lgshendy的专栏 204

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

【小知识探究系列四】synchronized的加锁原理和膨胀过程探究(以64位jvm为例)

一、synchronized加锁原理 由于synchronized总体的工作原理是通过操作对象头来实现加锁和解锁,因此在具体的了解synchronized之前,首先需要简单了解一下对象头。 所谓对象头其实是每个对象都存在的一份存储当前对象gc分代年龄、hashcode、锁标记、当前获取到锁的线程ID等信息的一块内存区域。对象头主要分为以下三块: 1.Mark Word 2.指向类的指针 3.数组长度(只有数组对象才有) synchronized在加锁过程中主要对mark word进行操作,因此主要对ma

浪舟子的博客 513
上一篇: 阿里P8推崇不已的Java4大开源框架,大厂面试必问底层文档
下一篇: 牛批!阿里P8熬夜冠军手码的Docker容器+k8s技术PDF,你还等啥呢
Java技术那些事儿
博客等级 码龄5年 2306粉丝 2584原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值