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
包中的原子类,如AtomicInteger
、AtomicLong
等,用于实现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时间浪费在忙等待上,这种情况下,自旋锁的效率可能会下降。
自旋锁适用于以下场景:
-
短期的临界区:当临界区的代码执行时间很短,锁占用时间很短,自旋锁可以避免线程切换的开销。
-
多处理器系统:在多处理器系统中,线程可能在其他处理器上执行,此时忙等待并不会占用全部的CPU资源,因为其他线程仍然可以在其他处理器上执行。
-
锁竞争不激烈:如果竞争锁的线程较少,自旋锁的效果可能比较好。
值得注意的是,自旋锁的效率和适用场景取决于多个因素,如锁占用时间、线程数量、处理器核心数等。在实际应用中,需要根据具体情况选择是否使用自旋锁,以及自旋的次数和策略。在某些情况下,自旋锁也可能会退化为传统的阻塞锁。
偏向锁(Biased Locking)是Java虚拟机(JVM)中一种针对低竞争情况下的锁优化机制。它旨在降低没有竞争的情况下使用锁的开销。在某些情况下,锁在对象创建后一段时间内只会被一个线程获取和释放,此时偏向锁能够有效地加速同步操作。
偏向锁的基本思想是:当一个线程获取了锁之后,JVM会在对象头中的"mark word"中存储该线程的标识。之后,如果同一个线程再次请求该对象的锁,JVM会直接检查对象头中的标识,如果匹配成功,表示线程已经偏向于该对象,无需竞争锁,可以直接进入临界区。这样可以避免了额外的同步操作和锁竞争,从而提高了单线程下的执行效率。
如果其他线程尝试获取同一个对象的锁,偏向锁会被撤销,锁升级为轻量级锁或重量级锁,具体取决于竞争情况。撤销偏向锁的过程称为偏向锁的撤销。
偏向锁适用于以下情况:
-
多线程环境下,对象在大部分时间只被一个线程访问,出现了热点对象(Hot Spot)。
-
对象的锁在初始化后,只有少数几次需要竞争的情况。
需要注意的是,偏向锁并不适用于所有场景,如果对象的竞争非常频繁,那么偏向锁可能会带来额外的性能开销。JVM中的偏向锁启用和撤销都需要进行特定的条件判断和CAS(Compare and Swap)操作,这些操作本身也会带来一定的开销。在实际应用中,可以通过JVM参数来控制是否启用偏向锁以及偏向锁的撤销策略,以根据具体场景来优化锁的性能。