Volatile Synchonized Reentranlock知识点

78 阅读7分钟

个人笔记

#juc# ​​​

volatile

共享变量 禁止指令重排序​ 内存屏障​ 轻量级锁

Java 线程内存模型确保所有线程看到这个变量的值是一致的

如何保持可见性?

底层的汇编代码会多出来 Lock 前缀 使得处理器缓存行的数据写到系统内存 其他 cpu 缓存的该内存地址的数据无效

如何保持一致性?

缓存一致性协议: 每个处理器通过嗅探总线传播的数据让检查自己的数据是否过期,过期的话每次被使用时会重新从系统内存读取到缓存

存在的问题

volatile 的原子性问题:volatile 不能保证原子性操作。 禁止指令重排序:volatile 可以防止指令重排序操作。

在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性

volatile 的使用优化思考

一个编程大师新增了一个队列集合 让 volatile 修饰的变量长度增加到了 64 字节,刚好是一个缓存行的长度, 使得每个处理器在处理缓存行时, 其中只有一条唯一的变量, 不影响其他处理器对自己数据的处理(因为如果不足一行,一行的开头如果是前面的尾节点,但一行被完全锁住,前面无法使用自己的尾结点)

避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。

  1. 不适用于非 64 字节的宽的处理器
  2. 共享变量不被频繁读写 因为本身追加了长度 如果不被频繁使用,得不偿失

此方法在 java7 不生效, java7 会淘汰或重新排列无用字段

synchonized

Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。

❑ 对于普通同步方法,锁是当前实例对象。

❑ 对于静态同步方法,锁是当前类的 Class 对象。

❑ 对于同步方法块,锁是 Synchonized 括号里配置的对象。

在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:

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

这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

偏向锁:

当一个锁的竞争较少的时候, 对一个经常使用的线程课引入偏向锁

对她很偏心, 所以检查的时候 只需简单测试 对象头的 mark word 是否是指向当前线程的偏向锁,成功即获得了锁
失败 再测一下偏向锁的标志是否为 1
是:将偏向锁指向当前线程
不是:使用 CAS 竞争

image.png

自旋锁:

在并发编程中,锁的自旋是指当一个线程尝试获取锁时,如果锁已经被其他线程持有,那么它将会进行一段时间的忙等待,不断地尝试获取锁。这个忙等待的过程称为自旋。自旋锁是利用了这种自旋的方式来减少线程阻塞所带来的开销。

在自旋锁的实现中,当一个线程发现锁已经被其他线程占用时,它会重复尝试获取锁,而不是立刻进入阻塞状态。由于自旋的时间很短,自旋锁可以快速地获得锁,从而避免了线程阻塞和恢复时所带来的上下文切换及调度器调度的开销,提高了系统的并发性能。

然而,如果锁竞争比较激烈,这种自旋的等待时间可能会很长,导致浪费 CPU 资源。因此,在实际应用中,通常会采用多种锁的组合使用策略,如自旋锁和互斥锁的组合使用,以便在不同的场景下选择合适的锁来达到最优的性能。

轻量级锁:

每次获取锁对象的时候 利用 CAS 尝试将 Mark Word 指向锁记录的指针

如果成功的话 其他线程再获取此对象的时候将使用自旋锁来获取

当其他线程使用自旋锁获得失败后,会产生锁膨胀, 使得失败线程阻塞,拥有锁对象的线程在释放锁时发现有人竞争,则升级为重量级锁,释放后唤醒被锁的失败线程

普通自旋锁: 每次自旋次数是固定的,只有超过这个次数之后,才升级为重量级锁
自适应自旋锁:每次自旋的次数不是固定的,是基于上一次抢占到锁自旋的次数,由 JVM 自适应的去调整的

重量级锁:

获取锁成功后,其他的线程不能通过自旋来获取,只能等待获取锁的对象来唤醒需要获取此对象的线程

当我们的 owner 释放锁时,会将 Xcq 里面的线程放到 EntryList 中
这个时候由 OnDeck Thread 去进行锁竞争,竞争失败的则继续留在 EntryList 中
当调用 Object.wait() 会进入 _WaitSet 队列,只要被唤醒时,才会重新进入 EntryList 中

  • 我们线程刚进来时,会进入 Cxq (竞争队列)的队列中
  • 当我们的 owner 释放锁时,会将 Xcq 里面的线程放到 EntryList 中
  • 这个时候由 OnDeck Thread 去进行锁竞争,竞争失败的则继续留在 EntryList 中
  • 当调用 Object.wait() 会进入 _WaitSet 队列,只要被唤醒时,才会重新进入 EntryList 中

image.png

image.png

image.png

原子操作

image.png

cas 原子操作的问题

  1. ABA 问题 数据由 A 到 B 再到 A 锁无法真正判断数据是否发生改变

使用版本号法,为每个数据提供一个版本号,版本号相同才可以改变数据

  1. 循环时间长开销大 自旋 cas 长时间不成功 会浪费资源
  2. 只能保证一个共享变量的原子操作

可以将多个共享变量合并为一个共享变量 比如 i=a 和 j=2 合并为 2a

​AtomicInteger​ 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset()​ 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

happens-before

as-ifserial 无论怎么重排序都不影响最终运行结果

image.png

实际 b 在 a 之前运行也不影响最终结果,所以 JMM 会允许这种指令重排序

公平锁和非公平锁

非公平锁 每次加锁时 直接通过 CAS 来判断 来尝试去获取锁

公平锁 每次加锁时 首先判断自己是否在队列的首部

AQS abstractQueueSynchronizer

ReentranLock

可重入锁 state字段记录重入次数,  每次进入加1 ,退出-1

基本的方法和实现: Lock  unLock  tryRelease

实现过程 加锁 释放 唤醒锁的过程

image.png

使用的数据结构:AbstractQueuedSynchronizer(AQS)内部的等待队列,它是基于单向链表的队列

如何实现:AQS 通过维护一个双向链表和一个等待队列

Thread.currentThread 作为唯一标识

如果使用一个普通的 LinkedList 来维护节点之间的关系,那么当一个线程获取了同步状态,而其他多个线程由于调用 tryAcquire(int arg)方获取同步状态失败而并发地被添加到 LinkedList 时,LinkedList 将难以保证 Node 的正确添加,最终的结果可能是节点的数量有偏差,而且顺序也是混乱的。

设计模式 模板模式