当前位置: 首页 > news >正文

5、并发锁机制之synchronized

并发锁机制之synchronized

  • i++/i--引起的线程安全问题分析
    • 原因分析
      • i++的JVM字节码指令
      • i--的JVM 字节码指令
      • 结论
    • 解决方案
  • synchronized的使用
    • 加锁方式
    • 使用synchronized解决之前的共享问题
      • 方式一
      • 方式二
  • synchronized底层实现原理分析
    • 查看synchronized的字节码指令序列
    • 重量级锁实现之Monitor(管程/监视器)机制详解
      • Monitor设计思路
        • MESA模型分析
        • ObjectMonitor数据结构分析
      • 重量级锁实现原理
    • 重量级锁的优化策略
      • 锁粗化
      • 锁消除
      • CAS自旋优化
      • 轻量级锁
        • 轻量级锁是否存在自旋问题分析
      • 偏向锁
      • 锁升级的过程
  • synchronized锁升级详解
    • sychronized多种锁状态设计详解
      • 对象的内存布局
      • 对象头详解
        • 使用JOL工具查看内存布局
      • Mark Word是如何记录锁状态的
    • 锁升级场景实验
    • 轻量级锁详解
      • 轻量级锁实现原理
    • 偏向锁详解
      • 偏向锁匿名偏向状态
      • 偏向锁延迟偏向
      • 偏向锁状态跟踪实验
      • 对象调用了hashCode不会开启偏向锁
      • 偏向锁撤销场景
        • 偏向锁撤销之调用对象HashCode
        • 偏向锁撤销之调用wait/notify
      • 偏向锁批量重偏向&批量撤销
        • 实现原理
        • 应用场景
        • 批量重偏向实验
        • 批量撤销实验
        • 总结
    • 锁升级的流程分析

i++/i–引起的线程安全问题分析

两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?


public class SyncDemo {private static int counter = 0;public static void increment() {counter++;}public static void decrement() {counter--;}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {increment();}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {decrement();}}, "t2");t1.start();t2.start();t1.join();t2.join();//思考: counter=?log.info("{}", counter);}
}

原因分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

我们可以查看i++和i–(i为静态变量)的JVM字节码指令(可以在idea中安装一个jclasslib插件)

i++的JVM字节码指令


getstatic i // 获取静态变量i的值,并将其值压入栈顶
iconst_1 // 将int型常量1压入栈顶
iadd // 将栈顶两int型数值相加并将结果压入栈顶
putstatic i // 将结果赋值给静态变量i

i–的JVM 字节码指令


getstatic i // 获取静态变量i的值,并将其值压入栈顶 
iconst_1 // 将int型常量1压入栈顶
isub // 将栈顶两int型数值相减并将结果压入栈顶 
putstatic i // 将结果赋值给静态变量i

如果是单线程运行以上8行代码是顺序执行(不会交错)没有问题。但多线程下这8行代码可能交错运行:

结论

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

解决方案

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源

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


//临界资源
private static int counter = 0;public static void increment() { //临界区counter++;
}public static void decrement() {//临界区counter--;
}

为了避免临界区的竞态条件发生,有多种手段可以达到目的:

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

synchronized的使用

  synchronized同步块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。

加锁方式

使用synchronized解决之前的共享问题

方式一


public static synchronized void increment() {counter++;
}public static synchronized void decrement() {counter--;
}

方式二


private static String lock = "";public static void increment() {synchronized (lock){counter++;}
}public static void decrement() {synchronized (lock) {counter--;}
}

synchronized实际是用对象锁保证了临界区内代码的原子性

synchronized底层实现原理分析

  synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。

查看synchronized的字节码指令序列

Method access and property flags:

  同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorentermonitorexit来实现。

重量级锁实现之Monitor(管程/监视器)机制详解

  Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

Monitor设计思路

MESA模型分析

  在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。下面我们便介绍MESA模型:

  管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

  Java参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简。MESA模型中,条件变量可以有多个,Java语言内置的管程里只有一个条件变量。模型如下图所示:


@Slf4j
public class WaitDemo {final static Object obj = new Object();public static void main(String[] args) throws InterruptedException {new Thread(() -> {log.debug("t1开始执行....");synchronized (obj) {log.debug("t1获取锁....");try {// 让线程在obj上一直等待下去obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("t1执行完成....");}},"t1").start();new Thread(() -> {log.debug("t2开始执行....");synchronized (obj) {log.debug("t2获取锁....");try {// 让线程在obj上一直等待下去obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("t2执行完成....");}},"t2").start();// 主线程两秒后执行Thread.sleep(2000);log.debug("准备获取锁,去唤醒 obj 上阻塞的线程");synchronized (obj) {// 唤醒obj上一个线程//obj.notify();// 唤醒obj上所有等待线程obj.notifyAll();log.debug("唤醒 obj 上阻塞的线程");}}}
ObjectMonitor数据结构分析

  java.lang.Object类定义了wait(),notify(),notifyAll()方法,这些方法的具体实现,依赖于ObjectMonitor实现,这是JVM内部基于C++实现的一套机制。

  ObjectMonitor其主要数据结构如下(hotspot源码ObjectMonitor.hpp):


ObjectMonitor() {_header       = NULL; //对象头  markOop_count        = 0;  _waiters      = 0,   _recursions   = 0;   // 锁的重入次数 _object       = NULL;  //存储锁对象_owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点_WaitSetLock  = 0 ;    _Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)FreeNext      = NULL ;_EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}

重量级锁实现原理

  synchronized底层是利用monitor对象,CAS和mutex互斥锁来实现的,内部会有等待队列(cxq和EntryList)和条件等待队列(waitSet)来存放相应阻塞的线程。未竞争到锁的线程存储到等待队列中,获得锁的线程调用wait后便存放在条件等待队列中,解锁和notify都会唤醒相应队列中的等待线程来争抢锁。然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。

  在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(QMode=0)是:如果EntryList为空,则将cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程。

  为什么会有_cxq和_EntryList两个列表来放线程?

  因为会有多个线程会同时竞争锁,所以搞了个_cxq这个单向链表基于CAS来hold住这些并发,然后另外搞一个_EntryList这个双向链表,来在每次唤醒的时候搬迁一些线程节点,降低_cxq的尾部竞争。

重量级锁的优化策略

  JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

锁粗化

  锁粗化,简单来说,就是将多个连续的锁扩展为一个更大范围的锁。也就是说,如果 JVM 检测到有连续的对同一对象的加锁、解锁操作,就会把这些加锁、解锁操作合并为对这段区域进行一次连续的加锁和解锁。


synchronized (lock) {// 代码块 1
}
// 无关代码
synchronized (lock) {// 代码块 2
}
// JVM 在运行时可能会选择将上述两个小的同步块合并,形成一个大的同步块:
synchronized (lock) {// 代码块 1// 无关代码// 代码块 2
}

为什么锁粗化有效

  加锁和解锁操作本身也会带来一定的性能开销,因为每次加锁和解锁都可能会涉及到线程切换、线程调度等开销。如果有大量小的同步块频繁地进行加锁和解锁,那么这部分开销可能会变得很大,从而降低程序的执行效率。

  通过锁粗化,可以将多次加锁和解锁操作减少到一次,从而减少这部分开销,提高程序的运行效率。

如何在代码中实现锁粗化

  在代码层面上,我们并不能直接控制JVM进行锁粗化,因为这是JVM在运行时动态进行的优化。不过,我们可以在编写代码时,尽量减少不必要的同步块,避免频繁加锁和解锁。这样,就为JVM的锁粗化优化提供了可能。

代码示例


StringBuffer buffer = new StringBuffer();
/*** 锁粗化*/
public void append(){buffer.append("aaa").append(" bbb").append(" ccc");
}

  上述代码每次调用buffer.append方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

  锁粗化是JVM提供的一种优化手段,能够有效地提高并发编程的效率。在我们编写并发代码时,应当注意同步块的使用,尽量减少不必要的加锁和解锁,从而使得锁粗化技术能够发挥作用。

锁消除

  锁消除主要应用在没有多线程竞争的情况下。具体来说,当一个数据仅在一个线程中使用,或者说这个数据的作用域仅限于一个线程时,这个线程对该数据的所有操作都不需要加锁。在Java HotSpot VM中,这种优化主要是通过逃逸分析(Escape Analysis)来实现的。

为什么锁消除有效

  锁消除之所以有效,是因为它消除了不必要的锁竞争,从而减少了线程切换和线程调度带来的性能开销。当数据仅在单个线程中使用时,对此数据的所有操作都不需要同步。在这种情况下,锁操作不仅不会增加安全性,反而会因为增加了额外的执行开销而降低程序的运行效率。

如何在代码中实现锁消除

  在代码层面上,我们无法直接控制JVM进行锁消除优化,这是由JVM的JIT编译器在运行时动态完成的。但我们可以通过编写高质量的代码,使JIT编译器更容易识别出可以进行锁消除的场景。例如:


public class LockEliminationTest {/*** 锁消除* -XX:+EliminateLocks 开启锁消除(jdk8默认开启)* -XX:-EliminateLocks 关闭锁消除* @param str1* @param str2*/public void append(String str1, String str2) {StringBuffer stringBuffer = new StringBuffer();stringBuffer.append(str1).append(str2);}public static void main(String[] args) throws InterruptedException {LockEliminationTest demo = new LockEliminationTest();long start = System.currentTimeMillis();for (int i = 0; i < 100000000; i++) {demo.append("aaa", "bbb");}long end = System.currentTimeMillis();System.out.println("执行时间:" + (end - start) + " ms");}}

  StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。因此,JIT 编译器会发现这种情况并自动消除 append 操作中的锁竞争。

测试结果:

  • 关闭锁消除执行时间:4688 ms
  • 开启锁消除执行时间:2601 ms

CAS自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • 在Java 6之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。可以使用-XX:+UseSpinning 参数来开启自旋锁,使用-XX:PreBlockSpin 参数来设置自旋锁的等待次数。
  • Java 7之后不能控制是否开启自旋功能,自旋锁的参数被取消,自旋锁总是会执行,自旋次数也由虚拟机自行调整。

注意自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)

轻量级锁

  我们再思考一下,是否有这样的场景:多个线程都是在不同的时间段来请求同一把锁,此时根本就用不需要阻塞线程,连monitor对象都不需要,所以就引入了轻量级锁这个概念,避免了系统调用,减少了开销。

在锁竞争不激烈的情况下,这种场景还是很常见的,可能是常态,所以轻量级锁的引入很有必要。

轻量级锁是否存在自旋问题分析

错误理解:轻量级锁加锁失败会自旋,失败一定次数后会膨胀升级为重量级锁

正确理解:轻量级锁不存在自旋,只有重量级锁加锁失败才会自旋。重量级锁加锁失败,会多次尝试cas和自适应自旋,如果一直加锁失败,就会阻塞当前线程等待唤醒

轻量级锁竞争没有自旋的原因其实是其设计并不是用于处理过于激烈的竞争场景,而是为了应对线程之间交替获取锁的场景。

偏向锁

  我们再思考一下,是否有这样的场景:一开始一直只有一个线程持有这个锁,也不会有其他线程来竞争,此时频繁的CAS是没有必要的,CAS也是有开销的。所以 synchronized就搞了个偏向锁,就是偏向一个线程,那么这个线程就可以直接获得锁。对于没有锁竞争的场合,偏向锁有很好的优化效果,可以消除锁重入(CAS操作)带来的开销

锁升级的过程

synchronized锁升级详解

sychronized多种锁状态设计详解

对象的内存布局

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

  • 对象头:比如hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

对象头详解

HotSpot虚拟机的对象头包括:

  • Mark Word
    • 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。
  • Klass Pointer
    • 对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。
  • 数组长度(只有数组对象有)
    • 如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节

使用JOL工具查看内存布局

  分享一个可以查看普通java对象的内部布局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看new出来的一个java对象的内部布局,以及一个普通的java对象占用多少字节。

1、引入maven依赖


<!-- 查看Java 对象布局、大小工具 -->
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.10</version>
</dependency>

2、使用方法


//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

测试


public static void main(String[] args) throws InterruptedException {Object obj = new Object();//查看对象内部信息System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

利用jol查看64位系统java对象(空对象),默认开启指针压缩,总大小显示16字节,前12字节为对象头

  • OFFSET:偏移地址,单位字节;
  • SIZE:占用的内存大小,单位为字节;
  • TYPE DESCRIPTION:类型描述,其中object header为对象头;
  • VALUE:对应内存中当前存储的值,二进制32位;

关闭指针压缩后,对象头为16字节:-XX:-UseCompressedOops

Mark Word是如何记录锁状态的

  Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。

  简单点理解就是:MarkWord结构搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。

Mark Word的锁标记结构


//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)。。。。。。
//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time

32位JVM下的对象结构描述

64位JVM下的对象结构描述

  • hash:保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
  • age:保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。分代年龄占4字节,二进制1111转成十进制就是15,分代年龄最大15
  • biased_lock:偏向锁标识位。由于无锁和偏向锁的锁标识都是01,没办法区分,这里引入一位的偏向锁标识位。
  • lock:锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  • JavaThread*:保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
  • epoch:偏向锁撤销的计数器,可用于偏向锁批量重偏向和批量撤销的判断依据。
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针

锁升级场景实验

示例代码:演示锁升级的过程


public class LockUpgrade {public static void main(String[] args) throws InterruptedException {User userTemp = new User();System.out.println("无锁状态(001):"+ ClassLayout.parseInstance(userTemp).toPrintable());/* jvm默认延时4s自动开启偏向锁,可通过-XX:BiasedLockingStartupDelay=0取消延时;如果不要偏向锁,可通过-XX:-UseBiasedLocking=false来设置*/Thread.sleep(5000);User  user = new User();System.out.println("启用偏向锁(101):"+ ClassLayout.parseInstance(user).toPrintable());for(int i=0;i<2;i++){synchronized (user){System.out.println("偏向锁(101)(带线程id):"+ ClassLayout.parseInstance(user).toPrintable());}System.out.println("偏向锁释放(101)(带线程id):"+ ClassLayout.parseInstance(user).toPrintable());}new Thread(new Runnable() {@Overridepublic void run() {synchronized (user){System.out.println("轻量级锁(00):"+ ClassLayout.parseInstance(user).toPrintable());try {System.out.println("睡眠3秒========================");Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("轻量级锁--->重量级锁(10):"+ ClassLayout.parseInstance(user).toPrintable());}}}).start();Thread.sleep(1000);System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());new Thread(new Runnable() {@Overridepublic void run() {synchronized (user) {System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());}}}).start();Thread.sleep(5000);System.out.println("无锁状态(001):" + ClassLayout.parseInstance(user).toPrintable());}
}

重量级锁释放之后变为无锁,此时有新的线程来调用同步块,会获取什么锁?

轻量锁

轻量级锁详解

轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

轻量级锁实现原理

在介绍轻量级锁的原理之前,再看看之前MarkWord图。

轻量级锁操作的就是对象头的MarkWord。

如果判断当前处于无锁状态,会在当前线程栈的当前栈帧中划出一块叫LockRecord的区域,然后把锁对象的MarkWord拷贝一份到LockRecord中称之为dhw(就是那个set_displaced_header方法执行的)里。

然后通过CAS把锁对象头指向这个LockRecord。

轻量级锁的加锁过程:

如果当前是有锁状态,并且是当前线程持有的,则将null放到dhw中,这是重入锁的逻辑。

我们再看下轻量级锁解锁的逻辑:

  逻辑还是很简单的,就是要把当前栈帧中LockRecord存储的markword(dhw)通过CAS换回到对象头中。如果获取到的dhw是null说明此时是重入的,所以直接返回即可,否则就是利用CAS换,如果CAS失败说明此时有竞争,那么就膨胀!

偏向锁详解

  偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。


/***StringBuffer内部同步***/
public synchronized int length() { return count; 
} 
//System.out.println 无意识的使用锁 
public void println(String x) { synchronized (this) {print(x); newLine(); } }

偏向锁匿名偏向状态

  当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

偏向锁延迟偏向

  偏向锁模式存在偏向锁延迟机制:HotSpot虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。


//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking 
//启用偏向锁
-XX:+UseBiasedLocking 

验证


@Slf4j
public class LockEscalationDemo{public static void main(String[] args) throws InterruptedException {log.debug(ClassLayout.parseInstance(new Object()).toPrintable());Thread.sleep(4000);log.debug(ClassLayout.parseInstance(new Object()).toPrintable());}
}

4s后偏向锁为可偏向或者匿名偏向状态:

偏向锁状态跟踪实验


public class LockEscalationDemo {public static void main(String[] args) throws InterruptedException {log.debug(ClassLayout.parseInstance(new Object()).toPrintable());//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式Thread.sleep(4000);Object obj = new Object();new Thread(new Runnable() {@Overridepublic void run() {log.debug(Thread.currentThread().getName()+"开始执行。。。\n"+ClassLayout.parseInstance(obj).toPrintable());synchronized (obj){log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"+ClassLayout.parseInstance(obj).toPrintable());}log.debug(Thread.currentThread().getName()+"释放锁。。。\n"+ClassLayout.parseInstance(obj).toPrintable());}},"thread1").start();Thread.sleep(5000);log.debug(ClassLayout.parseInstance(obj).toPrintable());}
}  

对象调用了hashCode不会开启偏向锁

对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的。

偏向锁撤销场景

偏向锁撤销之调用对象HashCode

调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的。

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向:

  • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。

偏向锁撤销之调用wait/notify

偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁


synchronized (obj) {// 思考:偏向锁执行过程中,调用hashcode会发生什么?//obj.hashCode();//obj.notify();try {obj.wait(100);} catch (InterruptedException e) {e.printStackTrace();}log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"+ ClassLayout.parseInstance(obj).toPrintable());
}

偏向锁批量重偏向&批量撤销

  从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

实现原理

  以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

  当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。


intx BiasedLockingBulkRebiasThreshold   = 20   //默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold  = 40   //默认偏向锁批量撤销阈值

我们可以通过-XX:BiasedLockingBulkRebiasThreshold和-XX:BiasedLockingBulkRevokeThreshold来手动设置阈值

应用场景

批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。

批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

批量重偏向实验

当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象的Thread ID


@Slf4j
public class BiasedLockingTest {//延时产生可偏向对象Thread.sleep(5000);// 创建一个list,来存放锁对象List<Object> list = new ArrayList<>();// 线程1new Thread(() -> {for (int i = 0; i < 50; i++) {// 新建锁对象Object lock = new BiasedLockingTest();synchronized (lock) {list.add(lock);}}try {//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活Thread.sleep(100000);} catch (InterruptedException e) {e.printStackTrace();}}, "thead1").start();//睡眠3s钟保证线程thead1创建对象完成Thread.sleep(3000);log.debug("打印thead1,list中第20个对象的对象头:");log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));// 线程2new Thread(() -> {for (int i = 0; i < 40; i++) {Object obj = list.get(i);synchronized (obj) {if(i>=15&&i<=21||i>=38){log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+ClassLayout.parseInstance(obj).toPrintable());}}//if(i==17||i==19){//    log.debug("thread2-第" + (i + 1) + "次释放锁\t"+//            ClassLayout.parseInstance(obj).toPrintable());//}}try {Thread.sleep(100000);} catch (InterruptedException e) {e.printStackTrace();}}, "thead2").start();LockSupport.park();}
}

测试结果:

thread1:创建50个偏向线程thread1的偏向锁 1-50 偏向锁

thread2:

1-19 偏向锁撤销,升级为轻量级锁 thread1释放锁之后为偏向锁状态)
20-40 偏向锁撤销达到阈值(20),执行了批量重偏向

批量撤销实验

当撤销偏向锁阈值超过40次后,jvm会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时


@Slf4j
public class BiasedLockingTest {public static void main(String[] args) throws  InterruptedException {//延时产生可偏向对象Thread.sleep(5000);// 创建一个list,来存放锁对象List<Object> list = new ArrayList<>();// 线程1new Thread(() -> {for (int i = 0; i < 40; i++) {// 新建锁对象Object lock = new BiasedLockingTest();synchronized (lock) {list.add(lock);}}try {//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活Thread.sleep(100000);} catch (InterruptedException e) {e.printStackTrace();}}, "thead1").start();//睡眠3s钟保证线程thead1创建对象完成Thread.sleep(3000);log.debug("打印thead1,list中第20个对象的对象头:");log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));// 线程2new Thread(() -> {for (int i = 0; i < 40; i++) {Object obj = list.get(i);synchronized (obj) {if(i>=15&&i<=21||i>=38){log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+ClassLayout.parseInstance(obj).toPrintable());}}//if(i==17||i==19){//   log.debug("thread2-第" + (i + 1) + "次释放锁\t"+//            ClassLayout.parseInstance(obj).toPrintable());//}}try {Thread.sleep(100000);} catch (InterruptedException e) {e.printStackTrace();}}, "thead2").start();Thread.sleep(3000);new Thread(() -> {for (int i = 0; i < 40; i++) {Object lock =list.get(i);if(i>=17&&i<=21){log.debug("thread3-第" + (i + 1) + "次准备加锁\t"+ClassLayout.parseInstance(lock).toPrintable());}synchronized (lock){if(i>=17&&i<=21){log.debug("thread3-第" + (i + 1) + "次加锁执行中\t"+ClassLayout.parseInstance(lock).toPrintable());}}}},"thread3").start();Thread.sleep(3000);log.debug("查看新创建的对象");log.debug((ClassLayout.parseInstance(new BiasedLockingTest()).toPrintable()));LockSupport.park();}
}

测试结果:

thread3:

1-19 从无锁状态直接获取轻量级锁(thread2释放锁之后变为无锁状态)

20-40 偏向锁撤销,升级为轻量级锁(thread2释放锁之后为偏向锁状态)

达到偏向锁撤销的阈值40,BiasedLockingTest会设置为不可偏向,所以新创建的对象是无锁状态

总结
  1. 批量重偏向和批量撤销是针对类的优化,和对象无关。
  2. 偏向锁重偏向一次之后不可再次重偏向。
  3. 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利

锁升级的流程分析

           }synchronized (lock){if(i>=17&&i<=21){log.debug("thread3-第" + (i + 1) + "次加锁执行中\t"+ClassLayout.parseInstance(lock).toPrintable());}}}},"thread3").start();Thread.sleep(3000);log.debug("查看新创建的对象");log.debug((ClassLayout.parseInstance(new BiasedLockingTest()).toPrintable()));LockSupport.park();
}

}

测试结果:thread3:1-19 从无锁状态直接获取轻量级锁(thread2释放锁之后变为无锁状态)[外链图片转存中...(img-7XKUNu1b-1724205873397)]20-40 偏向锁撤销,升级为轻量级锁(thread2释放锁之后为偏向锁状态)[外链图片转存中...(img-ukE0o4Gl-1724205873398)]达到偏向锁撤销的阈值40,BiasedLockingTest会设置为不可偏向,所以新创建的对象是无锁状态[外链图片转存中...(img-w6jSnilx-1724205873398)]#### 总结1. 批量重偏向和批量撤销是针对类的优化,和对象无关。
2. 偏向锁重偏向一次之后不可再次重偏向。
3. 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利## 锁升级的流程分析[外链图片转存中...(img-WDkBpAXf-1724205873399)]

相关文章:

5、并发锁机制之synchronized

并发锁机制之synchronized i/i--引起的线程安全问题分析原因分析i的JVM字节码指令i--的JVM 字节码指令结论 解决方案 synchronized的使用加锁方式使用synchronized解决之前的共享问题方式一方式二 synchronized底层实现原理分析查看synchronized的字节码指令序列重量级锁实现之…...

职场那些事:应对施暴者的智慧

在职场中&#xff0c;有些人用各种手段让你感到难受&#xff0c;让你屈服&#xff0c;甚至适应他们的变态心理。面对这种人&#xff0c;最重要的就是保持低调。&#x1f910; 很多同事选择倾诉&#xff0c;然而这其实是个大错特错。正如鲁迅所说&#xff0c;“同事间的悲欢并不…...

精细化管理和智慧化运营的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。用…...

vue项目配置基础路由vue-router

1、运行以下命令安装vue-router yarn add vue-router 2、在src目录下的components中新建两个vue页面 3、在src目录下新建router文件夹&#xff0c;在router文件夹下面新建index.js文件 4、配置main.js文件 //引入Vue import Vue from "vue"; //引入App import App…...

2024年入职/转行网络安全,该如何规划?_网络安全职业规划

前言 前段时间&#xff0c;知名机构麦可思研究院发布了 《2022年中国本科生就业报告》&#xff0c;其中详细列出近五年的本科绿牌专业&#xff0c;其中&#xff0c;信息安全位列第一。 网络安全前景 对于网络安全的发展与就业前景&#xff0c;想必无需我多言&#xff0c;作为…...

【Python系列】执行 Shell 命令的六种方法

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

2024华为OD机试真题-部门人力分配Python-C卷D卷-200分

【华为OD机试】-(C卷+D卷)-2024最新真题目录 目录 题目描述 输入描述 输出描述 用例1 考点 解题思路 代码 题目描述 部门在进行需求开发时需要进行人力安排。 当前部门需要完成 N 个需求,需求用 requirements 表述,requirements[i] 表示第 i 个需求的工作量大小,单…...

【Leetcode 1436 】旅行终点站—— 哈希表

给你一份旅游线路图&#xff0c;该线路图中的旅行线路用数组 paths 表示&#xff0c;其中 paths[i] [cityAi, cityBi] 表示该线路将会从 cityAi 直接前往 cityBi 。请你找出这次旅行的终点站&#xff0c;即没有任何可以通往其他城市的线路的城市。 题目数据保证线路图会形成一…...

springboot自动配置原理-面试题

网络上看很多文章并没什么用&#xff0c;重点没说到&#xff0c;不知道从那里入手讲&#xff0c;刷到的直接按照下面这个&#xff0c;背出来就行了 1、当启动springboot应用程序的时候&#xff0c;会先创建SpringApplication的对象&#xff0c;在对象的构造方法中会进行某些参数…...

【C++题解】1140 - 亲密数对

欢迎关注本专栏《C从零基础到信奥赛入门级&#xff08;CSP-J&#xff09;》 问题&#xff1a;1140 - 亲密数对 类型&#xff1a;自定义函数 题目描述&#xff1a; 键盘输入 N &#xff0c;N 在 2 至 2000 之间&#xff0c;求 2 至 N 中的亲密数对&#xff0c;就是 A 的因子和…...

学习大数据DAY40 基于 hive 的数据处理

目录 Hive 复合数据定义方法 Hive 复合数据查询方法 hive 内置函数 上机练习 Hive 复合数据定义方法 Hive 复合数据查询方法 hive 内置函数 -- 查看系统自带的函数 show functions; -- 显示自带的函数的用法 desc function upper; -- 详细显示自带的函数的用法 desc …...

[数据集][目标检测]手钳检测数据集VOC+YOLO格式141张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;141 标注数量(xml文件个数)&#xff1a;141 标注数量(txt文件个数)&#xff1a;141 标注类别…...

SQL注入(head、报错、盲注)

目录 【学习目标、重难点知识】 【学习目标】 【重难点知识】 1. 报错注入 1.1 那么什么是报错注入呢&#xff1f; 1.2 报错注入原理 extractvalue函数 updatexml函数 1.3 靶场解析 靶场练习 2. HEAD注入 2.1 相关全局变量 2.2 靶场解析 burp暴力破解 靶场练习 3…...

30. 包含 min 函数的栈

comments: true difficulty: 简单 edit_url: https://github.com/doocs/leetcode/edit/main/lcof/%E9%9D%A2%E8%AF%95%E9%A2%9830.%20%E5%8C%85%E5%90%ABmin%E5%87%BD%E6%95%B0%E7%9A%84%E6%A0%88/README.md 面试题 30. 包含 min 函数的栈 题目描述 定义栈的数据结构&#xff…...

五、OpenTK图形渲染基础

文章目录 一、顶点数据(一)顶点坐标、颜色、纹理坐标的定义(二)顶点数组的组织二、图元绘制(一)点列表、线列表、线带、三角形列表、三角形带的绘制(二)绘制模式(GL_POINTS、GL_LINES、GL_TRIANGLES 等)三、清除屏幕一、顶点数据 (一)顶点坐标、颜色、纹理坐标的定…...

桔子哥/基于云快充协议1.5版本的充电桩系统软件-充电桩系统 -新能源车充电平台源码

基于云快充协议1.5版本的充电桩系统软件 介绍 SpringBoot 框架&#xff0c;充电桩平台充电桩系统充电平台充电桩互联互通协议云快充协议1.5-1.6协议新能源汽车二轮车公交车二轮车充电-四轮车充电充电源代码充电平台源码Java源码 软件功能 小程序端&#xff1a;城市切换、附…...

零基础5分钟上手亚马逊云科技-高可用Web系统设计最佳实践

简介&#xff1a; 欢迎来到小李哥全新亚马逊云科技AWS云计算知识学习系列&#xff0c;适用于任何无云计算或者亚马逊云科技技术背景的开发者&#xff0c;通过这篇文章大家零基础5分钟就能完全学会亚马逊云科技一个经典的服务开发架构方案。 我会每天介绍一个基于亚马逊云科技…...

培训学校课程管理系统-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…...

基于STM32的智能婴儿床控制系统设计(手机APP+蓝牙无线控制)(210)

文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】设计实现的功能【3】项目硬件模块组成1.2 设计思路【1】整体设计思路【2】HC05工作模式配置1.3 项目开发背景【1】选题的意义【2】可行性分析【3】参考文献【4】项目背景【5】摘要1.4 开发工具的选择【1】设备端开发【2】上…...

四、前后端分离通用权限系统(4)

&#x1f33b;&#x1f33b; 目录 一、前端开发和前端开发工具1.1、前端开发介绍1.2、下载和安装 VS Code1.2.1、下载地址1.2.2、插件安装1.2.3、创建项目1.2.4、保存工作区1.2.5、新建文件夹和网页1.2.6、预览网页1.2.7、设置字体大小 二、Node.js2.1、Node.js 简介2.1.1、什么…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

AI Agent与Agentic AI:原理、应用、挑战与未来展望

文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例&#xff1a;使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例&#xff1a;使用OpenAI GPT-3进…...

【配置 YOLOX 用于按目录分类的图片数据集】

现在的图标点选越来越多&#xff0c;如何一步解决&#xff0c;采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集&#xff08;每个目录代表一个类别&#xff0c;目录下是该类别的所有图片&#xff09;&#xff0c;你需要进行以下配置步骤&#x…...

USB Over IP专用硬件的5个特点

USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中&#xff0c;从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备&#xff08;如专用硬件设备&#xff09;&#xff0c;从而消除了直接物理连接的需要。USB over IP的…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)

RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发&#xff0c;后来由Pivotal Software Inc.&#xff08;现为VMware子公司&#xff09;接管。RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用 Erlang 语言编写。广泛应用于各种分布…...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...

安卓基础(Java 和 Gradle 版本)

1. 设置项目的 JDK 版本 方法1&#xff1a;通过 Project Structure File → Project Structure... (或按 CtrlAltShiftS) 左侧选择 SDK Location 在 Gradle Settings 部分&#xff0c;设置 Gradle JDK 方法2&#xff1a;通过 Settings File → Settings... (或 CtrlAltS)…...

如何通过git命令查看项目连接的仓库地址?

要通过 Git 命令查看项目连接的仓库地址&#xff0c;您可以使用以下几种方法&#xff1a; 1. 查看所有远程仓库地址 使用 git remote -v 命令&#xff0c;它会显示项目中配置的所有远程仓库及其对应的 URL&#xff1a; git remote -v输出示例&#xff1a; origin https://…...

Linux基础开发工具——vim工具

文章目录 vim工具什么是vimvim的多模式和使用vim的基础模式vim的三种基础模式三种模式的初步了解 常用模式的详细讲解插入模式命令模式模式转化光标的移动文本的编辑 底行模式替换模式视图模式总结 使用vim的小技巧vim的配置(了解) vim工具 本文章仍然是继续讲解Linux系统下的…...

qt+vs Generated File下的moc_和ui_文件丢失导致 error LNK2001

qt 5.9.7 vs2013 qt add-in 2.3.2 起因是添加一个新的控件类&#xff0c;直接把源文件拖进VS的项目里&#xff0c;然后VS卡住十秒&#xff0c;然后编译就报一堆 error LNK2001 一看项目的Generated Files下的moc_和ui_文件丢失了一部分&#xff0c;导致编译的时候找不到了。因…...