JUC 十. synchronized深入

2023-11-16

一. 基础复习

  1. synchronized,与 ReentrantLock 都属于悲观锁
  2. 乐观锁,例如Version,Atomic包下的原子类AtomicInteger 等,基于cas实现乐观锁
  3. synchronized 根据编写方式不同分为: 同步代码块,同步方法
  4. synchronized 可以修饰方法,修饰代码块,修饰内部类,修饰对象等
  1. 修饰非静态成员: 例如修饰this代码块,非静态方法等使用的是this锁,调用进入同步区域时就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放,java的所有对象都含有一个互斥锁,这个锁由jvm自动获取和释放.synchronized方法正常返回或者抛异常而终止,jvm会自动释放对象锁。这里也体现了用synchronized来加锁的一个好处,即 :方法抛异常的时候,锁仍然可以由jvm来自动释放
  2. 修饰静态成员: 例如修饰class,静态代码块,静态方法等不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被声明为synchronized。此类所有的实例对象在调用此方法,共用同一把锁,我们称之为类锁, 对象锁是用来控制实例方法之间的同步,而类锁是用来控制静态方法(或者静态变量互斥体)之间的同步的。类锁只是一个概念上的东西,并不是真实存在的,他只是用来帮助我们理解锁定实例方法和静态方法的区别的。java类可能会有很多对象,但是只有一个Class(字节码)对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,只不过是Class对象的锁而已。获取类的Class对象的方法有好几种,最简单的是[类名.class]的方式

二. 根据synchronized修饰不同成员了解synchronized实现原理

  1. 了解synchronized底层需要对使用锁的类进行反编译,反编译命令
  1. javap -c 需要反编译的类名.class
  2. 如果需要查看更多信息使用" javap -v XXX.class" 会输出包括行号,本地变量表等详细信息

同步代码块

  1. 示例代码,该示例中在method1方法中,使用synchronized修饰对象
public class SynchronizedTest {

    private Object objLock = new Object();

    public void method1(){
        synchronized (objLock) {
            System.out.println("执行");
        }
    }

    public static void main(String[] args) {
    }
}
  1. 执行"javap -c SynchronizedTest.class" 命令对上述代码生成的类进行反编译: 其中"monitorenter"表示加锁, “monitorexit” 表示释放锁,分析下图会发现,在执行执行method1()方法是会加锁,方法执行完毕后会释放锁,但是一个monitorenter加锁会出现两个monitorexit释放锁
    在这里插入图片描述
  2. 示例代码二: method1()方法中手动抛出了一个异常,再次反编译查看
    在这里插入图片描述

小总结

  1. synchronized代码块时: 底层在对象头的"mark_word"中设置"monitorenter"指令表示加锁,"monitorexit"指令表示释放锁
  2. monitorenter与monitorexit 是成对出现的吗: 通常情况下一个monitorenter加锁命令会对应两个monitorexit释放锁指令,原因是防止代码发生异常时锁无法释放问题(例如上方示例中演示)
  3. monitorenter与monitorexit 不是成对出现的那么monitorexit 一定会是两个吗?: 不一定,假设示例代码的method1()方法中我们手动抛出异常,再去对该class进行反编译,就会发现只出现了一个monitorenter对应一个monitorexit,并且会伴随两个athrow

同步方法

  1. 示例代码,method()普通方法被 synchronized 修饰
public class SynchronizedTest {
    private Object objLock = new Object();

    public synchronized void method() {
        System.out.println("执行");
    }

	public static synchronized void method2() {
        System.out.println("静态同步方法");
    }

    public static void main(String[] args) {
    }
}
  1. 使用"javap -v XXX.class" 对该类进行反编译,查看mothod()普通同步方法发现普通同步方法时没有"monitorenter"与"monitorexit"锁相关的指令,而是通过在方法上flags加了一个"ACC_SYNCHRONIZED"标识
    在这里插入图片描述
  2. 使用"javap -v XXX.class" 对该类进行反编译,查看mothod2()静态同步方法发现与普通同步方法比只多了一个"ACC_STATIC"标识
    在这里插入图片描述

小总结

使用synchronized修饰方法实现锁原理: 与同步代码块不同,在执行调用指令时通过判断调用的方法是否被"ACC_SYNCHRONIZED"标识,如果有,执行线程会先持有monitor,然后再执行方法,方法执行完成释放monitor(无论方法正常或是异常都会释放),并且同步静态方法也是该方式与非同步方法比只是多了一个"ACC_STATIC"静态标识

synchronized 与 管程

  1. 管程: 进程与线程之间共享资源,但是要防止冲突,例如数据安全问题,管程就相当于这个协调者,Monitor(监视器),也可以说是我们平时说的锁,Monitor是一种同步机制,保证同一时间内只有一个线程访问某个数据和代码,举例: JVM中基于进入和退出监视器对象来实现的synchronized, java 虚拟机支持方法级同步,也就是上面的同步方法,与内部一段指令序列的同步也就是上面的同步代码块
  2. 根据上面同步代码块与同步方法反编译查看了解到:
  1. 同步一段指令序列(同步代码块): 在对象头的"mark_word"中设置"monitorenter"指令表示加锁,"monitorexit"指令表示释放锁,正常情况下一个monitorenter对应两个monitorexit,防止执行的代码块中发生异常锁无法释放,当代码块中手动抛出异常时,一个monitorexit对应一个monitorexit, 将异常释放放到了在异常抛到同步方法的边界之外时自动释放
  2. 方法级同步(同步方法): 是隐式同步,无序通过字节指令来控制,虚拟机通过判断方法常量池中该方法的表结构中是否有"ACC_SYNCHRONIZED"标志来判断这个方法是否是同步方法,如果是执行线程会先成功持有管程,然后才去执行方法,在执行期间其它线程无法获取同一个管程,如果该方法执行中抛出异常,那这个方法持有的管程将在异常抛到同步方法的边界之外时自动释放

底层分析

  1. java中所有类都继承自Object,在HotSpot虚拟机中,monitor管程是由ObjectMonitor实现继续往下扒(.cpp, .hpp是c中的)
    在这里插入图片描述
  2. 了解 ObjectMonitor结构
//结构体如下
ObjectMonitor::ObjectMonitor() {  
  _header       = NULL;  
  _count       = 0;  
  _waiters      = 0,  
  _recursions   = 0;       //线程的重入次数
  _object       = NULL;  
  _owner        = NULL;    //标识拥有该monitor的线程
  _WaitSet      = NULL;    //等待线程组成的双向循环链表,_WaitSet是第一个节点
  _WaitSetLock  = 0 ;  
  _Responsible  = NULL ;  
  _succ         = NULL ;  
  _cxq          = NULL ;    //多线程竞争锁进入时的单向链表
  FreeNext      = NULL ;  
  _EntryList    = NULL ;    //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
  _SpinFreq     = 0 ;  
  _SpinClock    = 0 ;  
  OwnerIsThread = 0 ;  
}  
  1. 其中几个关键属性
    在这里插入图片描述
  2. 了解了java中所有类都继承自Object,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象,这个ObjectMonitor就与ObjectMonitor结构,就解释了为什么每个对象就是一个监视器,就是一把锁
    在这里插入图片描述

三. 锁升级相关

  1. 先查看上面对象头相关笔记,了解一下对象头,synchronized利用对象头中的MarkWord,根据锁标志位偏向锁标志位的不同实现锁升级策略
    在这里插入图片描述
  2. 先提出synchronized锁升级过程是: 无锁 —> 偏向锁 —> 轻量级锁 —> 重量级锁
  3. 那聊一下为什么会出现锁升级: java5以前如果使用synchronized上来就是重量级锁,虽然保证了数据安全,但是会造成性能下降,假设对一个数据加锁,而实际访问这个数据时不是不出现是很少会出现多线程并发访问的场景,会造一种情况,大部分的时间都是单个线程去访问一个被加了重量级锁的数据,进而出现了锁升级
  4. 说一下重量级锁为什么会影响性能: 查看java中启动一个线程start源码会发现底层调用的是,native方法start0,如果继续向下通过OpenJDK去查看的话会发现调用了Thread.c中的JVM_StartThread,再向下JVM_StartThread中实际调用的是操作系统os的开启一个线程命令"start_thread()“,总结就是说:java早期版本synchronized加锁时监视器monitor依赖与底层操作系统的MutexLock实现,挂起线程与恢复线程都需要转入内核态完成,而java的线程最终会映射到操作系统原生线程上,如果要阻塞或者唤醒一个线程操作系统要介入,需要用户态到内核态之间的切换(也就是ObjectMonitor中的”_owner"属性 持有的线程切换),比较耗费时间性能
    在这里插入图片描述
  5. java6 为了减少获取锁跟释放锁带来的性能消耗,引入了轻量级锁和偏向锁

复习一下对象头

  1. 对象在内存中存储的布局分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
  2. 对象头中又分为mark_word 与 klass_word两部分,其中mark_word中存储了对象在runtime时用的信息: 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳、转发指针等等
    在这里插入图片描述
  3. 项目中引入jol-core依赖,通过对象头查看锁详情
		<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
  1. 举例查看创建的Object对象o的对象头信息
import org.openjdk.jol.info.ClassLayout;

public class MyObject {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}
  1. 有两个注意点:
  1. 锁是通过对象头中的"mark_word"实现,在查看对象头信息时值关注前两行"mark_word"部分即可
  2. 通过"ClassLayout.parseInstance(“对象”).toPrintable()"查看输出对象头信息VALUE下括号中的数子编码时,输出的数字编码是二进制的,要从后向前看
  3. VALUE 下括号中的是二进制数据,括号前面是对应括号中的十六进制数据
  4. 在前面我们学习对象头时,已经了解到创建对象后每个对象都有它对应的hashCode,为什么此处没有,因为hashCode是调用获取时才会生成,否则不会生成
    在这里插入图片描述

无锁演示

  1. 创建一个Object对象不加锁查看对象头
import org.openjdk.jol.info.ClassLayout;
public class MyObject {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}
  • 输出入下图
    在这里插入图片描述在这里插入图片描述

偏向锁

  1. 一段加锁的同步代码一直被同一个线程多次访问,由于只有这一个线程访问,在后续再访问时便会自动获得锁
  2. 在我们测试运行多个线程同时并发访问同一个加锁的数据时,实际会发现会有某几个或一个拿到锁的频率比较高,而另外几个线程获取到锁执行的频率普遍较低,也就是说实际测试下来锁总是被第一个占用它的线程获取到,这个线程就是偏向线程
  3. 先总结偏向锁的优点: 偏向锁的操作不涉及用户态到内核态的切换,同一个线程时不需要多次释放与获取锁,不存在CAS修改MarkWord线程指针
  4. 偏向锁是怎么实现的: 一个synchronized方法被线程获取到锁时,锁对象会在对象头MarkWord中标记为偏向锁状态,并且会用54位存储线程指针作为偏向标识,若该线程再次访问该锁时,只需要判断MarkWord中通过这54位存储的线程指针与当前线程是否一致,无需进入Monitor去竞争
    在这里插入图片描述
  5. 在锁被第一次获取时,JVM使用CAS操作把线程指针记录到MarkWord中,这个线程就可以看成偏向线程,并通过CAS操作修改对象头中偏向锁标志位,该偏向线程一直持有着锁,后续该线程进入和退出加锁执行代码时不再需要再次获取和释放锁,而是直接比较对象头中是否存储了指向当前线程的偏向锁,有两种情况
  1. 判断对象头中偏向锁偏向当前线程: 不需要再次尝试获取锁,直接进入同步代码,并且不需要加锁释放锁时通过CAS更新对象头,几乎没有性能消耗
  2. 判断对象头中不是当前线程ID: 说明发送了竞争,然后进行重偏向,或升级变为轻量级锁操作
  1. 注意点: 偏向锁只有在其它线程竞争获取偏向锁时,持有偏向锁的线程才会释放锁,偏向锁时线程是不会主动释放偏向锁的

演示

  1. 执行查看服务器偏向锁相关数据命令: “java -XX: +PrintFlagsIntitial |grep BiasedLock*” 会发现默认是开启偏向锁"UseBiasedLocking = true" 但是有4000毫秒的延迟,所以要修改
    在这里插入图片描述
  2. 执行命令修改延时4000毫秒为0 (IDEA中 “Run—>Edit Configurations —> VM options 中输入”-XX:BiasedLockingStartupDelay=0")
    在这里插入图片描述
    在这里插入图片描述
  3. 测试代码(创建一个锁对象,开启一个线程,线程中synchronized以创建的对象为锁监视器,打印锁对象对象头信息)
	public static void main(String[] args) {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }, "t5").start();
    }
  1. 输出"101"偏向锁
    在这里插入图片描述

偏向锁撤销

  1. 偏向锁时,即使同步代码执行完毕,线程是不会主动释放锁的,线程在获取锁时会判断MarkWord中存储的线程指针不是当前线程时,通过CAS自旋修改对象头线程指针为当前线程的,如果CAS修改失败说明锁已经被其它线程获取,这时将阻塞,撤销偏向锁,撤销需要等待到全局安全点,同时检查持有偏向锁的线程是否还在执行(两种情况)
  1. 情况一: 第一个线程正在执行synchronized方法(处于同步块)还未执行完毕,此时其它线程进来争夺线程,该偏向锁会被取消,升级为轻量级锁,轻量级锁由原持有偏向锁的线程持有,从安全点继续执行同步代码,后面进来的竞争线程会进入自旋等待升级为轻量级锁的线程执行完毕释放,获取到该锁
  2. 情况二: 第一个线程执行完毕,第二个线程进来判断对象头MarkWord中线程指针不是当前线程的,会将对象头设置成无锁状态并撤销偏向锁,然后进行重偏向
  3. (什么是全局安全点STW=stop the world 虚拟机相关有解释)

偏向锁与hashCode

  1. 前面我们了解对象头时知道对象头的MarkWord中存储了如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳、转发指针等信息,64位操作系统上这些数据占64bit
    在这里插入图片描述
  2. 查看一下无锁状态与偏向锁状态的不同: 在无锁状态时前56bit中有25bit不使用,31bit存储对象的hashCode,如果不获取这个对象的hashCode,那么用来存储hashCode的31bit是空,当获取是这31bit中才会有对应的值,而使用偏向锁时通过前54bit存储当前线程的线程指针,占用了存储hashCode的位置
  3. 问题: 如果一个对象的hashCode() 方法已经被调用过一次之后,这个对象还能被设置偏向锁么?答案是不能。因为如果可以的化,那 Mark Word中 的 identity hash code 必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用 hashCode() 方法得到的结果不一致
  4. HotSpot VM 的锁实现机制是:
  1. 当一个对象已经调用默认 hashCode() 或者 System.identityHashCode(),即计算过 identity hashcode 后,它就无法进入偏向锁状态。这意味着,如果要在不发生争用的对象上进行同步,则最好覆盖默认hashCode()实现,否则JVM不会优化。
  2. 当一个对象当前正处于偏向锁状态,需要计算 identity hashCode 的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
  3. 轻量级锁的实现中,会通过线程栈帧的锁记录存储 Displaced Mark Word;
  4. 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。

轻量级锁

  1. 先举个例子: 假设AB两个线程执行, A线程先获取到锁,后续B线程执行判断MarkWord中的线程指针不是当前B线程的,此时B线程CAS尝试获取锁,有两种情况: B线程CAS成功将MarkWord中的线程指针修改为当前线程说明获取偏向锁成功,并重偏向与当前线程,情况二如果CAS修改MarkWord失败,则偏向锁升级为轻量级锁,并且由原持有偏向锁的线程持有,此时B线程就会进入自旋等待A线程执行完毕释放轻量级锁
  2. 轻量级锁主要适用在没有多线程竞争情况下,通过CAS自旋再阻塞减少重量级锁的使用带来的性能消耗,
    在这里插入图片描述
  3. 当获取轻量级锁失败时,会自旋等待,当自旋次数与等待线程数达到限制时,锁升级为重量级锁
  4. 轻量级锁每次退出同步块时都需要释放锁,

轻量级锁等待线程数与自旋次数问题

  1. 轻量级锁问题点: 上面说到过假设两个线程竞争执行同步代码块,A线程还未执行完毕,此时B线程会在外面CAS等待,假如说A线程始终不释放锁,这时候B线程一直在外面CAS会销毁大量CPU资源,假如A线程不释放接着进来了B,C,D等多个线程呢,防止这个问题,轻量级锁设置了自旋的次数,与竞争线程数规定,当超过这个次数轻量级锁将不再自旋,升级为重量级锁
  2. java6前: 自旋线程数,自旋次数限制默认开启:默认自旋次数10次,或自旋线程数超过cpu核的一半会放弃自旋升级为重量级锁
  3. java6后: 底层根据同一个锁上一次自旋的时间,拥有锁线程的状态来决定的
  4. 通过"-XX:PreBlockSpin=10"修改

轻量级锁演示

  1. 执行命令"-XX:-UseBiasedLocking"关闭偏向锁
    在这里插入图片描述
  2. 执行代码,由于关闭偏向锁,加锁时直接到轻量级锁
	public static void main(String[] args) {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }, "t5").start();
    }

在这里插入图片描述

JIT锁消除与锁粗化

  1. 锁消除, 先查看下方一段代码
	//外部声明了一个锁对象
    private static Object lock = new Object();
    
    //而m1方法内部会new出来新的锁对象
    //此时m1每执行一次都会新建一个锁对象
    //synchronized锁的各自的新的对象,会出现锁不住的情况
    public void m1(){
        Object lock = new Object();
        synchronized (lock) {
            
        }
    }
  1. 锁粗化,同一个线程短时间内多次获取同一把锁时,JIT会优化将多次获取锁合成一次获取
	//声明锁对象
    private static Object lock = new Object();

    public void m1(){
        //在t1线程中多次获取同一把锁lock
        new Thread(()->{
            synchronized (lock) {
                System.out.println("11111");
            }
            synchronized (lock) {
                System.out.println("2222");
            }
            synchronized (lock) {
                System.out.println("3333");
            }

            synchronized (lock) {
                System.out.println("4444");
            }
            synchronized (lock) {
                System.out.println("5555");
            }
        },"t").start();
    }

	//优化后可看为是下方
	public void m2() {
        //在t1线程中多次获取同一把锁lock
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("11111");
                System.out.println("2222");
                System.out.println("3333");
                System.out.println("4444");
                System.out.println("5555");
            }
        }, "t").start();

    }

小总结

  1. JDK1.6后对synchronized进行了优化,升级过程: 无锁—>偏向锁—>轻量级锁—>重量级锁
  2. 内部还伴随的锁的重偏向,批量撤销等
  3. 重量级锁为什么性能低:
  4. 轻量级锁存在的问题,轻量级锁升级为重量级锁的条件
  5. 不同锁直接的优缺点对比(实际就是访问同步代码线程数,是否出现竞争的问题)
    在这里插入图片描述

四. synchronized 可重入分析

  1. synchronized是可重入锁,每个锁对象都有一个锁计数器,跟执行持有该锁线程的指针,当执行"monitorenter"加锁命令时,如果目标锁对象计数器为0,那么说明他没有被其它线程获取,java虚拟机会将该锁对象持有线程设置为当前线程,并对计数器进行加一,在目标锁对象计数器不为0时判断锁对象中线程是否是当前线程,如果是则计数器进行累加操作,否则阻塞等待释放,当执行monitorexit释放锁时,会对计数器进行累减操作,当计数器为0时表示释放锁
  2. 前面了解过ObjectMonitor结构体,也就是ObjectMonitor结构体中的: "_owner "属性表示指向持有当前ObjectMonitor对象的线程, “_recursions” 属性表示锁重入次数

五. 总结

synchronized 底层原理

  1. synchronized实现原理: 在说synchronized实现原理前首相需要了解对象头结构与管程
  1. 对象布局: 对象在堆内存中,存储布局可划分为三个部分:对象头Header, 实例数据Instance Data, 和对齐填充Padding,其中对象头内部是由对象标记MarkWord, 类型指针Class Pointer(又叫类元信息klass pointer) 两部分组成,对象标记MarkWord中用"31bit” 存储对象的哈希码,“4bit” 存储对象分代年龄,"1bit"用来标记是否偏向锁,再未获取对象hashCode时,有54bit存储偏向锁的线程id,等信息
  2. 管程: 在java中所有类都继承自Object,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象,这个ObjectMonitor就是指管程monitor对象,在ObjectMonitor有几个需要关注的属性:
  1. _owner: 持有ObjectMonitor对象线程
  2. _WaitSet: 存放处于wait状态的线程队列
  3. _EntryList: 存放处于等等锁block状态的线程队列
  4. _recursions: 锁的重入次数
  5. _count: 记录该线程获取锁的次数
  1. 当多个线程同时访问一段同步代码时:
  1. 首先会进入_EntryList集合,当线程获取到对象的monitor后,进入_owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒;
  3. 当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
  1. 同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。
    监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问,锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识认识一下对象的内存布局
  1. 使用javap -c 或javap -v 对加锁代码进行反编译,会发现synchronized同步代码块与同步方法底层实现原理不同
  2. synchronized同步代码块底层原理
  1. 底层在对象头的"mark_word"中设置"monitorenter"指令表示加锁,"monitorexit"指令表示释放锁
  2. 通常情况下一个monitorenter加锁命令会对应两个monitorexit释放锁指令,防止代码发生异常时锁无法释放
  3. monitorenter与monitorexit 不是成对出现的那么monitorexit 一定会是两个吗?: 不一定,假设示例代码的method1()方法中我们手动抛出异常,再去对该class进行反编译,就会发现只出现了一个monitorenter对应一个monitorexit,并且会伴随两个athrow
  1. synchronized同步方法底层原理

与同步代码块不同,在执行调用指令时通过判断调用的方法是否被"ACC_SYNCHRONIZED"标识,如果有,执行线程会先持有monitor,然后再执行方法,方法执行完成释放monitor(无论方法正常或是异常都会释放),并且同步静态方法也是该方式与非同步方法比只是多了一个"ACC_STATIC"静态标识

synchronized 锁升级原理

  1. 先聊一下为什么要升级优化: java5以前synchronized是重量级锁,加锁时监视器monitor依赖与底层操作系统的MutexLock实现,挂起线程与恢复线程都需要转入内核态完成,而java的线程最终会映射到操作系统原生线程上(查看启动线程start()源码会发现底层调用的是,native方法start0,继续向下追最终会执行到OpenJDK中的JVM_StartThread, 是操作系统os的开启一个线程命令"start_thread()“),如果要阻塞或者唤醒一个线程操作系统要介入,需要用户态到内核态之间的切换(也就是ObjectMonitor中的”_owner"属性 持有的线程切换),比较耗费时间性能
  2. 锁升级的过程: 无锁—> 偏向锁—> 轻量级锁—>重量级锁
  3. 偏向锁
  1. 偏向锁的优点: 偏向锁的操作不涉及用户态到内核态的切换,同一个线程时不需要多次释放与获取锁,不存在CAS修改MarkWord线程指针, 加锁线程执行完毕后,不会主动释放锁
  2. 获取偏向锁流程: 当线程执行被synchronized修饰的代码时,会在对象头MarkWord中标记为偏向锁状态,并使用54位存储线程指针作为偏向线程标识,当再次获取锁时首先判断线程id与MarkWord中存储的是否一致
  1. 一致: 不需要再次尝试获取锁,直接进入同步代码,并且不需要加锁释放锁时通过CAS更新对象头,几乎没有性能消耗
  2. 不一致: 说明发送了竞争,JVM进行CAS操作修改MarkWord存储的线程指针为当前线程进行重偏向,或执行偏向锁锁撤销
  1. 偏向锁锁撤销: 如果CAS修改失败说明锁已经被其它线程获取,这时将阻塞,撤销偏向锁,撤销需要等待到全局安全点,同时检查持有偏向锁的线程是否还在执行(两种情况)
  1. 锁被其它线程获取还未执行完毕,此时其它线程进来争夺线程,该偏向锁会被取消,升级为轻量级锁,轻量级锁由原持有偏向锁的线程持有,从安全点继续执行同步代码,后面进来的竞争线程会进入自旋等待升级为轻量级锁的线程执行完毕释放,获取到该锁
  2. 前面获取锁线程执行完毕,当前线程进来判断对象头MarkWord中线程指针不是当前线程的,会将对象头设置成无锁状态并撤销偏向锁,然后进行重偏向
  1. 偏向锁还有一个注意点: 偏向锁与hashCode, 偏向锁是由对象头中的MarkWord中存储偏向线程id来实现的,如果一个对象获取了hashCode,会造成对象头中无法存储偏向线程id,或获取偏向锁后,执行hashcode将对象头中记录的偏向线程id给覆盖了, HotSpot VM 规定
  1. 当一个对象执行 hashCode() 或System.identityHashCode(),计算了 hashcode 后,则无法进入偏向锁状态。这意味着,如果要在不发生争用的对象上进行同步,则最好覆盖默认hashCode()实现,否则JVM不会优化。
  2. 当一个对象当前正处于偏向锁状态,需要计算 identity hashCode 的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
  3. 轻量级锁的实现中,会通过线程栈帧的锁记录存储 Displaced Mark Word;
  4. 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。
  1. 返回头来再说一下为什么要有偏向锁:在我们测试多线程并发争抢锁时,由于CPU调度问题会发现有有几个线程获取锁的概率始终比较高,也就是会出现饥饿,根据这一特性同一个线程,直接进入同步代码没有性能消耗, 然后再说一下如何解决锁饥饿,例如JUC下的ReentrantLock公平锁
  1. 轻量级锁
  1. 先举个例子: 假设AB两个线程执行, A线程先获取到锁,后续B线程执行判断MarkWord中的线程指针不是当前B线程的,此时B线程CAS尝试获取锁,有两种情况: B线程CAS成功将MarkWord中的线程指针修改为当前线程说明获取偏向锁成功,并重偏向与当前线程,情况二如果CAS修改MarkWord失败,则偏向锁升级为轻量级锁,并且由原持有偏向锁的线程持有,此时B线程就会进入自旋等待A线程执行完毕释放轻量级锁
  2. 轻量级锁每次退出同步块时都需要释放锁,
  1. 重量级锁
  1. 上面说到过假设两个线程竞争执行同步代码块,A线程还未执行完毕,此时B线程会在外面CAS等待,假如说A线程始终不释放锁,这时候B线程一直在外面CAS会销毁大量CPU资源,假如A线程不释放接着进来了B,C,D等多个线程呢,防止这个问题,轻量级锁设置了自旋的次数,与竞争线程数规定,当超过这个次数轻量级锁将不再自旋,升级为重量级锁
  2. java6前: 自旋线程数,自旋次数限制默认开启:默认自旋次数10次,或自旋线程数超过cpu核的一半会放弃自旋升级为重量级锁, java6后: 底层根据同一个锁上一次自旋的时间,拥有锁线程的状态来决定的
  1. synchronized 可重入分析: synchronized是可重入锁,每个锁对象都有一个锁计数器,跟执行持有该锁线程的指针, 前面了解过ObjectMonitor结构体,也就是ObjectMonitor结构体中的: "_owner "属性表示指向持有当前ObjectMonitor对象的线程, “_recursions” 属性表示锁重入次数
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JUC 十. synchronized深入 的相关文章

  • JDK 文档是语言规范的一部分吗?

    只有一名官员Java语言规范 https docs oracle com javase specs jls se8 html index html所有 Java 实现都必须遵守它 API文档怎么样 所有Java实现都需要遵守吗这个版本 ht
  • 通过SOCKS代理连接Kafka

    我有一个在 AWS 上运行的 Kafka 集群 我想用标准连接到集群卡夫卡控制台消费者从我的应用程序服务器 应用程序服务器可以通过 SOCKS 代理访问互联网 无需身份验证 如何告诉 Kafka 客户端通过代理进行连接 我尝试了很多事情 包
  • 如何在 Firebase 远程配置中从 JSON 获取值

    我是 Android 应用开发和 Firebase 的新手 我想知道如何获取存储在 Firebase 远程配置中的 JSONArray 文件中的值 String 和 Int 我使用 Firebase Remote Config 的最终目标是
  • “java.net.MalformedURLException:未找到协议”读取到 html 文件

    我收到一个错误 java net MalformedURLException Protocol not found 我想读取网络上的 HTML 文件 mainfest uses permission android name android
  • 如何在 Antlr4 中为零参数函数编写语法

    我的函数具有参数语法 如下面的词法分析器和解析器 MyFunctionsLexer g4 lexer grammar MyFunctionsLexer FUNCTION FUNCTION NAME A Za z0 9 DOT COMMA L
  • Java:在 eclipse 中导出到 .jar 文件

    我正在尝试将 Eclipse 中的程序导出到 jar 文件 在我的项目中 我添加了一些图片和 PDF s 当我导出到 jar 文件时 似乎只有main已编译并导出 我的意愿是如果可能的话将所有内容导出到 jar 文件 因为这样我想将其转换为
  • Spring RestTemplate 使用 cookie 遵循重定向

    最近我遇到了一个问题 我需要做一个GET请求远程服务 我假设使用一个简单的 servlet 并且 RestTemplate 返回Too many redirects 经过一番调查 似乎对指定远程服务发出的第一个请求实际上只是一个 302 重
  • 通往楼梯顶部的可能路径

    这是一个非常经典的问题 我听说谷歌在他们的面试中使用过这个问题 问题 制定一个递归方法 打印从楼梯底部到楼梯顶部的所有可能的独特路径 有 n 个楼梯 您一次只能走 1 步或 2 步 示例输出 如果它是一个有 3 级楼梯的楼梯 1 1 1 2
  • 来自十六进制代码的 Apache POI XSSFColor

    我想将单元格的前景色设置为十六进制代码中的给定颜色 例如 当我尝试将其设置为红色时 style setFillForegroundColor new XSSFColor Color decode FF0000 getIndexed 无论我在
  • 需要使用 joda 进行灵活的日期时间转换

    我想使用 joda 解析电子邮件中的日期时间字符串 不幸的是我得到了各种不同的格式 例如 Wed 19 Jan 2011 12 52 31 0600 Wed 19 Jan 2011 10 15 34 0800 PST Wed 19 Jan
  • Jackson XML ArrayList 输出具有两个包装器元素

    我在 Jackson 生成的 XML 输出中得到了两个包装器元素 我只想拥有一个 我有一个 Java bean Entity Table name CITIES JacksonXmlRootElement localName City pu
  • 如何避免 ArrayIndexOutOfBoundsException 或 IndexOutOfBoundsException? [复制]

    这个问题在这里已经有答案了 如果你的问题是我得到了java lang ArrayIndexOutOfBoundsException在我的代码中 我不明白为什么会发生这种情况 这意味着什么以及如何避免它 这应该是最全面的典范 https me
  • HashMap 值需要不可变吗?

    我知道 HashMap 中的键需要是不可变的 或者至少确保它们的哈希码 hashCode 不会改变或与另一个具有不同状态的对象发生冲突 但是 HashMap中存储的值是否需要与上面相同 为什么或者为什么不 这个想法是能够改变值 例如在其上调
  • QuerySyntaxException:无法找到类

    我正在使用 hql 生成 JunctionManagementListDto 类的实际 Java 对象 但我最终在控制台上出现以下异常 org hibernate hql internal ast QuerySyntaxException
  • 是否可以使用 Java Guava 将函数应用于集合?

    我想使用 Guava 将函数应用于集合 地图等 基本上 我需要调整 a 的行和列的大小Table分别使所有行和列的大小相同 执行如下操作 Table
  • Hamcrest Matchers - 断言列表类型

    问题 我目前正在尝试使用 Hamcrest Matchers 来断言返回的列表类型是特定类型 例如 假设我的服务调用返回以下列表 List
  • Android:无法发送http post

    我一直在绞尽脑汁试图弄清楚如何在 Android 中发送 post 方法 这就是我的代码的样子 public class HomeActivity extends Activity implements OnClickListener pr
  • 如何使用play框架上传多个文件?

    我在用play framework 2 1 2 使用java我正在创建视图来上传多个文件 我的代码在这里 form action routes upload up enctype gt multipart form data
  • 将 Apache Camel 执行器指标发送到 Prometheus

    我正在尝试转发 添加 Actuator Camel 指标 actuator camelroutes 将交换 交易数量等指标 发送到 Prometheus Actuator 端点 有没有办法让我配置 Camel 将这些指标添加到 Promet
  • 在java中使用多个bufferedImage

    我正在 java 小程序中制作游戏 并且正在尝试优化我的代码以减少闪烁 我已经实现了双缓冲 因此我尝试使用另一个 BufferedImage 来存储不改变的游戏背景元素的图片 这是我的代码的相关部分 public class QuizApp

随机推荐

  • 第八章 假设检验

    目录 一 假设检验的基本概念 假设及假设检验的定义 原假设与备择假设 基本思想 接受域与拒绝域 假设检验的分类 两类错误 二 一个正态总体下的参数假设检验 期望 方差的假设检验 三 两个正态总体下的参数假设检验 期望的差异性 方差的差异性的
  • 【H5】 svg的 defs用法 渐变

    defs defs元素用于预定义一个元素使其能够在SVG图像中重复使用 在元素中定义的图形不会直接显示在SVG图像上 要显 示它们需要使用元素来引入它们 symbol 元素用于定义可重复使用的符号 嵌入在元素中的图形是不会被直接显示 的 除
  • 前端高频面试题汇总(css,html)

    目录 H5 的新特性有哪些 C3 的新特性有哪些 如何使一个盒子水平垂直居中 如何实现双飞翼 圣杯 布局 CSS 的盒模型 CSS 中选择器的优先级以及 CSS 权重如何计算 列举 5 个以上的 H5 input 元素 type 属性值 C
  • Keil警告warning: #223-D: function “memcpy” declared implicitly

    使用memcpy 函数编译后出现警告 解决方案 在 h文件中加上头文件 include string h
  • GPTzero

    关于GPTzero 网址 https gptzero me 这是我今天才发现的网站 功能是你可以分辨任何人工智能写的文章与人为写的文章 注册方法 注册方法非常简单 只需要一个电子邮箱 我用的是outlook邮箱 其他的也行 使用方法 登录成
  • 【超分辨率】—基于深度学习的图像超分辨率最新进展与趋势

    1 简介 图像超分辨率是计算机视觉和图像处理领域一个非常重要的研究问题 在医疗图像分析 生物特征识别 视频监控与安全等实际场景中有着广泛的应用 随着深度学习技术的发展 基于深度学习的图像超分方法在多个测试任务上 取得了目前最优的性能和效果
  • 我在修改jupyter字体的时候输入命令jt -l 遇到了jt既不是内部也不是外部命令咋整?...

    点击上方 Python爬虫与数据挖掘 进行关注 回复 书籍 即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 独立三边静 轻生一剑知 大家好 我是Python进阶者 一 前言 前几天在Python白银交流群 Joker 问了一
  • 如何在本地快速启动一个k8s集群?小技巧,学到了

    背景 最近在阅读 每天5分钟玩转Kubernetes 这本书 个人感觉是一本不错的 K8S 的入门书籍 我们在刚开始学习一项技术的时候 不论是通过官方文档 书籍 亦或是视频的形式 如果仅仅是去看 而不去练习实践的话 那么是很难将其真正应用起
  • JSTL的错误“attribute test does not accept any expressions”解决方法

    解决方法有2个 1 将 更改为 2 使用JSTL的备用库 将 更改为
  • 数据中台与数据仓库比较

    从三个点来说 1 提供服务的对象 2 业务域 3 层次的划分 1 提供服务的对象 a 数据仓库的服务对象基本上是人 明细数据 聚合指标 转化率模型 他们的目前用户都是人 b 数据中台的服务对象变成 人 机器 用户标签 机器学习模型 数据挖掘
  • 扩展Lucas定理

    本介绍主要是在学习后写下自己的理解 故以转载形式发出 题意 给定 n m p n m p n m p 求 C
  • 个人游戏作品动态图展示

    暗黑破坏神3D战斗手游 2D射击联网对战小游戏 类似以前玩过的4399 联网炸金花游戏 解密类小游戏
  • CRC 在线计算器

    On line CRC calculation and free library https www lammertbies nl comm info crc calculation html
  • warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]

    在C 11中有明确规定 char p abc valid in C invalid in C 如果你进行了这样的赋值 那么编译器就会跳出诸如标题的警告 但是如果你改成下面这样就会通过warning char p char abc OK 或者
  • Liunx基础-进程概念(下)

    目录 一 程序地址空间 二 fork返回值问题理解 一 程序地址空间 研究背景 kernel 2 6 32 32位平台 程序地址空间回顾 1 地址空间描述的基本空间大小单位是字节 2 32位下 gt 2 32次方个地址 3 一共有2 32个
  • 【2023】终端的n种打开方式+Anaconda修改虚拟环境默认安装路径+创建虚拟环境

    目录 一 终端的n种打开方式 1 使用 运行 对话框 2 通过右键菜单 3 通过开始菜单 4 通过搜索 5 通过文件资源管理器 6 通过任务管理器 二 更改虚拟环境安装路径 1 使用命令行修改 方法一 查看conda配置 使用如下命令更改默
  • 【MATLAB教程案例23】基于MATLAB图像分割算法仿真——阈值分割法、Otsu阈值分割法、K均值聚类分割法等

    FPGA教程目录 MATLAB教程目录 目录 1 软件版本 2 通过二值图实现图像分割 3 通过Otsu阈值分割实现图像分割
  • ES6的理解

    1 ES6是什么 用来做什么 ES6 全称 ECMAScript 6 0 是 JavaScript 的下一个版本标准 2015 06 发版 它的目标 是使得 JavaScript 语言可以用来编写复杂的大型应用程序 成为企业级开发语言 ES
  • 企业微信开发第三方应用开发视频教程,ToB Dev李月喜全网首发

    csdn程序员学院 企业微信三方应用开发 视频课程 全网企业微信三方应用开发教程首发 https edu csdn net course detail 30582 即将完结欢迎试看购买 下为目录 课程名称 企业微信开发之第三方应用开发篇 课
  • JUC 十. synchronized深入

    目录 一 基础复习 二 根据synchronized修饰不同成员了解synchronized实现原理 同步代码块 小总结 同步方法 小总结 synchronized 与 管程 底层分析 三 锁升级相关 复习一下对象头 无锁演示 偏向锁 演示