IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: 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 中的每一个对象都可以作为锁,有三种加锁的方式:
? 对于普通同步方法,锁是当前实例对象。
? 对于静态同步方法,锁是当前类的 Class 对象。
? 对于同步方法块,锁是 Synchonized 括号里配置的对象。

对象头

synchronized关键字的实现,依赖于Java的对象头。一个对象由三部分组成:对象头、实体数据、对齐填充。对象头的长度不是固定的,如果是数据类型则对象头占12个字节,非数组类型对象头占8个字。非数组类型的对象头分两部分,Mark Word 和对象类型指针,数组类型对象会多一部分来存储数组的长度。而synchronized关键字的实现,就和对象头中的Mark Word密切相关。

在这里插入图片描述
在这里插入图片描述
Java 对象头里的 Mark Word 里默认存储对象的 HashCode、分代年龄、是否偏向锁以及锁标记位。32位 JVM 的 Mark Word 的默认存储结构如下
在这里插入图片描述
为了提高虚拟机空间的使用效率,Mark Word被设计成一个非固定的动态数据结构,以便存储更多的信息。不同状态下对象头Mark Word存储的信息如下图
在这里插入图片描述
由图可知分为未锁定、偏向所、轻量级锁、重量级锁、GC,但是在轻量级锁升级为重量级锁时,还可能进行自旋。synchronized锁的状态被分为4种,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。注意在锁标志中并没有出现自旋锁,它仅仅是锁可能存在的一种状态,是暂时性的,并没有官方的标志。32位虚拟机下,Mark Word的最后3位就可以判断锁的状态。无锁和偏向锁标志位都是01,只是偏向锁时偏向模式会被置为1。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

难道锁的不同仅仅是标志不同吗?肯定不是,不同的锁各自实现的途径不同,适合的场景也各不相同。理解各种锁是怎样实现的,也就理解了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替换尝试加锁。如果以前偏向的线程还需要加锁,升级为轻量级锁。

所以线程不会主动的将偏向锁设置为匿名偏向状态,不会主动的去释放锁。

批量偏向与批量撤销

偏向锁有三个参数:
BiasedLockingBulkRebiasThreshold:偏向锁批量重偏向阈值,默认为20次
BiasedLockingBulkRevokeThreshold:偏向锁批量撤销阈值,默认为40次
BiasedLockingDecayTime:重置计数的延迟时间,默认值为25000毫秒(即25秒)

批量重偏向是以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即可,比较方便快速。

缺点:
如果有多个线程访问,就会出现竞争,竞争需要等到安全点时,并且进行一系列分析比较耗费时间。另外,偏向锁存放线程ID和Epoch后,对象头中不存在Hash值,如果程序需要Hash值需要调用HashCode,这会导致偏向锁退出。

如果对象需要调用Object方法,会启用对象的minoter内置锁.。此时会直接由偏向锁退出进入重量级锁。

轻量级锁

轻量级锁是相对于使用操作系统互斥量来实 现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

轻量级锁的获取

虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,此空间包含两部分:

displaced mark word:用于存储锁对象目前的Mark Word的拷贝
owner:指向当前的锁对象的指针

虚拟即首先会将对象的Mark World拷贝到栈帧中的Lock Record,此时owner为空。
在这里插入图片描述
然后虚拟机使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针(此空间占30bit, 除了最后两位锁标志,Mark Word前30bit均用来存放指针),如果操作成功则代表线程拥有了这个对象锁,对象锁标志位变为00.如失败了则会会判断对象Mark Word存储的指针是否指向自己,如果是则表示拥有锁,执行同步代码,如果不是则意味着其他线程抢占,出现激烈竞争需要升级为重量级锁。
在这里插入图片描述
轻量级锁的释放

使用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的负载进行优化:

如果平均负载小于CPUs则一直自旋

如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

如果CPU处于节电模式则停止自旋

自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

自旋时会适当放弃线程优先级之间的差异

轻量级锁的优缺点
优点:

适合多个线程不同时访问同步对象场景。偏向锁的撤销必须在安全点才可以进行,假设多个线程交替访问某个对象,处理完成后又释放锁,这种情况下偏向锁也可以完成但是效率会很低,每次撤销再加锁必须等到安全点,同时还要进行Epoch的分析,因此偏向锁才会有批量撤销机制,撤销偏向锁次数过多则意味偏向锁不适用,不再新增偏向锁而直接变成轻量级锁。轻量锁则很方便,直接CAS替换就可以,对这种多线程竞争不激烈的场景很适用。锁的自旋也是为此设计。

缺点:

竞争激烈的场景下不适用,此时进行自旋就是再浪费CPU资源。竞争激烈时可能进行很多次的自旋都不回获得锁,这种浪费的代价比上下文切换的代价要大,所以引入自适应自旋来裁决。

重量级锁

重量级锁的实现依赖于ObjectMonitor,而ObjectMonitor又依赖于操作系统底层的Mutex Lock(互斥锁)实现。

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。主要包含以下几部分:

在这里插入图片描述

Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
Owner:当前已经获取到所资源的线程被称为Owner;
!Owner:当前释放锁的线程。
count:monitor的计数器,数值加1表示当前对象的锁被一个线程获取,线程释放monitor对象时减1

重量级锁的获取

锁升级为重量级之后,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最后的杀手锏了。

参考阅读:
再谈synchronized锁升级
Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
Java锁—偏向锁、轻量级锁、自旋锁、重量级锁
谈谈JVM内部锁升级过程
偏向锁竞争
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
java 偏向锁

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-25 07:59:26  更:2021-11-25 07:59:28 
 
开发: 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-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码