多线程--10--偏向锁、轻量级锁、重量级锁、volatile

基础知识

锁从宏观上分类,分为悲观锁与乐观锁。

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁

但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。

java中的悲观锁就是Synchronized AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

java线程阻塞的代价

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源.

因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

  1. 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
  2. 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,

  • JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁, 他们都属于乐观锁。

对象结构

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:

  • 对象头(Header)、
  • 实例数据(Instance Data)
  • 对齐填充(Padding)。

下图是普通对象实例与数组对象实例的数据结构:

在这里插入图片描述

对象头

HotSpot虚拟机的对象头包括两部分信息:

  1. markword
    第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。
  2. klass
    对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
    数组长度(只有数组对象有)

如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

在这里插入图片描述

markword

markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:

在这里插入图片描述
32位虚拟机在不同状态下markword结构如下图所示:

在这里插入图片描述

小结

前面提到了java的4种锁,他们分别是重量级锁、自旋锁、轻量级锁和偏向锁,
不同的锁有不同特点,每种锁只有在其特定的场景下,才会有出色的表现,java中没有哪种锁能够在所有情况下都能有出色的效率,引入这么多锁的原因就是为了应对不同的情况;

前面讲到了重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁,所以现在你就能够大致理解了他们的适用范围,但是具体如何使用这几种锁呢,就要看后面的具体分析他们的特性;

偏向锁

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。

如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

在这里插入图片描述

偏向锁与轻量级锁理念上的区别:

  • 轻量级锁:
    在无竞争的情况下使用CAS操作去消除同步使用的互斥量
  • 偏向锁:
    在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了

意义:锁偏向于第一个获得它的线程。如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

在这里插入图片描述

轻量级锁

CAS- -比 较 并 交 换

  • 内 存 值
  • 预 期 值
  • 新 值

乐 观 锁 的 核 心 算 法 是 CAS( Compareand Swap, 比 较 并 交 换 ) , 它 涉
及 到 三 个 操 作 数 : 内 存 值 、 预 期 值 、 新 值 。 当 且 仅 当 预 期 值 和 内 存 值 相 等 时 才 将 内 存 值 修 改 为 新 值 。

这 样 处 理 的 逻 辑

  1. 首 先 检 查 某 块 内 存 的 值 是 否 跟 之 前 我 读 取 时 的 一 样

  2. 如 不 一 样 则 表 示 期 间 此 内 存 值 已 经 被 别 的 线 程 更 改 过 , 舍 弃 本 次 操 作 ,自旋

  3. 否 则 说 明 期 间 没 有 其 他 线 程 对 此 内 存 值 操 作 , 可 以 把 新 值 设 置 给 此 块 内 存 。

重量级锁

Synchronized 原理

Synchronized 是 由 JVM 实 现 的 一 种 实 现 互 斥 同 步 的 一 种 方 式 , 如 果 你 查 看 被 Synchronized 修 饰 过 的 程 序 块 编 译 后 的 字 节 码 , 会 发 现 , 被 Synchronized 修 饰 过 的 程 序 块 , 在 编 译 前 后 被 编 译 器 生 成 了 monitorenter 和 monitorexit 两 个 字 节 码 指 令 。

  • monitorenter
  • monitorexit

两 个 指 令 是 什 么 意 思 呢 ?

在 虚 拟 机 执 行 到 monitorenter 指 令 时 , 首 先 要 尝 试 获 取 对 象 的 锁 :
如 果 这 个 对 象 没 有 锁 定 , 或 者 当 前 线 程 已 经 拥 有 了 这 个 对 象 的 锁 , 把 锁 的 计 数 器 +1; 当 执 行 monitorexit 指 令 时 将 锁 计 数 器 -1; 当 计 数 器 为 0 时 , 锁 就 被 释 放 了 。

如 果 获 取 对 象 失 败 了 , 那 当 前 线 程 就 要 阻 塞 等 待 , 直 到 对 象 锁 被 另 外 一 个 线 程 释 放 为 止 。 Java 中 Synchronize 通 过 在 对 象 头 设 置 标 记 , 达 到 了 获 取 锁 和 释 放 锁 的 目 的 。

在这里插入图片描述

volatile

关 键 字 volatile 是 Java 虚 拟 机 提 供 的 最 轻 量 级 的 同 步 机 制 。 当
一 个 变 量 被 定 义 成 volatile 之 后 , 具 备 两 种 特 性 :

  1. 保 证 此 变 量 对 所 有 线 程 的 可 见 性 。 当 一 条 线 程 修 改 了 这 个 变 量 的 值 , 新 值 对 于 其 他 线 程 是 可 以 立 即 得 知 的 。 而 普 通 变 量 做 不 到 这 一 点 。

  2. 禁 止 指 令 重 排 序 优 化 。 普 通 变 量 仅 仅 能 保 证 在 该 方 法 执 行 过 程 中 , 得 到 正 确 结 果 , 但 是 不 保 证 程 序 代 码 的 执 行 顺 序 。

ConcurrentHashMap
在这里插入图片描述

volatile 对 比 Synchronized 的 异 同

在这里插入图片描述

Synchronized 和lock的区别?

在这里插入图片描述
在这里插入图片描述

锁的升级(偏向锁->轻量级锁->重量级锁)

前面提到过, synchronized 代码块是由一对 monitorenter/moniterexit 字节码指令实现, monitor 是其同步实现的基础, Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制

  • 偏向锁(Biased Lock )
  • 轻量级锁( Lightweight Lock)
  • 重量级锁(Heavyweight Lock)

锁机制的切换是根据竞争激烈程度进行的

  1. 在几乎无竞争的条件下, 会使用偏向锁
  2. 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁,
  3. 在重度竞争的情况下, 会升级到重量级锁。

在这里插入图片描述
synchronized的执行过程:

  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  6. 如果自旋成功则依然处于轻量级状态。
  7. 如果自旋失败,则升级为重量级锁。

synchronized的锁状态变化?

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁?是这么一步一步走过来的么?

其实状态不是这样一点一点来的。

无锁无法到偏向锁

image.png
在这里插入图片描述

这里整体是一个锁升级的过程,那存在锁降级么?

偏向锁是可以到无锁状态的,偏向锁没有地儿存储hashcode之类的值,为了存储,要么升级到轻量级锁,存储到LockRecord里,要么降级为无锁,存在MarkWord里。

锁优化

减少锁的时间

不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放

减少锁的粒度

它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;

java中很多数据结构都是采用这种方法提高并发操作的效率:

ConcurrentHashMap 1.7 Segment
java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组

Segment< K,V >[] segments

使用读写锁

读写分离
CopyOnWriteArrayList 、CopyOnWriteArraySet

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWrite并发容器用于读多写少的并发场景,因为,读的时候没有锁,但是对其进行更改的时候是会加锁的,否则会导致多个线程同时复制出多个副本,各自修改各自的;

使用cas 乐观锁

如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择;

ConcurrentHashMap 1.8 CAS

尽量使用Java .util .concurrent线程安全的并发类 来代替

  • concurrentHashmap

避免死锁:

1.加锁顺序:
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁
2.加锁时限:
在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。
3.死锁检测:
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

那么当检测出死锁时,这些线程该做些什么呢?

  1. 一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。
  2. 一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的使用轻量级的示例代码: ```java public class LightLockExample { private int count; private volatile Object lock = new Object(); public void increment() { synchronized (lock) { count++; } } public void incrementWithLightLock() { // 尝试获取轻量级 if (compareAndSetLock()) { try { count++; } finally { // 释放轻量级 unlock(); } } else { // 获取轻量级失败,使用重量级 synchronized (lock) { count++; } } } private boolean compareAndSetLock() { // 使用CAS操作尝试获取轻量级 return unsafe.compareAndSwapObject(this, lockOffset, null, Thread.currentThread()); } private void unlock() { // 将的持有者设置为null,释放轻量级 unsafe.putObjectVolatile(this, lockOffset, null); } private static final sun.misc.Unsafe unsafe = sun.misc.Unsafe.getUnsafe(); private static final long lockOffset; static { try { lockOffset = unsafe.objectFieldOffset(LightLockExample.class.getDeclaredField("lock")); } catch (Exception ex) { throw new Error(ex); } } } ``` 该示例中,使用了一个Object类型的对象lock来保证线程安全。在incrementWithLightLock方法中,首先尝试使用compareAndSetLock方法获取轻量级,如果成功,则执行count++操作,并在finally块中释放轻量级;如果失败,则使用重量级来保证线程安全。 compareAndSetLock方法使用了Unsafe类的compareAndSwapObject方法来进行CAS操作,尝试将lock对象的值从null修改为当前线程,如果修改成功,则获取轻量级;如果修改失败,则说明其他线程已经获取了轻量级,当前线程需要使用重量级来保证线程安全。 在unlock方法中,使用了Unsafe类的putObjectVolatile方法来将的持有者设置为null,释放轻量级

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值