并发编程探寻之路 - 共享模型值之内存无锁工具

5.共享模型之内存

Monitor主要关注的是访问共享变量时,保证临界区代码的原子性

这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题

5.1 Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。

JMM 体现在以下几个方面

  • 原子性 -保证指令不会受到线程上下文切换的影响

  • 可见性 -保证指令不会受cpu缓存的影响

  • 有序性 -保证指令不会受cpu指令并行优化的影响

5.2 可见性

解决办法

volatile(易变关键字)

它可以用来修饰成员变量静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存

可见性 vs 原子性 前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可 见, 不能保证原子性,仅用在一个写线程,多个读线程的情况

注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低

5.3 有序性

指令重排

解决方法 volatile 修饰的变量,可以禁用指令重排

volatile原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障

  • 对 volatile 变量的读指令前会加入读屏障

1.如何保证可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

2.如何保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

还是那句话,不能解决指令交错: 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去 而有序性的保证也只是保证了本线程内相关代码不被重排序

6.共享模型之无锁

CAS:

是"Compare and Swap"(比较并交换)的缩写,是一种用于实现多线程并发操作的技术。它是一种非阻塞算法,用于解决并发环境下的共享数据访问问题。

在并发编程中,当多个线程同时对同一个共享变量进行读写操作时,可能会出现数据不一致的问题。传统的加锁机制可以保证数据的一致性,但会引入阻塞和上下文切换的开销。

CAS通过使用原子操作来解决这个问题。它由三个基本操作组成:比较内存值、比较与预期值是否相等,如果相等则交换新值到内存位置。这个过程是原子的,不会被其他线程打断。如果在执行交换操作之前,其他线程修改了共享变量的值,CAS操作会失败,需要重新尝试。

CAS的优势在于它避免了传统锁的开销,并且不会引起线程的阻塞和上下文切换。因此,它对于高并发系统的性能有很大的提升。然而,CAS也存在ABA问题,即当共享变量的值从A变为B,再变回A时,CAS无法感知到这个变化,可能导致数据不一致。为了解决ABA问题,可以使用版本号或者标记来辅助CAS操作。

Java提供了java.util.concurrent.atomic包中的原子类,如AtomicIntegerAtomicLong等,用于实现CAS操作,简化了在并发编程中的使用。

6.2 CAS与volatile

volatile 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时 候,发生上下文切换,进入阻塞。

  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还 是会导致上下文切换。

6.3 原子整数

J.U.C 并发包提供了: AtomicBoolean AtomicInteger AtomicLong

6.4 原子引用

AtomicReference AtomicMarkableReference AtomicStampedReference

ABA问题及解决

6.5 原子数组
6.6 字段更新器
6.7 原子累加器

源码之 LongAdder

原理之伪共享

8.共享模型之工具

8.1 线程池

1.自定义线程池

8.2 JUC
AQS原理

1.概述

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

  • 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁

    • getState - 获取 state 状态

    • setState - 设置 state 状态

    • compareAndSetState - cas 机制设置 state 状态

    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList

  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

ReentrantLock 原理

非公平锁(从构造器看)

public ReentrantLock() { sync = new NonfairSync(); }

子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)

  • tryAcquire

  • tryRelease

  • tryAcquireShared

  • tryReleaseShared

  • isHeldExclusively

AQS要实现的功能目标

  • 阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire

  • 获取锁超时机制

  • 通过打断取消机制

  • 独占机制及共享机制

  • 条件不满足时的等待机制

独占锁:一个锁只能被一个线程所获取,其它线程必须等待该线程释放后才可获取

需要注意的是,不可重入锁和独占锁并不是互斥的关系。一个锁既可以是不可重入的,也可以是独占的。例如,Java中的ReentrantLock就是可重入的独占锁。

synchronized原理

对象头mark word中包含:对象的标识位、对象的哈希码、锁状态信息。

当一个线程尝试获取锁的时候会对象头中锁状态的信息,如果对象被锁定,则该线程需要等待,知道锁被释放。如果该对象没有被锁定,线程就可以成功获得锁,并把对象头中的mark word修改为自己拥有锁的状态。

自旋锁是一种基于忙等待(busy-waiting)的并发控制机制。在多线程编程中,当一个线程尝试获取某个共享资源的锁时,如果锁已经被其他线程占用,传统的做法是让线程进入阻塞状态等待锁的释放。与之不同,自旋锁是一种乐观的锁策略,它不会让线程阻塞,而是采用忙等待的方式,在获取锁的时候不断地进行自旋(循环检测锁状态),直到锁被释放为止。

自旋锁的基本思想是,如果共享资源的锁已经被其他线程占用,当前线程就不断地检测锁状态,而不是立即放弃 CPU 控制权或进入休眠状态。这样做的好处在于,如果资源被占用的时间很短,自旋锁可以避免线程切换的开销,从而提高并发性能。然而,如果资源占用的时间较长,自旋锁可能会导致大量的CPU时间浪费在忙等待上,这种情况下,自旋锁的效率可能会下降。

自旋锁适用于以下场景:

  1. 短期的临界区:当临界区的代码执行时间很短,锁占用时间很短,自旋锁可以避免线程切换的开销。

  2. 多处理器系统:在多处理器系统中,线程可能在其他处理器上执行,此时忙等待并不会占用全部的CPU资源,因为其他线程仍然可以在其他处理器上执行。

  3. 锁竞争不激烈:如果竞争锁的线程较少,自旋锁的效果可能比较好。

值得注意的是,自旋锁的效率和适用场景取决于多个因素,如锁占用时间、线程数量、处理器核心数等。在实际应用中,需要根据具体情况选择是否使用自旋锁,以及自旋的次数和策略。在某些情况下,自旋锁也可能会退化为传统的阻塞锁。

偏向锁(Biased Locking)是Java虚拟机(JVM)中一种针对低竞争情况下的锁优化机制。它旨在降低没有竞争的情况下使用锁的开销。在某些情况下,锁在对象创建后一段时间内只会被一个线程获取和释放,此时偏向锁能够有效地加速同步操作。

偏向锁的基本思想是:当一个线程获取了锁之后,JVM会在对象头中的"mark word"中存储该线程的标识。之后,如果同一个线程再次请求该对象的锁,JVM会直接检查对象头中的标识,如果匹配成功,表示线程已经偏向于该对象,无需竞争锁,可以直接进入临界区。这样可以避免了额外的同步操作和锁竞争,从而提高了单线程下的执行效率。

如果其他线程尝试获取同一个对象的锁,偏向锁会被撤销,锁升级为轻量级锁或重量级锁,具体取决于竞争情况。撤销偏向锁的过程称为偏向锁的撤销。

偏向锁适用于以下情况:

  1. 多线程环境下,对象在大部分时间只被一个线程访问,出现了热点对象(Hot Spot)。

  2. 对象的锁在初始化后,只有少数几次需要竞争的情况。

需要注意的是,偏向锁并不适用于所有场景,如果对象的竞争非常频繁,那么偏向锁可能会带来额外的性能开销。JVM中的偏向锁启用和撤销都需要进行特定的条件判断和CAS(Compare and Swap)操作,这些操作本身也会带来一定的开销。在实际应用中,可以通过JVM参数来控制是否启用偏向锁以及偏向锁的撤销策略,以根据具体场景来优化锁的性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值