多线程进阶(六)----锁机制

多线程进阶(六)----锁机制,第1张

多线程基础概念:多线程入门(一)
多线程基础实现:多线程入门(二)
多线程管理:多线程基础(三)
线程间的状态转换:多线程基础(四)
线程间的通信:多线程进阶(五)

这篇文章,我们先聊聊java多线程里的"锁"。

首先明确一点:Java多线程的锁都是基于对象的,Java中的每⼀个对象都可以作为⼀个锁

多线程进阶(六)锁机制
  • 1、synchronized
    • 1.1 作用域
    • 1.2 几种锁
    • 1.3 偏向锁
      • 1.3.1 原理
      • 1.3.2 升级过程
    • 1.4 轻量级锁
      • 1.4.1 原理
    • 1.5 重量级锁
    • 1.6 各种锁对比
    • 1.7 总结锁的升级流程

1、synchronized

synchronized中文意思是同步,也称之为”同步锁“。作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。

1.1 作用域

我们通常使⽤ synchronized 关键字来给⼀段代码或⼀个⽅法上锁。它通常有以下三种形式:

// 关键字在实例⽅法上,锁为当前实例
public synchronized void instanceLock() {
 // code
}
// 关键字在静态⽅法上,锁为当前Class对象
public static synchronized void classLock() {
 // code
}
// 关键字在代码块上,锁为括号⾥⾯的对象
public void blockLock() {
 Object o = new Object();
 synchronized (o) {
 // code
 }
}
1.2 几种锁

一个对象其实有四种锁状态,它们有低到高,依次是:

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

几种锁,会伴随着竞争情况逐渐升级,锁的升级很容易发生,但是降级发生的条件却十分的苛刻。
下面分别介绍几种锁以及它们之间的锁升级:

1.3 偏向锁

⼤多数情况下锁不仅不存在多线程竞争,⽽且总是由同⼀线程多次获得,于是引⼊了偏向锁。
偏向锁会偏向第一个访问锁的线程,如果在接下来运行过程中,没有别的线程再去竞争该锁,那么持有该锁的线程将永远不会触发同步,提高了程序的运行性能。

1.3.1 原理

原理:当线程第一次进入同步代码块时,会在对象头和栈帧中的锁记录⾥存储锁的偏向的线程ID。当下一次该线程再次进入该同步代码块时,会去对象头的mark word里查看标记,是否存在自己线程的线程ID。
如果是,则表明该线程已经获得了锁,进入同步代码块,不再需要花费CAS *** 作来加锁解锁。
如果不是,代表是另一个线程来竞争偏向锁,这时候CAS会尝试替换mark word的线程ID为新线程的线程ID。

  • 成功,表示老线程ID不存在了,替换成功,这时候还是偏向锁。
  • 失败,表示老线程ID依然存在,这时候会升级为轻量级锁,会按照轻量级锁的方式去竞争锁。
1.3.2 升级过程

偏向锁升级轻量级锁时,会暂停使用该锁的线程,重置偏向锁标识,大概过程如下:

  • 系统会先找一个没有字节码在执行的时间点,,然后停止拥有该锁的线程;
  • 遍历线程栈,修复或修改mark word,使其变为无锁的状态;
  • 唤醒被停止的线程,使其变成轻量级锁;

所以,如果应⽤程序⾥所有的锁通常出于竞争状态,那么偏向锁就会是⼀种累赘, 我们可以对其默认关闭。

-XX:UseBiasedLocking=false
1.4 轻量级锁

多个线程在不同的时段获取同一把锁,不存在锁竞争的情况。

1.4.1 原理

如果一个线程在获得锁的时候发现是轻量级锁,就会把锁的mark word复制到dispace mark word里面。然后尝试使用CAS将锁的mark word替换为指向锁记录的指针。

  • 成功,当前线程获得锁;
  • 失败,表名mark word 已经被替换成了其他线程的锁记录,接下来会尝试自旋来获取锁,如果一直获取不到,则升级为重量级锁。

一张图说明加锁释放锁的过程:

1.5 重量级锁

重量级锁依赖于 *** 作系统的互斥量(mutex) 实现的,⽽ *** 作系统中线程间状态的转换需要相对⽐较⻓的时间,所以重量级锁效率很低。
当多个线程同时请求某个对象锁时,对象锁会设置⼏种状态⽤来区分请求的线程:

  • Contention List:所有请求锁的线程将被⾸先放置到该竞争队列
  • Entry List:Contention List中那些有资格成为候选⼈的线程被移到Entry List
  • Wait Set:那些调⽤wait⽅法被阻塞的线程被放置到Wait Set
  • OnDeck:任何时刻最多只能有⼀个线程正在竞争锁,该线程称为OnDeck
  • Owner:获得锁的线程称为Owner
  • !Owner:释放锁的线程
    当线程释放锁时,会从Contention List或EntryList中挑选⼀个线程唤醒,被选中的线程叫做 Heir presumptive 即假定继承⼈,假定继承⼈被唤醒后会尝试获得锁,但 synchronized 是⾮公平的,所以假定继承⼈不⼀定能获得锁。 这是因为对于重量级锁,线程先⾃旋尝试获得锁,这样做的⽬的是为了减少执⾏ *** 作系统同步 *** 作带来的开销。如果⾃旋不成功再进⼊等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有⼀个不公平的地⽅是⾃旋线程可能会抢占了Ready线程的锁。
1.6 各种锁对比

1.7 总结锁的升级流程
  1. 每⼀个线程在准备获取共享资源时: 第⼀步,检查MarkWord⾥⾯是不是放的⾃⼰ 的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
  2. 第⼆步,如果MarkWord不是⾃⼰的ThreadId,锁升级,这时候,⽤CAS来执⾏切换,新的线程根据MarkWord⾥⾯现有的ThreadId,通知之前线程暂停,之前线程 将Markword的内容置为空。
  3. 第三步,两个线程都把锁对象的HashCode复制到⾃⼰新建的⽤于存储锁的记录空 间,接着开始通过CAS *** 作, 把锁对象的MarKword的内容修改为⾃⼰新建的记录 空间的地址的⽅式竞争MarkWord。
  4. 第四步,第三步中成功执⾏CAS的获得资源,失败的则进⼊⾃旋 。
  5. 第五步,⾃旋的线程在⾃旋过程中,成功获得资源(即之前获的资源的线程执⾏完 成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果⾃旋失败 。
  6. 第六步,进⼊重量级锁的状态,这个时候,⾃旋的线程进⾏阻塞,等待之前线程执 ⾏完成并唤醒⾃⼰。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/721585.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存