首页 > 编程知识 正文

偏向锁和轻量级锁区别,多线程synchronized

时间:2023-05-04 01:44:57 阅读:31486 作者:4145

Java锁定机构(同步) Java锁定机构(同步) JVM内存结构对象、对象头和结构同步锁定四种状态无锁定偏转锁定轻量级锁定

Java锁定机制JVM内存结构

在了解Java锁定机制之前,请复习一下JVM的内存结构

对象、对象标头和结构Java中的对象包含三个部分:对象标头、实例数据和对齐填充字节。

对齐字节旨在满足以下条件:Java对象的大小必须是8bit的倍数

实例数据是您初始化对象时设置的属性和状态的内容(属性和方法)。

对象头包含对象本身运行时的信息。 对象头包含两部分:拼命毛豆Word和Class Pointer。 与实例数据相比,对象标头是几个外部的存储开销,因此设计得非常小以提高效率。

Class Pointer是指向当前对象类型所在的方法区域中的类型数据的指针。 冒着生命危险的毛豆Word保存着很多有关当前对象的运行时锁定状态的数据(例如

从这张表可以看出,冒着生命危险的毛豆Word只有32bit,是结构化的。 因此,在锁标志位下,不同的字段只是重用不同的位,从而节省空间。

首先,让我们来关注冒着生命危险的毛豆Word的最后两个人。 这两个人表示锁定标记的位置,分别对应无锁定、偏转锁定、轻量锁定、重量锁定4种状态。

同步Java众所周知,可以使用同步关键字同步线程。 同步被编译后生成monitorenter和monitorexit的双字节码指令,依赖于该双字节码指令进行线程同步。

我们写验证码

publicclasstestsync {私有内部=0; public void test () {for ) intI=0; i1000; I )同步(this ) system.out.println (thread 3360 ) thread.current thread (.getid ) ) (num ) )。 } } publicstaticvoidmain (字符串[ ] args ) ) testsync=newtestsync ); threadT1=newthread(newrunnable () {@Overridepublic void run ) {sync.test ); }; ); threadT2=newthread(newrunnable () {@Overridepublic void run ) {sync.test ); }; ); t1 .开始(; T2 .开始(; 使用javac编译java文件,然后使用javap -c反向编译class文件,即可获得字节码。

可以看到monitorenter和monitorexit包装了我们的核心代码,表明验证成功。

Monitor经常被翻译为监视器管程。 简而言之,他可以认为是只能容纳一个人的房间。 检索对象的线程是进入房间的人,一个线程进入Monitor后,其他人只能等待,该线程结束时可以进行其他线程

synchronized的同步机制

请看上面的图。 首先,Entry Set中聚集了想进入监视器的线程。 如果他们都处于等待状态,并且参加了一个名为A的线程并成功进入监视器,那么他就会变为活动状态。 假设在执行线程的过程中出现判断条件,需要临时转让执行权时,进入等待集,状态也被标记为等待,Entry Set的线程b有机会进入监视器。 线程b进入监视器并执行代码后,以通告的形式启动等待集的a线程。

synchronized的性能问题

上述同步生成两个字节代码指令: Monitorenter和monitorexit,但monitor依赖操作系统的mutex lock实现。 Java线程实际上映射操作系统线程,因此每次线程唤醒或挂起时,都会在操作系统中的用户状态和内核状态之间切换。 在某些情况下,这种操作时的重量切换时间可能超过线程的任务执行时间,因此使用同步会严重影响程序的性能。

的四种状态未锁定的yydbmh是资源未锁定,并且所有线程都可以访问同一资源。 这涉及很多情况

无冲突:线程之间不存在冲突,直接获取资源就存在冲突:使用非锁定方式实现同步线程。 这就是我们熟知的CAS (比较器) CAS是通过操作系统的指令实现的

,所以它可以保证原子性,通过诸如CAS这种方式,我们就可以进行无锁编程。

偏向锁

​ 上面我们也分析了依赖操作系统的mutex lock导致性能低下的原因,所以在大部分情况下无锁的效率是很高的,但这并非意味着无锁能全面代替有锁。

​ 现在我们给对象开始加锁,假如一个对象被加锁了,但在实际运行时只有一个线程会获取这个对象锁。那么我们最理想的情况就是不通过线程状态切换,也不需要通过CAS来获得锁,因为这多多少少还是会耗费一些资源。我们设想的是最好对象能够认识这个线程,只要是这个线程过来,那么对象直接把锁交出去,我们就可以认为这个锁偏爱这个线程,所以被称为偏向锁,那么偏向锁是如何实现的?

再看到这幅玩命的毛豆 Word的图:

我们先通过判断后三位来判断是否是偏向锁,如果是偏向锁的话,它的前23位就是用来记录偏爱的进程ID。

轻量级锁

如果对象发现目前不知有一个线程,而是有多个线程正在竞争锁,那么偏向锁就会升级成轻量级锁。

当锁的状态还是偏向锁时,是通过玩命的毛豆 Word中的线程id来找到线程,那么当锁的状态升级到轻量级锁的时候,如何判断线程和锁之间的绑定关系呢?

图中给出了答案:通过前30位来指向栈中锁记录的指针。

我们来具体研究一下:

当一个线程想要获得某个对象的锁时,加入看到锁的标志位为00,那么就知道它是一个轻量级锁。

这时线程会再自己的虚拟机栈中开辟一块被称为Lock Record的空间。

关于虚拟机栈,上面JVM内存结构中提到了这是线程私有的。

那么Lock Record存放的是什么呢?存放的是对象头中玩命的毛豆 Word的副本,以及owner指针

线程通过CAS去尝试获取锁,一旦获得那么将会赋值该对象头中的玩命的毛豆 Word到Lock Record中,并且将Lock Record中的owner指针指向该对象。

同时对象的玩命的毛豆 Word的前30位将会生成一个指针,指向虚拟机栈中的Lock Record

这样就完成了线程和对象之间的绑定,他们可以互相知道对方的存在。

完成了线程和对象之间的绑定之后,万一有其他线程也想要获取到这个对象怎么办?

此时其他需要资源的线程将会进入自选等待(CPU空转)如果长时间进行自旋对CPU来说是一种浪费,于是出现了**”适应性自旋“**的优化。

适应性自旋

​ 自旋时间不再固定,而是由上一次在同个锁上的自旋时间以及锁状态,这两个条件来进行决定。

​ 举个例子:当前正在自旋等待的线程刚刚已经成功获得过了锁,但是锁目前是被其他线程占用,那么虚拟机就会认为这次自旋也很有可能会成功,进而允许更长的自旋时间。

​ 假如此时有一个线程正在进行自旋,那么这个线程将会进行等待。如果同时有多个线程想要获得这个对象锁,也就是一旦自选等待的线程超过一个,那么轻量级锁将会升级为重量级锁。

重量级锁

如果对象锁状态被标记为重量级锁,那么就是和我们最初讲的那样,需要通过Monitor来对线程进行控制,此时将会完全锁定资源,对线程的管控最为严格。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。