| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> 吃透synchronized关键字 -> 正文阅读 |
|
[Java知识库]吃透synchronized关键字 |
Java中的锁分为显示锁和隐式锁。隐式锁由synchronized关键字实现,而显示锁是由实现了Lock接口和AQS框架等等类来实现。 锁的分类 乐观锁:乐观锁就是对数据冲突保持乐观点态度,认为不会有其他线程同时修改数据。因此乐观锁不会上锁,只是在更新数据都时候判断是否有其他线程更新,如果没有其他线程修改则跟新数据,有其他线程修改则放弃数据,重新读取数据处理。 悲观锁:悲观锁被数据冲突持悲观的态度,认为总是发生数据冲突。因此它以一种预防的态度,先行把数据锁住,知道操作完成才释放锁,在此期间其他线程无法操作数据。 synchronized关键字 Java 中的每一个对象都可以作为锁,有三种加锁的方式: 对象头 synchronized关键字的实现,依赖于Java的对象头。一个对象由三部分组成:对象头、实体数据、对齐填充。对象头的长度不是固定的,如果是数据类型则对象头占12个字节,非数组类型对象头占8个字。非数组类型的对象头分两部分,Mark Word 和对象类型指针,数组类型对象会多一部分来存储数组的长度。而synchronized关键字的实现,就和对象头中的Mark Word密切相关。
难道锁的不同仅仅是标志不同吗?肯定不是,不同的锁各自实现的途径不同,适合的场景也各不相同。理解各种锁是怎样实现的,也就理解了synchronized关键字,理解了锁优化过程。 无锁 按最后3位标识位来判断,无锁应该是001状态,偏向标志为0,锁标志为01。更准确的讲001状态是无锁不可偏,还有一种是101状态,无锁可偏(又叫匿名偏向)。无锁不可偏状态下遇到同步,会直接升级为轻量级锁,而不会变为偏向锁(看名字也知道,不可偏嘛)。只有在无锁可偏的状态下,才可能变成偏向锁。匿名偏向状态下虽然标识码是101,但是线程ID部分全部0,意味着没有线程实际获得偏向锁。 为啥要分无锁可偏和无锁不可偏呢?因为所有偏向锁的起点就是匿名偏向状态,无锁不可偏状态会直接变为轻量级锁。首先如果JVM设置取消偏向锁,那么无锁状态只可能是无锁不可偏。JDK8默认启动了偏向锁,但是偏向锁时在JVM启动几秒(默认4秒,但可以设置更改)之后才会启动,此时能设置为匿名偏向的会全部设置为匿名偏向,因此匿名偏向是偏向锁的起点。 而JVM为啥会在几秒之后才会启用偏向锁,这是因为JVM内部的代码会使用synchronized,这些类里有很多激烈线程竞争,如果采用锁升级策略这些锁会浪费很多时间。所以JVM索性先不开启偏向锁,先执行这些库类,等过几秒差不多都执行完了再开启偏向锁。至于JDK8为啥默认4秒,这是个经验值,4秒大多数类都启动完了,此数值可以修改。 偏向锁 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。这个锁永远会偏向于获得它的线程,如果在获得锁之后并没有其他线程获取,则获得偏向锁的线程永远不需要同步,减少锁带来的时间消耗。 偏向锁的获取 偏向锁对象头将不在存放Hash值,而在此位置上存放线程ID(23bit)+Epoch(2bit),一共25bit,其他部分保持不变。Epoch是一个时间戳,用来判断线程ID是否过时。具体获得锁流程如下: 匿名偏向是偏向锁的初始状态,所以先判断锁标志,再判断偏向锁标志位,只有最后三位是101才开始,否则直接走其他的锁。如果是匿名状态,线程ID为0,采用CAS去将当前线程写入,如果成功则获得锁,不成功表示存在竞争。线程ID不为0,此前已经有偏向,判断此值是否和当前线程相同,若一致则表示线程之前就获得了锁,不一致就尝试CAS替换。同意,替换成功获得锁,替换失败存在竞争。未获得锁时将会等待安全点(STW),安全点会进行偏向锁的撤销。 安全点是JVM在进行垃圾GC时为了保证引用关系不会发生变化而设置的安全状态(GC Roots的确定就在此时),STW(STOP THE WORLD)此时将暂停所有线程的工作。在STW会检测持有偏向锁的线程是否还存活,如果存活则升级轻量级锁,如果线程未存活或者已经退出来同步代码块,将会判断是否可重偏向,否则直接升级为轻量级锁。允许重偏向时会先设置为匿名重偏向,在使用CAS偏向线程。 判断是否可重偏向需要用到Epoch,偏向锁中有一个Epoch,对应的Class类中也有一个Epoch。在进入全局安全点之后,首先会对Class类中的Epoch进行增加,得到新的Epoch_new,然后扫描所有持有Class类实例的线程,根据线程信息判断是否锁住了该对象。如果锁住了说明此对象还在使用,将Epoch_new更新给它,如果未锁住则说明不需要加锁,不进行更新。如果对象的Epoch和类的Epoch相同,则表示它是被更新过的,需要锁,不能重偏向。而如果不相同,则表示已经不需要加锁了,此对象可以重偏向到其他线程。
从偏向锁的获取过程可以看到,等到竞争出现的时候才会释放。如果没有出现竞争,它不会去改变Mark Word的相关字段。就算是线程已经执行完同步代码块,不需要加锁了,也不会去修改对象头,那个锁依旧存在,依旧保持偏向。只是在其他线程需要偏向,出现了竞争的时候会进行判断,如果以前偏向的线程不需要了,那么对象首先会被设置为匿名偏向,然后CAS替换尝试加锁。如果以前偏向的线程还需要加锁,升级为轻量级锁。 所以线程不会主动的将偏向锁设置为匿名偏向状态,不会主动的去释放锁。 批量偏向与批量撤销 偏向锁有三个参数: 批量重偏向是以class而不是对象为单位的,每个class会维护一个偏向锁的撤销计数器,每当该class的对象发生偏向锁的撤销时,该计数器会加一,当这个值达到默认阈值20时,jvm就会认为这个锁对象不再适合原线程,因此进行批量重偏向。而距离上次批量重偏向的25秒内,如果撤销计数达到40,就会发生批量撤销,如果超过25秒,那么就会重置在[20, 40)内的计数。 当一个线程建立了大量的对象,并对他们都加了偏向锁。而此时若另一个线程也来获取这些对象,此时发生了竞争理论上都会升级轻量级锁。但是因为批量偏向的存在,并不会全部升级。 假设线程A建立了10个对象,全部加偏向锁,随后线程B同样也对这40个对象加锁。线程B对每一个对象进行加锁是,都会导致撤销一次偏向锁,升级为轻量级锁。当这个数值变为20后,JVM会认为其余的对象也不适合线程A,当后面的对象遇到需要同步的时候,会先被重置为可偏向状态,以便快速冲偏向。这样线程B对后面的对象加锁就不会升级为轻量级锁,而是偏向了线程B。 当线程C再来加锁的时候,前20个对象竞争轻量级锁,直接竞争,后20个锁是偏向线程B的,是偏向锁。此时不会触发批量重偏向,所以后20个也升级为轻量级锁。升级为轻量级锁就需要撤销偏向锁,加上之前的20次,一共40次。达到40次撤销偏向锁,会触发批量撤销机制,将偏向锁升级为轻量级锁,并且此类新建的对象都不是无锁不可篇状态,不会出现偏向锁。 偏向锁的优缺点 优点: 在只有单一线程访问对象的时候,偏向锁几乎没有影响。只有第一次需要CAS操作替换,随后的只要比较线程ID即可,比较方便快速。 缺点: 如果对象需要调用Object方法,会启用对象的minoter内置锁.。此时会直接由偏向锁退出进入重量级锁。 轻量级锁 轻量级锁是相对于使用操作系统互斥量来实 现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 轻量级锁的获取 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,此空间包含两部分: displaced mark word:用于存储锁对象目前的Mark Word的拷贝 虚拟即首先会将对象的Mark World拷贝到栈帧中的Lock Record,此时owner为空。 使用CAS尝试将Lock Record中的displaced mark word替换回去,需要检查对象头中的指针是否指向当前线程。如果替换成功,表示没有竞争,锁成功释放,如果替换失败会进行自旋,如果自旋之后仍未获得锁表示存在竞争并升级为重量级锁。当其他线程竞争轻量级锁时,并不会对已经持有轻量级锁的线程发送什么,而是对象头的锁标志被修改,同时竞争线程自身被挂起。因此如果CAS替换失败,原本持有锁的线程除了释放锁之外,还需要唤醒被挂起的线程。
轻量级锁的重入 轻量锁的每一次重入,都会在栈中生成一个Lock Record。只是只有第一次会拷贝Mark Word,随后的加锁Displaced Mark Word区域为NULL,owner区域统一指向对象头。 线程第一次获得锁时,将对象的Mark Word拷贝到本线程Lock Record,同时将对象的指针指向自己,Lock Record中的owner也指向对象。随后的加锁对象中存储的是指向本线程的指针,并没有Mark Word,也就不需要拷贝。只是将owner指向对象即可。 每加一次锁帧栈中多一个Lock Record,Lock Record的个数也就是加锁的次数。释放锁时也要一个一个释放,只有解锁次数等于加锁次数,才会真正释放锁。释放锁时,如果是重入锁则直接删掉一个Lock Record,如果不是重入则采用CAS替换对象的Mark Word。 轻量级锁自旋 当轻量级出现竞争以后,会尝试进行自旋。自旋就是CPU空转,线程没有挂起依然在执行,等过一段时间后再去加锁。这是因为如果升级为重量级锁,是通过操作系统来实现,涉及到内核态和用户态之间的切换,这个操作的比较耗时。如果竞争没有那么激烈,锁住的同步代码块执行的时间还没有切换上下文花的时间多,反而得不偿失。因此采用自旋锁,出现竞争之后等一等再去尝试,可能前面获得锁的线程已经执行完了,再次加锁。这样就免去了升级重量锁带来的消耗。 自旋不能一直进行。自旋时CPU是空转,这就浪费了处理器资源。上面的情况是竞争不激烈,但假设竞争激烈那么自旋完全是浪费时间,还不如直接升级到重量级锁省资源。以前的自旋次数默认是10,如果10次之后依然不行说明竞争很激烈,需要升级到重量级锁。JDK1.6以后加入了自适应自旋: 对于某个锁对象,如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而允许自旋等待持续相对更长时间 除此之外,JVM还会根据CPU的负载进行优化:
轻量级锁的优缺点 适合多个线程不同时访问同步对象场景。偏向锁的撤销必须在安全点才可以进行,假设多个线程交替访问某个对象,处理完成后又释放锁,这种情况下偏向锁也可以完成但是效率会很低,每次撤销再加锁必须等到安全点,同时还要进行Epoch的分析,因此偏向锁才会有批量撤销机制,撤销偏向锁次数过多则意味偏向锁不适用,不再新增偏向锁而直接变成轻量级锁。轻量锁则很方便,直接CAS替换就可以,对这种多线程竞争不激烈的场景很适用。锁的自旋也是为此设计。 缺点: 竞争激烈的场景下不适用,此时进行自旋就是再浪费CPU资源。竞争激烈时可能进行很多次的自旋都不回获得锁,这种浪费的代价比上下文切换的代价要大,所以引入自适应自旋来裁决。 重量级锁 重量级锁的实现依赖于ObjectMonitor,而ObjectMonitor又依赖于操作系统底层的Mutex Lock(互斥锁)实现。 Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。主要包含以下几部分: Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中; 重量级锁的获取 锁升级为重量级之后,Mark Word中存储的指针不再指向线程,而是指向ObjectMonitor。当线程访问同步代码块时,每个线程都会被封装成一个ObjectWaiter对象进入monitor。 JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。 OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。 处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。 Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。 为啥采取非公平锁,主要还是考虑到性能。采取非公平锁下一次获得锁线程可能还是原来持有锁的线程,这样就避免了内核和用户态的切换,节省时间。 重量级锁的重入 线程如果获取到锁,会判断是否重入,如果是重入锁,count计数+1,释放锁时count数值减1. 重量级锁的优缺点 重量级锁需要内核态和用户态的切换,这个代价很大。所以把它放在最后,经过偏向和轻量级之后才是它。但是只有它能应对竞争激烈的场景,也算是JVM最后的杀手锏了。 参考阅读: |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 3:46:46- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |