Java 内存模型
多线程风险
在 Java 程序中,存储数据的内存空间分为共享内存和本地内存。线程在读写主存的共享变量时,会先将该变量拷贝一份副本到自己的本地内存,然后在自己的本地内存中对该变量进行操作,完成操作之后再将结果同步至主内存。
类型 | 存储介质 | 数据 | 特征 |
---|---|---|---|
共享内存 | 主内存 | 存放变量 | 多线程共享 |
本地内存 | CPU 高速缓存、缓冲区、寄存器以及其它硬件优化 | 临时存放线程使用的变量副本 | 使用期间其它线程无法访问 |
- 优势:由于 CPU 执行速度明先快于内存读写速度,将运算需要的数据拷贝到 CPU 高速缓存中运算,可以大大加快程序运行速度。
- 劣势:主内存数据和本地内存的不同步,导致多个线程同时操作主内存里的同一个变量时,变量数据可能会遭到破坏。
public class ThreadDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
class MyThread implements Runnable {
private int x = 0; // 对象中的数据由线程共享
@Override
public void run() {
for (int i = 0; i < 10000; i ) {
x ;
}
System.out.println("final x: " x); // 最后输出的数据不一定为 20000
}
}Copy to clipboardErrorCopied
行为规范
JMM 定义了共享内存系统中多线程程序读写操作行为的规范,用来保证共享内存的原子性、可见性、有序性。
原子性
原子性是指一个操作,要么全部执行并且执行过程不会被打断,要么就都不执行。
- Java 语言本身只保证了基本类型变量的读取和赋值是原子性操作。
- 简单操作的原子性可以通过 Atomic 原子类实现。
- 通过 synchronized 和 ReenTrantLock 等锁结构可以保证更大范围的原子性。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- Java 语言会尽可能保证主内存数据和本地内存同步,但仍可能出现不可见问题。
- 通常用 volatile 关键字来保证可见性。
- 通过 synchronized 和 ReenTrantLock 等锁结构在释放锁之前会将对变量的修改刷新到主存当中,也能够保证可见性。
有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。
- Java 内存模型具备先天的有序性。但 Java 允许编译器和处理器对指令进行重排序,可能影响多线程并发执行时的有序性。
- 通过 synchronized 和 ReenTrantLock 等锁结构可以保证有序性。
- volatile 关键字可以禁止 JVM 的指令重排,也可以保证有序性。
线程锁
互斥锁和自旋锁
- 互斥锁
阻塞锁。当线程需要获取的锁已经被其他线程占用时,该线程会被直接挂起。直到其他线程释放锁,由操作系统激活线程。
适用于锁使用者保持锁时间比较长的情况,线程挂起后不再消耗 CPU 资源。
- 自旋锁
非阻塞锁。当线程需要获取的锁已经被其他线程占用时,该线程会不断地消耗 CPU 的时间去试图获取锁。
适用于锁使用者保持锁时间比较短的情况,没有用户态和内核态调度、上下文切换的开销和损耗。
悲观锁和乐观锁
- 悲观锁
每次读写资源时都会给资源上锁,其他线程想获取该资源时会被阻塞,直到其释放锁。
适用于写频繁的应用场景,写资源请求不会被一直驳回。synchronized 和 ReentrantLock 等独占锁都是悲观锁。
- 乐观锁
读资源时不会给资源上锁,多个线程可以同时读取资源。写资源时会比对数据检查其他线程有没有更新过该资源,如果未更新就写入资源并更新版本号,否则写资源请求被驳回,重新读取并写资源。
适用于读频繁的应用场景,多线程同时读取能有效提高吞吐量。CAS 算法和版本号机制都是乐观锁,悲观锁的抢占也会利用 CAS 算法。
公平锁和非公平锁
- 公平锁
加入到队列中等待唤醒,先到者先拿到锁。
公平锁不会出现线程饥饿,迟迟无法获取锁的情况。ReentrantLock 可以实现公平锁。
- 非公平锁
当线程要获取锁时通过两次 CAS 操作去抢锁,如果没抢到加入到队列中等待唤醒。
非公平锁的性能更好。synchronized 是非公平锁,ReentrantLock 默认情况下也是非公平锁。
可重入锁
允许一个线程对同一对象多次上锁。由 JVM 记录对象被线程加锁次数,只有当线程释放掉所有锁(加锁次数为0)时,其他线程才获准进入。
synchronized 和 ReentrantLock 等锁结构都是可重入锁。