高并发专题 - Java内存模型
# 1. 什么是多线程安全
# 1.1 定义
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
# 1.2 出现线程安全问题的必要条件
- 多线程环境
- 多个线程操作共享数据
- 操作共享数据的语句不是原子性的
# 1.3 概括
在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写⼊入已经开始,但还没结束),其他线程对这个写了一半的资源进⾏了读操作,或者对这个写了一半的资源进⾏了写操作,导致此资源出现数据错误。
# 2. 线程安全如何解决
# 2.1 原则
- 保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性)。
- 将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)。
# 2.2 方式
# 2.2.1 Synchronized 关键字
同步方法或同步代码块。保证方法或代码块操作的原子性、保证监视资源(Monitor)的可见性、保证线程间操作的有序性。
# 2.2.2 Volatile 关键字
保证被 Volatile 关键字描述变量的操作具有可见性和有序性(禁止指令重排)
注意:
Volatile 只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值操作和对象的引⽤赋值操作有效。
对于 i++ 此类复合操作, Volatile 无法保证其有序性和原子性。
相对 Synchronized 来说 Volatile 更加轻量一些。
# 2.2.3 java.util.concurrent.atomic 包
java.util.concurrent.atomic 包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等类。使用这些类来声明变量可以保证对其操作具有原子性来保证线程安全。
实现原理上与 Synchronized 使用 Monitor(监视锁)保证资源在多线程环境下阻塞互斥访问不同,java.util.concurrent.atomic 包下的各原子类基于 CAS(CompareAndSwap) 操作原理实现。
CAS 又称无锁操作,一种乐观锁策略,原理就是多线程环境下各线程访问共享变量不会加锁阻塞排队,线程不会被挂起。通俗来讲就是一直循环对比,如果有访问冲突则重试,直到没有冲突为止。
# 2.2.4 Lock
Lock 也是 java.util.concurrent 包下的一个接口,定义了一系列的锁操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 实现类。与 Synchronized 不同是 Lock 提供了获取锁和释放锁等相关接口,使得使用上更加灵活,同时也可以做更加复杂的操作。
# 3. 同步代码块
- 静态同步代码块。使用 static 关键字修饰的代码块,synchronized 中的 Monitor 需要是类的字节码对象(XXX.class)
- 非静态同步代码块。未使用 static 关键子修饰的代码块,synchronized 中的 Monitor 是共享的数据对象
# 4. 多线程死锁
# 4.1 出现的原因
- 当前线程拥有其他线程需要的资源
- 当前线程等待其他线程已拥有的资源
- 都不放弃自己拥有的资源
# 4.2 产生死锁的必要条件
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。
# 4.3 如何解决死锁
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
# 4.4 概括
- 线程之间交错执行
- 解决:以固定的顺序加锁
- 执行某方法时就需要持有锁,且不释放
- 解决:缩减同步代码块范围,最好仅操作共享变量时才加锁
- 永久等待
- 解决:使用tryLock()定时锁,超过时限则返回错误信息
# 5. ThreadLocal
# 5.1 定义
ThreadLocal 是 JDK底层提供的一个解决多线程并发问题的工具类,它为每个线程提供了一个本地的副本变量机制,实现了和其它线程隔离,并且这种变量只在本线程的生命周期内起作用,可以减少同一个线程内多个方法之间的公共变量传递的复杂度。
# 5.2 ThreadLocal 和 synchronized 的区别
ThreadLocal 和 synchronized 都是用来处理多线程环境下并发访问变量的问题,只是二者处理的角度不同、思路不同。 ThreadLocal 是一个类,通过对当前线程中的局部变量操作来解决不同线程的变量访问的冲突问题。所以 ThreadLocal 提供了线程安全的共享对象机制,每个线程都拥有其副本。 Java中的 synchronized 是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量时多个线程共享的。 同步机制(synchronized 关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而 ThreadLocal 采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
# 5.3 原理总结
- 每个 Thread 维护着一个 ThreadLocalMap 的引用
- ThreadLocalMap 是 ThreadLocal 的内部类,用Entry来进行存储
- 调用 ThreadLocal 的 set() 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象,值是传递进来的对象
- 调用 ThreadLocal 的 get() 方法时,实际上就是往 ThreadLocalMap 获取值,key 是 ThreadLocal 对象
- ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
# 6. Java 内存模型
多核CPU处理内存数据时会将内存数据缓存到CPU的高速缓存中,多核同时处理统一数据会出现 数据不一致 的问题。
解决方案:
- BUS总线锁
- 缓存一致性协议 (MESI 协议)
# 6.1 并发内存模型的实质
Java内存模型围绕着并发过程中如何处理原子性、可见性和顺序性这三个特征来设计的。
# 原子性(Automicity)
由Java内存模型来直接保证原子性的变量操作包括read、load、use、assign、store、write这6个动作,虽然存在long和double的特例,但基本可以忽律不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock和unlock也可以满足需求。lock和unlock虽然没有被虚拟机直接开给用户使用,但是提供了字节码层次的指令monitorenter和monitorexit对应这两个操作,对应到java代码就是synchronized关键字,因此在synchronized块之间的代码都具有原子性。
# 可见性
可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。 除了volatile,synchronized和final也可以实现可见性。synchronized关键字是通过unlock之前必须把变量同步回主内存来实现的,final则是在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。
# 有序性
有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句值得是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。 保证有序性的关键字有volatile和synchronized,volatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作”来保证。
总体来看,synchronized对三种特性都有支持,虽然简单,但是如果无控制的滥用对性能就会产生较大影响。
# JVM对Java原生锁的优化
JDK提供了三种不同的 Monitor 实现,也就是三种不同的锁:
- 偏向锁(biased locking)
- 轻量级锁
- 重量级锁
这三种锁使得JDK得以优化 Synchronized 的运行。当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的『升级』和『降级』。
- 当没有竞争出现时,默认使用『偏向锁』
- 如果有另一个线程试图锁定某个被『偏向锁』锁过的对象,JVM就撤销『偏向锁』,切换为『轻量级锁』。
- 『轻量级锁』依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,则使用普通的『轻量级锁』,否则,升级为『重量级锁』。
# CAS
# 同步器
JUC 中的同步器主要有三个: CountDownLatch、CyclicBarrier 和 Semaphore。通过它们可以方便的实现很多线程之间的协作。
# CountDownLatch
TODO
# CyclicBarrier
TODO