三、锁的分类

锁的存放位置

所有的锁都是基于对象的

每个 Java 对象都有一个对象头。如果是非数组类型,则用 2 个字宽来存储对象头,如果是数组,则会用 3 个字宽来存储对象头

对象头:

长度 内容 说明
一字宽(32/64 bit) Mark Word 存储对象的 hashCode 或锁信息
一字宽(32/64 bit) Class Metadata Address 存储对象类型数据的指针
一字宽(32/64 bit) Array length (若是数组)数组的长度

Mark Word 格式:

锁状态 29/61 bit 是否为偏向锁(1 bit) 锁标志位(2 bit)
无锁 0 01
偏向锁 线程 ID 1 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量的指针 10
GC 标记 11

GC 标记指的是垃圾收集器在进行垃圾回收

CAS 操作

概念

1.定义:

CAS(Compare-and-Swap)是一种乐观锁的实现方式,全称为“比较并交换”,是一种无锁的原子操作。

2.比较并交换的过程:

V:要更新的变量(var

E:预期值(expected)(本质上指的是旧值)

N:新值(new

判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。

3.原子性:

CAS 是一种原子操作,它是一种系统原语,从 CPU 层面已经保证它的原子性

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败

三大问题

ABA 问题

问题:

一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次

解决:

在变量前面追加上版本号或者时间戳,Java 中提供了追加版本号的解决方案

长时间自旋

问题:

CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源

自旋:不断尝试去获取锁,一般用循环来实现

解决:

让 JVM 支持 CPU 提供的 pause 指令。自旋失败后进入短暂延迟,为其他线程让出 CPU

多共享变量的原子操作

问题:

CAS 只能保证单个变量的原子性,无法保证多个变量共同的原子性

解决:

1)将多个变量放置到一个对象中进行 CAS 操作

2)使用 Synchronized 悲观锁

乐观锁与悲观锁

一种对锁的分类方式

悲观锁

1)它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行

2)用于“写多读少”的环境,避免频繁失败和重试影响性能

3)Synchronized 使用的是悲观锁

乐观锁

1)假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 操作来保证线程执行的安全性

2)用于“读多写少”的环境,避免频繁加锁影响性能

3)因为没有实际地锁存在,所以天生免疫死锁

无锁、偏向锁、轻量级锁、重量级锁

与悲观锁乐观锁相同,也是一种对锁的分类方式

无锁

没有对资源进行锁定,任何线程都可以尝试去修改它

偏向锁

定义

偏向锁会偏向于第一个访问锁的线程,在运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步,无需再走各种加锁/解锁流程。当发生了竞争再进行同步操作。

原理

1)线程第一次进入同步块,在锁对象中存储线程 ID。当一个线程进入同步块时,会检查该线程 ID 与自己的线程 ID 是否相同

2)若相同,则进入/退出同步块不需要进行 CAS 操作来加锁和解锁;若不同,则尝试使用 CAS 替换线程 ID

3)若替换成功,则之前的线程已不存在,锁的线程 ID 改为新线程 ID。锁仍然为偏向锁,不会升级;若替换失败,则之前的进程仍存在,暂停之前的进程,升级锁为轻量级锁,按照轻量级锁的方式进行竞争

轻量级锁

定义

JVM 采用轻量级锁来避免线程的阻塞与唤醒。多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞

原理

1)一个线程获得锁时是轻量级锁,会将锁的 Mark Word 复制到自己的 Displaced Mark Word 存储

2)尝试 CAS 将锁的 Mark Word 替换为指向锁记录的指针以获取锁。若成功,则当前线程获得锁。若失败,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁

自旋:不断尝试去获取锁,一般用循环来实现

自旋是需要消耗 CPU 的,若一直获取不到锁,线程会一直浪费 CPU 资源

通常会指定自旋次数,JDK 采用适应性自旋。若线程自旋成功,则下次自旋的次数更多,反之更少

3)当自旋超限后,线程就会阻塞,同时锁升级为重量级锁

重量级锁

定义

重量级锁依赖于操作系统的互斥锁实现,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU

概念

当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:

1)Contention List:竞争队列,所有请求锁的线程被首先置于该队列

2)Entry ListContention List中有资格成为候选人的线程被移入该队列

3)Wait Set:放置调用 wait 方法被阻塞的线程

4)OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck

5)Owner:获得锁的线程

6)!Owner:释放锁的线程

原理

1)线程尝试获得锁时,若锁已经被占用,则将该线程封装成一个ObjectWaiter对象插入到Contention List队列的队首,然后调用park方法挂起该线程

2)当持有锁的线程释放锁时,会从Contention ListEntryList中挑选一个线程唤醒尝试获得锁,被选中的线程叫做Heir presumptive即假定继承人

3)假定继承人不一定能百分百获取锁,因为尝试获取锁失败,线程会直接进入阻塞状态,等待操作系统的调度

4)若线程获得锁后调用Object.wait方法,则会加入到WaitSet中。当线程被Object.notify唤醒后,会将线程从WaitSet移动到Contention ListEntryList中去

(当调用一个锁对象的waitnotify方法时,锁会直接膨胀成重量级锁)

锁的升级流程

对于每个线程,尝试获取锁时:

1)检查MarkWord里面是不是放的自己的ThreadId,若是,表示当前线程是处于“偏向锁”,直接获取锁并执行

2)若不是自己的ThreadId,锁升级为轻量级锁,用 CAS 来执行切换,新线程根据MarkWord中的ThreadId,通知之前的线程暂停,之前的线程将Markword的内容置为空

3)两个线程都将锁对象的HashCode复制到自己新建的用于存储锁的空间,接着开始通过 CAS 操作竞争锁(把锁对象的MarKword的内容修改为自己存储锁的地址)

4)竞争失败的则进入自旋 ,若自旋时成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则锁依然处于轻量级锁的状态。

5)若自旋失败 ,进入重量级锁的状态,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己

公平锁与非公平锁

公平锁:线程在获取锁时排队,按照先来后到

非公平锁:后来的锁会按情况进行插队(能提升一定的效率,但可能发生饥饿)

共享锁、独享锁(排他锁)

共享锁:同时可以被多个线程共享的锁

独享锁(排它锁):同时只能被一个线程占有

读写锁

读锁:属于共享锁,可以被多个线程共享,使得同个线程同时进行读操作

写锁:属于独享锁,一个线程占有写锁时其他所有的读线程和写线程均被阻塞

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇