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

天津网站制作西安html5网站开发实战

天津网站制作西安,html5网站开发实战,网站建设与管理视频,成都品牌logo设计目录 synchronized关键字详解1、synchronized关键字简介2、synchronized作用和使用场景作用使用场景①、用在代码块上(类级别同步)②、用在代码块上(对象级别同步)③、用在普通方法上(对象级别同步)④、用在静态方法上(类级别同步)总结#xff1a; 3、synchronized底层原理 3、synchronized底层原理JVM层面IDEA配置javap工具查看字节码查看同步代码块的字节码分析同步代码块字节码查看同步方法的字节码分析同步方法字节码补充知识点对象头Monitor监视器监视器锁synchronized到底锁的是哪个对象让秀逗告诉你锁的本质(通俗点的例子)Java中为什么要设计成全员皆锁 4、synchronized和wait、notify首先需要先明确一个问题为什么wait()和notify()方法必须和synchronized一起使用等待监视器锁的线程放在哪儿了引出生产者-消费者模式 5、synchronized监视器锁可重入原理动画总结synchronized 监视器锁原理6、synchronized锁升级过程补充知识点Mutex Lock互斥锁内核态和用户态内核态和用户态的切换JDK6之前synchronized为什么性能不好真正的主角登场(JDK1.6对锁的升级)对锁的优化方式介绍锁的升级过程查看对象的内存结构①、无锁②、偏向锁的匿名偏向状态③、 匿名偏向状态→偏向锁③、无锁或偏向锁→轻量级锁④、轻量级锁→重量级锁锁升级图示 7、总结synchronized保证原子、可见、有序性原理①、Java代码层面②、JVM 层面③、操作系统层面④、处理器层面 补充为什么有synchronized了JDK还要设计Lock写在最后 synchronized关键字详解 1、synchronized关键字简介 synchronized 直译为 同步它的作用是实现线程同步 synchronized 能够确保同一时刻只有一个线程可以执行某个代码块或方法我们可以把共享变量的修改放在synchronized 修饰的代码块中或者方法中从而避免多个线程同时访问共享资源时引发的线程安全问题。 2、synchronized作用和使用场景 作用 保证线程安全。 由于synchronized的同步机制能够确保同一时刻只有一个线程可以执行某个代码块 所以可以保证并发场景下的原子性有序性可见性。 使用场景 ①、用在代码块上(类级别同步) 示例 public class TestA {private static int count 0;public static void main(String[] args) {Thread t1 new Thread(() - {synchronized (TestA.class) {for (int i 0; i 2000; i) {count 1;}}});Thread t2 new Thread(() - {synchronized (TestA.class) {for (int i 0; i 2000; i) {count 1;}}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(count); // 4000} } 上面例子使用的是类级别的锁synchronized (TestA.class)即TestA的Class对象, 类级别的锁粒度较大。 对于类级别的同步使用类对象如 TestA.class或静态变量如 private static final Object lock new Object();作为锁对象确保所有实例共享同一个锁。 补充知识点(后续整理JVM相关知识点会详细说到) 在同一个类加载器下对于同一个类Class对象是唯一的(所以我们上面可以使用TestA.class作为类级别的锁)。 TestA.class文件中的类只是通过默认的类加载路径被加载而不涉及自定义类加载器那么在整个JVM运行期间无论创建多少个TestA类的对象TestA.class对应的Class对象都是同一个。 但是不同的类加载器可以产生不同的Class对象即使它们加载的类的字节码是相同的。 ②、用在代码块上(对象级别同步) 示例 public class TestA {public int count 0;private final Object lock new Object();public static void main(String[] args) {TestA testA1 new TestA();for (int i 0; i 2000; i) {new Thread(() - {testA1.counter();}).start();}TestA testA2 new TestA();for (int i 0; i 2000; i) {new Thread(() - {testA2.counter();}).start();}System.out.println(testA1.count); // 2000System.out.println(testA2.count); // 2000}public void counter() {synchronized (lock) {count;}}public int getCount() {return count;} } 对于对象级别的同步使用实例变量private final Object lock new Object();作为锁对象确保每个实例独立同步。 ③、用在普通方法上(对象级别同步) public class TestA {public int count 0;public static void main(String[] args) {TestA testA new TestA();Thread t1 new Thread(() - {for (int i 0; i 2000; i) {testA.counter();}});Thread t2 new Thread(() - {for (int i 0; i 2000; i) {testA.counter();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(testA.count);}public synchronized void counter() {count;}}synchronized修饰普通方法它隐式地以调用该方法的对象作为锁对象,即隐含的锁对象是this关键字所指的对象在上面的例子中就是 testA对象。 synchronized修饰普通方法相当于对象锁。对于上面的例子锁的范围仅限于TestA的具体实例testA。 ④、用在静态方法上(类级别同步) public class TestA {public static int count 0;public static void main(String[] args) {Thread t1 new Thread(() - {for (int i 0; i 2000; i) {counter();}});Thread t2 new Thread(() - {for (int i 0; i 2000; i) {counter();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(count);}public synchronized static void counter() {count;}} synchronized修饰静态方法隐含的锁对象是类的 Class 对象实例在上面的例子中就是 TestA.class对象。 当synchronized修饰静态方法时锁作用于整个类的所有实例。 总结 ①、synchronized关键字用在代码块上锁是synchonized括号里配置的对象可以实现类级别的同步(使用类的Class对象作为锁或者使用静态实例变量作为锁)。 也可以实现对象级别的同步(使用类的实例变量作为锁)。 ②、synchronized关键字用在普通方法上可以实现对象级别的同步隐式地以调用该方法的对象作为锁对象,即隐含的锁对象是this关键字所指的对象。 ③、synchronized关键字用在静态方法上可以实现类级别的同步隐含的锁对象是类的 Class 对象实例。 补充 synchronized关键字不能用在构造方法上构造方法本身由JVM保证线程安全但是不保证构造方法中使用的共享变量的线程安全我们可以利用final修饰构造方法中使用的共享变量达到线程安全的目的。final关键字在线程安全的应用(后面另写一篇文章总结)。 3、synchronized底层原理JVM层面 上面说了三种synchronized关键字的使用方式都提到了锁而且只有synchronized关键字用在代码块上我们显示的给了一个对象作为锁代码块或者方法执行完毕了我们也不需要释放锁。 下面我们看看synchronized到底是怎样使用锁以及怎么释放锁的。 IDEA配置javap工具查看字节码 配置 除了-v 还有其他参数可以使用 -c显示方法的字节码指令。 -s显示字段和方法的内部类型签名。 -v输出详细信息包括常量池、方法、字段等的修饰符和属性。 -l显示行号和局部变量表便于调试和理解字节码与源代码的对应关系。 这些参数也可以一起使用。 (推荐都加上输入更详细的信息) 使用 查看同步代码块的字节码 查看下下面这段Java代码生成的字节码内容 public class TestA {public static void main(String[] args) {synchronized (TestA.class){// ...}} }同步代码块字节码 下面只截取了一部分字节码。 Code:stack2, locals3, args_size10: ldc #2 // class TestA2: dup3: astore_14: monitorenter5: aload_16: monitorexit7: goto 1510: astore_211: aload_112: monitorexit13: aload_214: athrow15: return分析同步代码块字节码 这里仅分析字节码中 monitorenter 和 monitorexit 这两个JVM指令。 monitorenter 和 monitorexit 是 Java 虚拟机JVM指令集中的两个特殊指令它们用于实现 Java 中synchronized 关键字的同步机制。 在 Java源代码编译阶段javac 编译编译器遇到由 synchronized 关键字修饰的方法或代码块时它会在字节码(.class文件)级别生成相应的 monitorenter 和 monitorexit 指令。 其中 monitorenter 指令会被插入到 synchronized 块的开始处以及 synchronized 方法的入口。它的作用是尝试获取对象的内部锁监视器锁。如果锁未被占用则当前线程可以获得锁并继续执行如果锁已被其他线程占用则当前线程将被阻塞直到锁被释放。 monitorexit 指令会被插入到 synchronized 块的结束处以及可能抛出异常的路径上确保即使发生异常也能释放锁。当线程执行完 synchronized 块的代码或抛出异常时monitorexit 指令会释放之前获取的锁允许其他等待的线程有机会获取锁并进入同步块。 画个图演示下 monitorenter 和 monitorexit 流程 这里的所计数器操作涉及到锁重入的概念。 下面第5点 有详细介绍。 查看同步方法的字节码 查看下下面这段Java代码生成的字节码内容 public class TestA {public synchronized void method() {System.out.println(synchronized修饰方法);} }同步方法字节码 下面只截取了一部分字节码。 public synchronized void method();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack2, locals1, args_size10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String synchronized 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 3: 0line 4: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 this LTestA;分析同步方法字节码 synchronized 修饰方法是通过 ACC_SYNCHRONIZED 标识来实现方法级别的同步。ACC_SYNCHRONIZED 是Java类文件中方法的访问标志之一用来标识方法是否被 synchronized 关键字修饰。指示该方法在执行时会获取对象的监视器其他线程需要等待锁的释放才能执行该方法的代码。 补充知识点对象头 每个 Java 对象实例都有一个对象头Object Header对象头包含几部分分别是: Mark Word: Mark Word 是对象头的一部分用于存储对象的运行时数据如哈希码HashCode、GC 分代年龄Generational GC age、锁状态标志、线程持有的锁、偏向线程 ID 等。Mark Word 是一个非常灵活的结构在不同的对象状态下会有不同的表示方式。下面会说明。 Klass Pointer: 这部分存储的是对象所属类的元数据指针。通过这个指针可以找到对象的类信息包括类的名字、父类、方法表等。 Array length: 这个部分只存在于数组对象中用于存储数组的长度。只有数组对象才有这部分内容。 对于普通对象对象头包括Mark Word、Klass Pointer。 对于数组对象对象头包括Mark Word、Klass Pointer、Array length。 Mark Word在不同的对象状态下会有不同的表示方式: 下表是64位JVM下的 Mark Word 各个位储存的数据变化情况(参考了一些资料整理的表格可能不够准确如果有问题欢迎指正) 解释下上面偏向锁的ThreadID和Epoch先留个印象下面锁升级过程会详细介绍 ThreadID54bit ThreadID 存储持有偏向锁的线程的唯一标识符ID。偏向锁的设计目的是为了在无竞争的情况下减少同步的开销。当一个对象第一次被一个线程获取时(必须是一个线程且对象是匿名偏向状态)会在 Mark Word 中记录该线程的 ID以表示该对象偏向于该线程。 作用: 当该线程再次进入同步块时如果对象的 Mark Word 中的 ThreadID 与当前线程的 ID 匹配线程就可以直接进入而无需进行任何同步操作从而提高了性能。 Epoch2bit Epoch 是一个用于表示偏向锁的时间戳或时期的字段。它用来区分不同时间段内的偏向锁避免出现锁膨胀和长时间持有偏向锁的问题。 作用: Epoch 字段在某些情况下会被更新例如当偏向锁被撤销时Epoch 会增加这样可以防止旧的偏向锁信息干扰新的偏向锁。它确保偏向锁机制能够正常工作并在需要时重新分配偏向锁。 指向线程栈中锁记录的指针 当一个线程尝试获取轻量级锁时JVM 会在该线程的栈中创建一个锁记录Lock Record。 锁记录用于存储 Mark Word 的拷贝以及其他锁状态信息。线程尝试将对象头中的 Mark Word 替换为指向线程栈中锁记录的指针。 替换操作通过 CAS 完成以确保操作的原子性。 如果 CAS 操作成功Mark Word 中存储的就是指向线程栈中锁记录的指针此时线程获得轻量级锁。 Mark Word 的内容变为指向锁记录的指针同时锁记录中保存原始的 MarkWord。 (说人话就是对象头Mark Word只有几个字节的空间存不下轻量级锁的信息了所以把锁的信息和原Mark Word里信息放到线程栈里面去存一个锁记录Mark Word只存这个锁记录的指针方便快速获取锁记录的详细信息) 指向重量级锁的指针 当锁膨胀为重量级锁时JVM在堆中创建一个监视器对象用于管理锁的信息。 对象头中的Mark Word更新为指向监视器对象的指针。 通过这个指针线程可以访问并操作重量级锁的详细信息。 Monitor监视器 在Java中每一个对象都有一个关联的监视器这个监视器用于实现线程同步。监视器是一个结构它包含了一些用于控制线程访问共享资源的数据和逻辑。 监视器的功能 同步方法和代码块监视器用于实现同步方法和synchronized代码块。当一个线程进入一个同步方法或同步代码块时它必须首先获得该对象的监视器。 线程管理监视器负责管理等待和通知机制wait/notify/notifyAll使得线程可以在条件不满足时等待条件满足时被唤醒。 监视器锁 监视器锁是监视器的一部分确保同一时间只有一个线程能够执行同步代码块或同步方法。每个对象都有一个隐式的监视器锁当线程需要进入一个同步代码块或方法时它必须先获得这个锁。 监视器锁的工作过程 获取锁 当一个线程进入synchronized方法或代码块时它尝试获取监视器锁。如果锁被其他线程持有则该线程会被阻塞直到锁被释放。 ( 监视器锁在有些资料上也叫内置锁属于X锁(即排他锁) ) 释放锁 当线程退出synchronized方法或代码块时它会释放监视器锁允许其他被阻塞的线程获取锁并执行同步代码。 重入锁 Java的监视器锁是可重入锁这意味着如果一个线程已经持有了一个监视器锁它可以再次进入由同一个监视器保护的同步代码而不会被阻塞。 synchronized到底锁的是哪个对象 synchronized表面上给使用者的感觉是代码块或者函数之间形成了互斥同一时间只能有一个线程可以执行到synchronized修饰的代码块或者函数。这是synchronized表面给使用者最直观的感觉。 但是实际上synchronized是给对象加了锁。当一个线程进入synchronized方法或代码块时就会去获取这个对象的锁上面说了这个锁叫监视器锁。 下面我们来分析下锁到底加在了哪个对象上。 ①、对于静态成员函数锁是加在类的Class对象上面的。相当于代码块的synchronized (TestA.class) public class TestA {public static void main(String[] args) {synchronized (TestA.class){// 锁的是 TestA.class 这个对象}}public synchronized static void aa(){// 锁的也是 TestA.class 这个对象} }②、对于非静态成员函数锁是加在this对象上面的。相当于代码块的synchronized (this) public synchronized void aa(){synchronized (this){// 锁的是 this 这个对象}}public synchronized void bb(){// 锁的也是 this 这个对象}让秀逗告诉你锁的本质(通俗点的例子) 通俗点的例子 一个比较形象的比喻锁相当于狗狗王国里的五星上将秀逗(也就是狗狗王国的保安)并且这个叫秀逗的狗狗非常称职它是35岁的程序员再就业少走20年弯路应聘上的狗狗王国的保安。 秀逗深谙互斥锁的道理秀逗把都每个回家的狗狗都当成一个线程这对于曾经做过程序员的秀逗来说简直小菜一碟每个狗狗想通过狗狗王国大门前的门禁都必须得到秀逗的许可秀逗认为自己就是一把锁大门的门禁就是秀逗锁住的代码块或者函数同一时刻秀逗只允许一只狗狗走过门禁。 这个时候大黄来了秀逗说站住你必须等前面的四眼走完门禁的这段路你才能进去。于是大黄只能乖乖等四眼走过去之后才能继续进入门禁。 上面的例子秀逗是锁大黄和四眼就是线程门禁就是同步代码块或者同步的方法。 程序员的角度 从程序员的角度来看锁就是一个“对象”这个对象要完成一些事情 ①、需要记录当前有没有线程获得锁。 (比如上面说的秀逗不让大黄进入门禁因为现在门禁里面还有狗狗没走完) ②、需要记录当前获得锁的是哪个线程。秀逗得知道目前正在通过门禁的是四眼 ③、需要记录还有哪些线程在阻塞等待获取锁。(秀逗得知道后面还有多个狗狗在排队等待进入门禁) 那么我们把这个锁对象单独拎出来实现上面说的三个功能。我们就可以把这个对象放到 synchronized () 的括号里作为锁。 比如上面的秀逗可以作为锁把秀逗当成锁对象 synchronized (秀逗) { // 需要同步访问的代码 }这个时候狗狗王国里面的物业经理二哈看秀逗工作轻松每月轻轻松松月入2千根本花不完二哈不乐意了二哈觉得既然秀逗可以当锁对象那四眼大黄也能当只要四眼大黄或者其他什么狗狗都掌握上面说的三点技能 ①、需要记录当前有没有线程获得锁。 ②、需要记录当前获得锁的是哪个线程。 ③、需要记录还有哪些线程在阻塞等待获取锁。 那么任何一个狗狗都能当锁。 二哈觉得这个主意不错以后让狗狗们自己都掌握锁对象的技能然后走门禁的时候谁正在门禁内谁就是锁对象后面的狗狗就得排队。 至此秀逗掌握的技能其他狗狗都掌握了。狗狗王国也不需要秀逗这个保安了因为狗狗王国里面的每个狗狗对象现在都掌握了锁的技能它们每个人都可以当保安(锁对象)。 这样门禁的管理更方便了。 只有秀逗默默流下了泪水~ 好秀逗的故事说完了我们再来说一下Java中的锁。 上面说了锁也是对象并且我们的共享资源(比如变量、代码块、方法等)本身也属于对象的一部分。 于是Java语言就把锁和线程要访问的共享资源对象合二为一。 让Java中每个对象都能作为锁这样synchronized就能够把任意对象当做锁来用。 为了实现每个对象都能作为锁这个目的于是就有了对象头中的Mark Word以及与对象关联的Monitor当然Mark Word结合Monitor能干的事情包含但不限于下面这三点 ①、需要记录当前有没有线程获得锁。 ②、需要记录当前获得锁的是哪个线程。 ③、需要记录还有哪些线程在阻塞等待获取锁。 是不是到这儿就豁然开朗了。 最后总结下这个例子 锁的比喻 秀逗 代表锁。作为狗狗王国的保安秀逗控制狗狗们线程通过门禁同步代码块或方法。 大黄和四眼 代表线程。它们需要等待秀逗锁许可才能通过门禁同步代码块或方法。 锁的功能 记录当前有没有线程获得锁秀逗控制同一时刻只允许一个狗狗通过门禁。 记录当前获得锁的线程秀逗知道当前正在通过门禁的是哪个狗狗线程。 记录等待获取锁的线程秀逗知道哪些狗狗在排队等候通过门禁。 锁对象的实现 任何狗狗都可以作为锁对象只要掌握了记录当前锁状态、当前锁定线程和等待线程的技能。 在Java中锁对象与共享资源对象合二为一每个对象都能作为锁通过synchronized关键字实现同步。 Mark Word和Monitor的作用 记录锁的状态当前有没有线程获得锁。 记录获得锁的线程当前获得锁的是哪个线程。 记录等待获取锁的线程哪些线程在等待获取锁。 还有一些其他功能比如记录获取锁后又释放锁进入等待状态的线程等。 Java中为什么要设计成全员皆锁 下面总结了几个方面的原因 简化同步机制 在Java中每个对象都可以作为锁这使得同步机制变得非常简单和直观。程序员可以使用任何对象来实现同步而不需要额外的锁对象。这种设计避免了创建和管理独立锁对象的复杂性。 统一的锁定模型 统一的锁定模型意味着每个对象都有相同的同步行为和机制。程序员不需要记住额外的规则或模式来使用锁因为所有对象都可以用于同步。只需使用synchronized关键字即可。 提高灵活性 由于每个对象都可以作为锁程序员可以根据具体的需求选择合适的对象进行同步。这样能够更灵活地设计同步逻辑。例如可以使用方法所在的对象this作为锁也可以使用类的静态成员作为锁。 提高代码可读性和可维护性 在代码中使用共享资源所在的对象进行同步例如使用共享资源对象本身可以提高代码的可读性和可维护性。这样读代码的人可以很容易地理解同步的意图而不需要去寻找额外的锁对象。 效率优化 Java虚拟机JVM可以对锁的实现进行多种优化例如偏向锁、轻量级锁和重量级锁的不同优化策略。这些优化可以在不改变程序员使用锁的方式的情况下提高性能。 支持细粒度锁定 通过让每个对象都可以作为锁Java允许程序员使用细粒度的锁定策略以减少锁争用和提高并发性能。例如可以为不同的资源使用不同的锁对象从而实现更高效的并发控制。 上面都是比较书面的表达说人话就是 这样设计简单方便、灵活好用方便(JVM)优化。就像为synchronized打广告一样~ 4、synchronized和wait、notify 首先需要先明确一个问题 为什么wait()和notify()方法必须和synchronized一起使用 要回答这个问题我们需要先明确wait()和notify()方法的作用线程通信。而且要保证线程通信的正确性。 wait() 当一个线程执行wait()时它会放弃持有的对象锁并进入该对象的等待队列中直到其他线程调用notify()或notifyAll()唤醒它。线程被唤醒后需要重新获得对象的锁才能继续执行。 notify() 当一个线程执行notify()时它会随机唤醒在该对象等待队列中等待的一个线程如果有的话。被唤醒的线程会尝试重新获得对象的锁成功后继续执行。 notifyAll() 唤醒所有在该对象等待队列中等待的线程。被唤醒的线程会依次尝试重新获得对象的锁成功后继续执行。 假如不在synchronized中使用wait()和notify()方法会怎么样呢 看下面的例子 我使用两个线程分别执行两个方法这两个方法都使用同一个对象分别调用了wait()和notify()并且没使用synchronized同步。 public class TestA {public static void main(String[] args) {TestA testA new TestA();new Thread(()-{try {testA.aa();} catch (InterruptedException e) {e.printStackTrace();}}).start();new Thread(()-{testA.bb();}).start();}public void aa() throws InterruptedException {this.wait();}public void bb(){this.notify();} } 执行结果 抛了异常 IllegalMonitorStateException Exception in thread Thread-0 Exception in thread Thread-1 java.lang.IllegalMonitorStateException可以想像一下上面两个线程之间通信但是对于同一个对象this(指向的是testA对象)来说一个方法要等待一个要唤醒。 这个种操作本身就需要同步如果没有同步就会出现线程安全问题。 比如上面代码就可能出现一种情况 notify() 是在 wait() 之前调用的没有同步无法保证线程执行的顺序则等待的线程可能会永远等待而导致死锁等线程安全问题。 所以说 Java 明确要求 wait()、notify() 和 notifyAll() 必须在持有对象监视器的前提下调用即这些方法必须在同步块或同步方法中调用否则会抛出 IllegalMonitorStateException 异常。 现在我们再思考一个之前在Java基础知识中提到过的一个小知识点 为什么 wait()、notify() 和 notifyAll() 是和线程通信相关的方法却放在了Object类中定义 相信通过上面的讲解对于这个问题就有非常明确的答案了之前我在Java基础知识点这篇文章中说因为锁可以是任意的对象所以wait()、notify() 和 notifyAll()这些方法只有放在所有类的父类Object中才能满足锁可以是任意的对象这个条件。 现在再结合上面对于 对象头Mark Word、对象监视器监视器锁的讲解应该能对这个问题会有更深刻的理解。 等待监视器锁的线程放在哪儿了引出生产者-消费者模式 等待监视器锁的线程通常放在对象的监视器Monitor的等待队列中。Java中每个对象都有一个监视器负责管理锁的状态和线程的同步。 去瞅一眼JVM源码对wait()、notify()的注释关于JVM源码的下载可以看下之前的文章 Java中volatile关键字详解 查看 objectMonitor.cpp 和objectMonitor.hpp这两个C源码文件。 下面是objectMonitor的构造函数 ObjectMonitor() {_WaitSet NULL;_cxq NULL ;_EntryList NULL ; // ... 省略其他}_cxq: _cxq 是 ObjectMonitor 类的一个成员变量表示竞争队列Contended Queue。 在多线程环境中当有多个线程竞争获取同一个对象的锁时它们会被放置在 _cxq 中等待获取锁。 _cxq 中的线程会在锁释放时被移动到 _EntryList 中。 _EntryList: _EntryList 是 ObjectMonitor 类的另一个成员变量表示进入列表Entry List。 当一个线程成功获取对象的锁时它会从 _EntryList 中移除。 如果有多个线程等待获取锁它们会按照一定的策略如 FIFO被放置在 _EntryList 中等待。 _WaitSet: _WaitSet 是 ObjectMonitor 类的成员变量表示等待集合Wait Set。 当线程调用对象的 wait() 方法时它会释放对象的锁并进入 _WaitSet 等待状态。 当其他线程调用对象的 notify() 或 notifyAll() 方法时会从 _WaitSet 中选择一个或多个线程移动到 _EntryList 或 _cxq 中。 这和我这篇文章中 BlockingQueue详解 中利用阻塞队列实现线程同步的思想其实是一致的。 实际上利用队列实现通知机制的思想是一种经典的并发编程模式它通过队列作为中介实现了多线程之间的解耦和通信。 我们常见的线程池任务分发、消息队列、事件驱动等都是利用的这个思想。 那这个模式书面名称叫啥呢? 没错就是大名鼎鼎的Producer-Consumer Pattern(生产者-消费者模式). 5、synchronized监视器锁可重入原理 可重入锁指的是同一个线程在持有锁的情况下可以多次进入同步代码块或同步方法而不会因为自己已经持有锁而阻塞。 synchronized可重入性的实现主要依赖于每个对象的监视器锁monitor lock和线程的线程栈帧中的锁计数器lock count 需要注意 在 Java 中线程的锁计数器lock count是针对监视器锁monitor lock的也就是使用 synchronized 关键字获取的锁。这种锁计数器仅适用于监视器锁而不适用于其他类型的锁比如 ReentrantLock 或者其他自定义的锁实现。 当一个线程进入同步代码块或同步方法时会尝试获取对象的监视器锁。如果对象的监视器锁未被其他线程持有当前线程将获取到锁并将锁计数器设置为1。如果当前线程已经持有了对象的监视器锁那么在锁计数器上递增。当线程退出同步代码块或同步方法时锁计数器会递减。当锁计数器递减为0时表示当前线程已经完全释放了对象的监视器锁其他等待线程可以尝试获取锁。 例如 public class TestA {public static void main(String[] args) {TestA testA new TestA();Thread t1 new Thread(() - {for (int i 0; i 3; i) {testA.aa();}}, t1);t1.start();}public synchronized void aa() {String name Thread.currentThread().getName();System.out.println(name 线程执行了aa方法);}}结果 t1线程执行了aa方法 t1线程执行了aa方法 t1线程执行了aa方法t1线程执行了三次synchronized 修饰的aa方法实际上只需要第一次获取testA对象的监视器锁即可后面执行都是锁重入。 动画总结synchronized 监视器锁原理 通过上面的整理梳理应该能大致理解synchronized的锁原理了。 这里再画个动画来梳理下多个线程调用synchronized代码块或者方法获取和释放或者等待的过程, 帮助理解。 这里就不截图了眨眼补帧吧。哈哈~ 调用 wait() 和 notify() 需要用到_WaitSet队列动画没画可以自行脑补一下。 因为画这玩意儿太费时间了我偷懒了~ 这里copy一张其他博客上的图来说明这几个队列之间数据的流转 下面张图片来源https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/ 6、synchronized锁升级过程 先回答一个问题。 为什么在JDK6当中优化了监视器锁。 先来段废话文学~ 因为JDK6之前的锁性能低。通过JDK6当中优化了许多锁的技术细节提高了获取和释放锁整体操作的性能。 好了忘记上面的废话文学吧还是来点靠谱的分析吧。 补充知识点 上面我们已经分析了synchronized监视器锁在JVM层面的原理。 下面我们继续往底层探索探索。 Java代码的synchronized关键字在编译成.class字节码时会生成monitorenter 和 monitorexit 指令。 在 JVM 层面 monitorenter 和 monitorexit 指令用于实现监视器锁。每个对象都有一个与之关联的监视器锁当一个线程进入同步代码块时它会尝试获取对象的监视器锁如果成功则进入同步块否则该线程会阻塞直到获得锁。 在操作系统层面 JVM 的监视器锁最终依赖操作系统提供的同步机制。以 x86-64(AMD64) 架构处理器和 Windows 64 位操作系统为例JVM 的锁实现会依赖于操作系统提供的 Mutex Lock 来实现线程之间的互斥访问。 在处理器硬件层面 比如x86-64 架构处理器提供了原子操作指令如 lock cmpxchg 和 lock xchg这些指令能够在多核处理器环境中确保某些操作的原子性、可见性、有序性防止并发访问导致的数据竞争。本质上就是lock前缀指令可以去看 我的这篇文章volatile关键字详解里面有介绍lock前缀指令。 Mutex Lock互斥锁 Mutex Lock互斥锁是一种同步机制用于确保在多线程或多进程环境中同一时刻只有一个线程或进程可以访问共享资源。它是通过操作系统提供的原语如特殊的CPU指令或者操作系统的内核功能来实现的。 Mutex Lock是操作系统的内核层功能。 内核态和用户态 CPU硬件层面 内核态Kernel Mode CPU可以执行所有指令包括特权指令。CPU可以访问所有的内存地址和硬件设备。这个模式用于操作系统内核和一些关键的系统级服务。 用户态User Mode CPU只能执行非特权指令受限于访问内存和硬件设备。用户态用于运行应用程序以保护系统不受用户程序错误或恶意行为的影响。 操作系统软件层面 内核态 操作系统内核运行在CPU的内核态下以便能够执行特权指令和访问所有资源。内核态下运行的代码包括设备驱动程序、文件系统、网络协议栈等。 用户态 应用程序运行在CPU的用户态下。操作系统通过提供系统调用接口允许用户态程序请求内核态服务如文件读写、进程管理、网络通信等。 为什么需要内核态和用户态的区分 可以从以下几个方面来看 安全性CPU内核态和用户态的区分可以防止用户程序直接访问和控制系统核心资源从而保护系统的稳定性和安全性。 稳定性CPU内核态运行的代码如操作系统的内核代码具有更高的权限可以执行特权指令管理系统资源确保系统的稳定运行。 资源管理CPU内核态负责资源的分配和管理通过系统调用提供受控的接口给用户态程序访问。 内核态和用户态的切换 在程序运行过程中当用户态程序需要执行特权操作时(比如需要使用Mutex Lock时)必须通过系统调用切换到CPU的内核态。操作系统内核在处理完CPU内核态的系统调用后CPU会返回用户态继续执行用户程序。这种切换过程涉及到保存和恢复 CPU 的上下文信息包括寄存器、程序计数器、堆栈指针等。 切换过程 用户态到内核态当用户态程序发起系统调用时CPU 切换到内核态操作系统内核处理请求。 内核态到用户态操作系统内核完成请求后CPU恢复用户态的上下文返回用户态继续执行。 一个线程下的CPU内核态和用户态的切换 用户态线程执行 -- 系统调用/中断 -- 保存用户态上下文 -- 切换到内核态 -- 执行内核代码 -- 恢复用户态上下文 -- 返回用户态线程执行 线程上下文切换过程示例(注意和上面区分) 线程 A 执行 -- 保存线程 A 上下文 -- 选择线程 B -- 恢复线程 B 上下文 -- 线程 B 执行 JDK6之前synchronized为什么性能不好 经过上面的铺垫再总结下这个JDK6之前synchronized为什么性能不好的问题就比较自然了。 重量级锁Heavyweight Locking 实现方式在JDK6之前synchronized使用的是基于操作系统的Mutex Lock互斥锁进行实现。每次线程进入和退出同步块时都需要通过调用操作系统的内核函数来获取和释放锁。 内核态和用户态切换获取和释放锁的过程涉及到CPU从用户态切换到内核态再从内核态切换回用户态。这种切换需要保存和恢复CPU的上下文信息开销大。 系统调用开销每次锁的获取和释放都会进行系统调用系统调用本身也会带来较大的性能开销。 线程阻塞和唤醒 线程阻塞当一个线程试图获取一个已经被其他线程持有的锁时该线程会被阻塞。阻塞操作会导致线程的上下文切换开销大。 线程唤醒当锁被释放时需要唤醒一个被阻塞的线程。唤醒操作同样涉及线程的上下文切换并且需要操作系统内核进行调度开销也很大。 真正的主角登场(JDK1.6对锁的升级) 对锁的优化方式介绍 为了解决JDK中锁机制的性能问题。 JDK1.6对锁进行了一系列的升级操作。 主要有下面这些优化方式 锁粗化自旋锁自适应自旋锁锁消除偏向锁轻量级锁 下面分别介绍 ①、锁粗化 通常情况下建议将同步块的范围尽量缩小也就是锁粒度尽量缩小以减少锁的竞争。 然而如果在一个短时间内对象被频繁地加锁和解锁反而会增加性能开销。 锁粗化通过扩展同步块的范围将多个连续的加锁和解锁操作合并为一个从而减少锁操作的频率提升性能。 锁粗化是JVM在即时编译JIT期间进行的一种优化它会自动将多个连续的加锁和解锁操作合并为一个更大的同步块。 示例 // 优化前 for (int i 0; i 100; i) {synchronized (lock) {// 操作} }// 优化后 synchronized (lock) {for (int i 0; i 100; i) {// 操作} }②、自旋锁 自旋锁是指线程在短时间内尝试获取锁时不会立即进行阻塞而是在循环中不断尝试获取锁。 这样可以避免线程频繁地进入和退出阻塞状态(涉及线程上下文切换)从而减少线程调度带来的开销。 但如果自旋时间过长会让CPU空转会占用CPU资源反而降低性能。 示例 // 简单自旋锁示例 while (!compareAndSet(lock, null, Thread.currentThread())) {// 自旋 }③、自适应自旋锁 自适应自旋锁是自旋锁的改进版本。自适应自旋锁的自旋次数不是固定的而是根据前一次自旋锁的获取情况以及锁的拥有者的状态来动态调整。自适应自旋锁能够更加智能地选择自旋时间进一步提升性能。 示例 // 自适应自旋锁示例伪代码 int spins calculateAdaptiveSpins(); while (!compareAndSet(lock, null, Thread.currentThread()) spins 0) {spins--; } ④、锁消除 锁消除是指JVM在即时编译JIT期间通过对代码的逃逸分析判断某些同步块所使用的锁对象不会逃逸出线程从而消除这些不必要的锁操作。这样可以减少不必要的同步开销提高程序执行效率。 示例 public void example() {Object lock new Object();synchronized (lock) {// 操作} }// 优化后 public void example() {// JVM会消除不必要的同步操作// 操作 } ⑤、偏向锁 偏向锁是指当一个线程获得锁后锁进入偏向模式此时锁会偏向于该线程如果该线程再次请求同一个锁便不再进行加锁操作直接进入同步块从而减少锁操作的开销。当有其他线程请求该锁时偏向锁才会被撤销。 ⑥、轻量级锁 轻量级锁是指当锁处于无竞争或低竞争状态时线程会通过CAS操作Compare-And-Swap将对象头的Mark Word替换为指向线程栈中锁记录的指针。轻量级锁可以避免重量级锁的开销提高性能。如果出现一定条件的竞争轻量级锁会膨胀为重量级锁。 ⑦、重量级锁 重量级锁是通过操作系统的互斥量Mutex来实现的当线程竞争激烈或轻量级锁膨胀时会使用重量级锁。重量级锁会让线程进入阻塞状态这样可以避免CPU空转但会带来较高的线程上下文切换开销。 简单总结下 优化方式优点缺点适用场景锁粗化减少锁操作的频率提升性能不适合所有场景可能会引入不必要的锁竞争短时间内频繁加锁和解锁的场景自旋锁避免线程阻塞减少线程调度开销长时间自旋会占用CPU资源降低系统性能预期锁竞争时间很短的场景自适应自旋锁动态调整自旋次数更智能地选择自旋时间需要额外的逻辑判断自旋时间较长仍会占用CPU资源锁竞争情况变化较大的场景锁消除消除不必要的同步块减少同步开销依赖于JVM的逃逸分析不适用于所有情况锁对象不会逃逸出线程的场景偏向锁减少同一线程多次请求锁的开销多线程竞争时撤销偏向锁需要额外开销锁大部分时间被同一线程占用的场景轻量级锁避免重量级锁的开销提升性能有锁竞争时会膨胀为重量级锁带来额外开销低竞争环境中的锁操作重量级锁确保线程安全通过操作系统的Mutex Lock互斥锁实现线程阻塞和上下文切换开销大高竞争环境中需要严格线程同步的场景 锁的升级过程 锁的升级过程涉及了Java中 synchronized 关键字在不同竞争条件下的优化和状态变化。这些状态包括无锁、偏向锁、轻量级锁和重量级锁根据线程竞争的程度和情况JVM会自动将锁从一种状态升级到另一种状态以降低获取锁的成本提高并发性能。 下表是64位JVM下的 Mark Word 各个位储存的数据变化情况(上面已经提到过了) 注意 偏向锁还有一个匿名偏向的状态下面会说到。 匿名偏向状态即锁对象为偏向锁但是没有线程偏向于这个锁对象。对应的 Mark Word 标志位后三位是001前61位都是0。 查看对象的内存结构 一开始查锁升级相关资料的时候感觉有点绕还是觉得自己去看下对象头比较靠谱。于是就想JDK有没有提供类似反射的机制获取对象头信息的功能。查了查资料好像没有。 只能通过第三方的jar(JOL)提供的功能。 dependencygroupIdorg.openjdk.jol/groupIdartifactIdjol-core/artifactIdversion0.17/version /dependencyjol-core 是一个 Java 库用于分析对象布局Layout和对象内存结构。它的全称是 Java Object Layout通常简称为 JOL。 主要用处 对象布局分析锁状态分析。通过分析对象的结构来进行性能优化。 ①、无锁 需要先补充知识点 偏向锁是JDK1.6引入的与此同时JVM也引入了和偏向锁相关的JVM启动参数。 查看默认的JVM启动参数命令java -XX:PrintFlagsFinal -version 有非常多默认的JVM启动参数命令下面列出来两个和偏向锁相关的配置参数。 # 这个参数用于启用或禁用偏向锁 # 默认值是 true表示偏向锁是启用的。 UseBiasedLocking true # 这个参数指定在JVM启动后多长时间以毫秒为单位启用偏向锁。 # 默认值是 4000表示偏向锁在JVM启动后4秒钟才会启用。 BiasedLockingStartupDelay 4000示例代码 import org.openjdk.jol.info.ClassLayout;public class TestA {public static void main(String[] args) {TestA testA new TestA(); // 无锁System.out.println( 无锁 );System.out.println(ClassLayout.parseInstance(testA).toPrintable());} } 运行结果(只取Mark Word 部分) OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)non-biasable表示无锁0x0000000000000001 转成64位二进制后 后三位 001 也对应无锁 分析 JVM默认启用偏向锁但是JVM启动后4秒钟才会启用TestA testA new TestA();是在JVM启用偏向锁之前就创建了此时testA 对象处于无锁状态。 ②、偏向锁的匿名偏向状态 JVM的参数UseBiasedLocking 是个启动开关没啥好说的。 JVM的参数BiasedLockingStartupDelay 为什么要延迟启用偏向锁呢 延迟启用偏向锁的主要目的是希望在应用程序启动阶段避免由于类加载和初始同步造成的短期内大量偏向锁撤销开销。 JVM启动时会延时初始化偏向锁默认是4秒(可以通过 JVM参数BiasedLockingStartupDelay 修改)。初始化后会将所有加载的Klass的prototype header修改为匿名偏向样式。Klass 是 JVM 用来表示 Java 类的一种结构。每个 Klass 对象都包含一个 prototype header这是一个模板用于初始化新创建的对象的对象头Object Header。当创建一个新对象时它的对象头会从这个模板中复制初始值。 所以当创建一个对象时会通过Klass的prototype_header来初始化该对象的对象头。 在JVM偏向锁初始化结束后后续创建的所有对象都为匿名偏向状态因为JVM偏向锁初始化后会将所有加载的Klass的prototype header修改为匿名偏向样式在此之前创建的对象则为无锁状态。而对于无锁状态的锁对象如果有竞争会直接进入到轻量级锁可以避免偏向锁撤销的开销从而提高JVM的启动速度。 比如可以看下图JVM启动时会加载很多类到内存类加载的时候就用了synchronized 代码块此时如果是默认的JVM启动参数那么4秒内创建的对象刚创建出来是无锁状态一但遇到竞争就会跳过偏向锁直接进入轻量级锁状态。这也是JVM优化锁性能的一种方式。 示例代码 import org.openjdk.jol.info.ClassLayout;import java.util.concurrent.TimeUnit;public class TestA {public static void main(String[] args) {try {// BiasedLockingStartupDelay 4000// 这个参数指定在JVM启动后多长时间以毫秒为单位启用偏向锁。// 默认值是 4000表示偏向锁在JVM启动后4秒钟才会启用。// 这里等待5秒TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}// 5秒后创建对象TestA testA new TestA();System.out.println( 偏向锁(匿名偏向状态) );System.out.println(ClassLayout.parseInstance(testA).toPrintable());} } 运行结果(只取Mark Word 部分) OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)biasable表示可以偏向0x0000000000000005 转成64位二进制后 后三位 101 也对应偏向锁前61位都是0表示匿名偏向状态。 ③、 匿名偏向状态→偏向锁 匿名偏向状态的对象在满足以下条件时可以转变为偏向锁状态 单线程首次访问 当一个线程第一次访问该对象的同步代码块时JVM将该对象的锁标志设置为偏向锁并将Mark Word中的线程ID字段更新为当前线程的ID。 无竞争 如果在一个线程持有偏向锁期间没有其他线程尝试获取该对象的锁偏向锁将保持不变。此时线程可以在不进行同步操作的情况下快速进入和退出同步块。 代码示例 import org.openjdk.jol.info.ClassLayout;import java.util.concurrent.TimeUnit;public class TestA {public static void main(String[] args) {try {// BiasedLockingStartupDelay 4000// 这个参数指定在JVM启动后多长时间以毫秒为单位启用偏向锁。// 默认值是 4000表示偏向锁在JVM启动后4秒钟才会启用。// 这里等待5秒TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}// 5秒后创建对象TestA testA new TestA();System.out.println( 偏向锁(匿名偏向状态) );System.out.println(ClassLayout.parseInstance(testA).toPrintable());// 遇到同步代码块synchronized (testA){System.out.println( 匿名偏向状态 → 偏向锁 );System.out.println(ClassLayout.parseInstance(testA).toPrintable());}} } 运行结果 偏向锁(匿名偏向状态) OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)匿名偏向状态 → 偏向锁 OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000002e92805 (biased: 0x000000000000ba4a; epoch: 0; age: 0)biased表示偏向锁状态0x0000000002e92805 转成64位二进制后 后三位 101 也对应偏向锁前61位有非0数据表示锁偏向的线程id等信息。 ③、无锁或偏向锁→轻量级锁 升级为轻量级锁 升级条件 偏向锁撤销 如果一个对象已经被偏向某个线程但是另一个线程尝试获取该对象的锁偏向锁会被撤销从而升级为轻量级锁。 轻量级锁的优势 轻量级锁通过CAS操作尝试获取锁避免了线程阻塞。虽然CAS操作比偏向锁稍有开销但在高竞争环境下其效率要高于频繁撤销和重新设置偏向锁的开销。 轻量级锁转换和获取过程概述 线程在执行同步块之前JVM会先在当前线程的栈桢中创建用于存储锁记录的空间并将对象头中的Mark Word复制到锁记录中官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功当前线程获得锁如果失败表示其他线程竞争锁当前线程便尝试使用自旋来获取锁。 代码示例 import org.openjdk.jol.info.ClassLayout;import java.util.concurrent.TimeUnit;public class TestA {public static void main(String[] args) {// 无锁对象TestA testA new TestA();System.out.println( 无锁状态 );System.out.println(ClassLayout.parseInstance(testA).toPrintable());synchronized (testA) {System.out.println( 无锁状态 → 轻量级锁 );System.out.println(ClassLayout.parseInstance(testA).toPrintable());}try {// BiasedLockingStartupDelay 4000// 这个参数指定在JVM启动后多长时间以毫秒为单位启用偏向锁。// 默认值是 4000表示偏向锁在JVM启动后4秒钟才会启用。// 这里等待5秒TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}// 匿名偏向锁对象TestA testB new TestA();System.out.println( 匿名偏向锁状态 );System.out.println(ClassLayout.parseInstance(testB).toPrintable());synchronized (testB) {System.out.println( 匿名偏向锁状态 → 偏向锁 且偏向主线程);System.out.println(ClassLayout.parseInstance(testB).toPrintable());}// 此时另一个线程t1尝试获取 testB 对象的锁 会导致testB的偏向锁撤销 从而升级为轻量级锁new Thread(()-{synchronized (testB) {System.out.println( 偏向锁 → 轻量级锁 );System.out.println(ClassLayout.parseInstance(testB).toPrintable());}},t1).start();} }运行结果 无锁状态 OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)无锁状态 → 轻量级锁 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x00000000033ff2d0 (thin lock: 0x00000000033ff2d0)匿名偏向锁状态 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) 匿名偏向锁状态 → 偏向锁 且偏向主线程 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x00000000035a2805 (biased: 0x000000000000d68a; epoch: 0; age: 0) 偏向锁 → 轻量级锁 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x00000000206af270 (thin lock: 0x00000000206af270) 分析 无锁状态 → 轻量级锁 这个是因为JVM偏向锁功能还没启动完成。所以线程遇到同步代码块会直接由 无锁状态 → 轻量级锁。 偏向锁 → 轻量级锁 因为testB对象已经被偏向主线程但是另一个线程t1尝试获取该对象的锁偏向锁会被撤销从而升级为轻量级锁。 ④、轻量级锁→重量级锁 轻量级锁升级为重量级锁的条件 自旋失败 当一个线程尝试获取一个已经被其他线程持有的轻量级锁时JVM会让这个线程自旋一段时间尝试获取锁。如果自旋次数超过一定的阈值默认是10次对应JVM参数PreInflateSpin自旋会失败导致 轻量级锁→重量级锁。 竞争激烈 如果多个线程频繁地争抢同一个锁并且这些线程自旋失败的情况持续发生那么 JVM 会将该锁升级为重量级锁以避免自旋浪费大量 CPU 资源。 线程数目多 当等待获取锁的线程数目达到一定程度通常是 2 个以上轻量级锁会被升级为重量级锁。重量级锁依赖操作系统的互斥机制会导致线程阻塞和上下文切换。 JVM参数 ## 轻量级锁升级为重量级锁之前自旋等待的次数 ## 这个参数决定了线程在放弃轻量级锁并升级为重量级锁之前自旋尝试获取锁的最大次数 PreInflateSpin 10## 线程在进入阻塞状态之前自旋等待的次数 ## 这个参数决定了线程在放弃轻量级锁并进入阻塞状态之前自旋尝试获取锁的最大次数 PreBlockSpin 10在windows版本的java version 1.8.0_202 这个版本中 我并没有找到PreBlockSpin 这个参数也许这个版本已经移除了PreBlockSpin这个参数。猜测 当 JVM 决定将轻量级锁膨胀为重量级锁时会创建一个重量级锁monitor 对象。轻量级锁的持有线程将对象头的 Mark Word 中的指针更新为指向 monitor 对象。Monitor 对象包含了锁的状态、持有锁的线程、等待队列等信息。 竞争锁失败的线程将进入 monitor 对象的等待队列操作系统会阻塞这些线程。当持有锁的线程释放锁时它会通知操作系统唤醒等待队列中的一个或多个线程尝试重新获取锁。重量级锁的获取和释放涉及操作系统的系统调用会导致CPU进行状态切换用户态→内核态→用户态、还有可能导致线程阻塞和唤醒(线程上下文切换)因此性能较低(这个在上面的知识铺垫里有详细说到)。 代码示例 import org.openjdk.jol.info.ClassLayout;public class TestA {public static void main(String[] args) {// 无锁对象TestA testA new TestA();System.out.println( 无锁状态 );System.out.println(ClassLayout.parseInstance(testA).toPrintable());synchronized (testA) {System.out.println( 无锁状态 → 轻量级锁 );System.out.println(ClassLayout.parseInstance(testA).toPrintable());}new Thread(() - {synchronized (testA) {System.out.println( 轻量级锁 → 重量级锁 t1竞争);System.out.println(ClassLayout.parseInstance(testA).toPrintable());}}, t1).start();new Thread(() - {synchronized (testA) {System.out.println( 轻量级锁 → 重量级锁 t2竞争);System.out.println(ClassLayout.parseInstance(testA).toPrintable());}}, t2).start();} }运行结果 无锁状态 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 无锁状态 → 轻量级锁 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x00000000032bf1a8 (thin lock: 0x00000000032bf1a8) 轻量级锁 → 重量级锁 t1竞争 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x00000000205ef070 (thin lock: 0x00000000205ef070) 轻量级锁 → 重量级锁 t2竞争 TestA object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x000000001ceef5da (fat lock: 0x000000001ceef5da)可以看到t2竞争的时候锁升级到了重量级。 有时候执行上面代码t1和t2打印的都有可能是重量级。 锁升级图示 这个就不自己整理了(又偷懒了~) 图片来源https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/ 图片来源的这篇博客里对于锁的升级过程偏向JVM实现层面。如果想深入了解的话建议去仔细看看这篇博客。 注意 偏向锁在JDK 15中被标记为弃用deprecated并计划在未来版本中删除。 在JDK 17中偏向锁已经被移除。 JDK为什么要移除偏向锁 大致有下面几个原因 复杂性: 偏向锁增加了HotSpot JVM代码的复杂性。收益减少: 在现代硬件和应用程序中偏向锁带来的性能提升变得不明显。维护成本: 随着JVM的演进维护偏向锁功能变得越来越困难。 就是吃力不讨好了干脆就不要了。 7、总结synchronized保证原子、可见、有序性原理 ①、Java代码层面 在 Java 中使用 synchronized 关键字可以确保同一时刻只有一个线程能执行被 synchronized 修饰的代码块或方法从而保证临界区代码的原子性和线程安全。同时JMM的监视器锁规则对同一个监视器的解锁happens-before于对该监视器的加锁。也保证了可见性。 ②、JVM 层面 锁的实现 synchronized 关键字在编译后的字节码中会转换成 monitorenter 和 monitorexit 指令。monitorenter 在同步代码块的开始位置monitorexit 在同步代码块的结束位置和异常处。 Monitor 和对象头 每个对象在 JVM 中都有一个对象头其中包含Mark Word区域Mark Word包含锁标志位和其他信息。monitorenter 和 monitorexit 指令会检查并操作对象头中Mark Word的锁信息。如果锁竞争激烈JVM 会进行锁升级并在对象头中设置指向 monitor 对象的指针。Monitor 对象包含锁的状态、持有锁的线程以及等待队列等信息。 注意点 Monitor 监视器并不是在对象创建时就存在的而是在锁竞争激烈的情况下如轻量级锁失败并升级为重量级锁时才会创建。初始状态下对象头的 Mark Word 仅包含基本的锁信息而不包含具体的 Monitor 对象。只有在需要重量级锁时才会分配并使用 Monitor 对象来管理锁的状态和线程的阻塞与唤醒。 锁的类型 偏向锁无竞争时线程偏向于自己持有的锁。 轻量级锁有少量竞争时采用 CAS 操作避免线程阻塞。 重量级锁高竞争时使用操作系统的互斥锁机制涉及线程阻塞和唤醒性能较低。 ③、操作系统层面 互斥锁Mutex Lock 重量级锁依赖于操作系统的互斥锁机制。当轻量级锁竞争失败时JVM 会将其升级为重量级锁涉及系统调用(可能会有线程的上下文切换和CPU内核态状态转换)。 ④、处理器层面 缓存一致性协议和lock前缀指令 多处理器系统中为了保证可见性和有序性处理器采用缓存一致性协议如 MESI 协议来维护各个处理器缓存的同步。 JIT编译器使用lock前缀指令完成处理器级别的原子操作禁止重排序操作以此保证共享变量的原子性和有序性并通过缓存一致性协议保证共享变量的可见性。 补充为什么有synchronized了JDK还要设计Lock 除了 synchronized 关键字之外JDK 还引入了 java.util.concurrent.locks 包中的显式锁如 ReentrantLock。两者虽然都用于线程同步但各有优缺点和适用场景。 原因其实很简单有些业务场景synchronized不支持。 比如 尝试获取锁2秒获取不到就放弃。 让等待的线程响应中断。 设置锁的公平性。 获取不到锁立马返回false(非阻塞式)。 有多个等待或者唤醒的条件。 无法自定义synchronized的行为。 上面说的synchronized不支持的 Lock 都支持但是synchronized并非一无是处synchronized不用显式释放锁api使用简单不需要创建特殊的锁对象。 所以实际业务中进行简单的同步使用synchronized是比较推荐的。 Lock 的详解就放到后面吧。 写在最后 这篇文章写着写着又是一周过去了。。。发现并发这块内容真的不整理还好一整理就真的~ 一周能整理出一篇就不错了自闭中~ 希望这篇文章对你理解synchronized有所帮助。 我写的这篇内容也只是东拼西凑罢了稍微加了点自己的理解补充了一些知识点让知识点更自然的衔接起来。 因为写这些文章的目的就是构建自己Java知识体系所以排版就顺着自己的思维走会显得很啰嗦。 最后感谢下面这些资料 https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/ https://www.cnblogs.com/star95/p/17542850.html https://javaguide.cn https://pdai.tech https://ifeve.com/ 《Java并发编程的艺术》 《Java并发实现原理JDK源码剖析》 《Java并发编程之美》 《图解Java多线程设计模式》
http://www.w-s-a.com/news/30302/

相关文章:

  • 黄浦专业做网站海南网站策划
  • 网站开发工程师有证书考试吗织梦cms是免费的吗
  • 电子商务网站建设需要学什么门户网站推广介绍方案
  • 网站里的专题页面wordpress查询数据库结构
  • WordPress子站站群网站建设代码生成器
  • 怎么攻击织梦网站甘肃省最新消息今天
  • 赣州哪里可以做网站看装修案例的网站
  • 旅游网站专业化建设的要点php 手机网站 模板
  • wordpress百度站长主动推送长春火车站官网
  • 比较好的响应式网站wordpress博客增加音乐页面
  • 广告公司出售家具 税率江门做网站seo的
  • 网站设计建议建设商务网站作用
  • 网站策划的最终体现是什么模板网站建设流程图
  • 网站设计与开发技术教程十度公司做网站怎么样
  • 企业网站推广方案在哪里智慧团建登录入口官网手机版
  • google网页版入口seo索引擎优化
  • 东乡做网站常州网络公司联系方式
  • 做网站激励语家居装饰网站设计论文
  • 镜像的网站怎么做排名无极网站建设质量
  • 奉贤集团公司网站建设小工具文本wordpress
  • 不用代码做网站网站建设和运行费用
  • 阜阳网站开发招聘网站建设合作协议申请
  • 电子配件 技术支持 东莞网站建设wordpress 生成html代码
  • 网站用免费空间好不好网站建设的视频
  • 网站开发项目职责门户资源分享网站模板
  • 建网站需要什么语言如何做二维码跳转到网站
  • 天津建设交培训中心网站做网站起名字
  • 黑河北京网站建设湛江市住房和城乡建设局网站
  • 网站建设拾金手指下拉十九企业查询官网
  • 邢台提供网站建设公司哪家好五合一建站