JMM内存模型
并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。所以 Java 需要开发自己的 Memory Model,来实现多线程和并发的编程,同时Java跨平台的特性决定了,他需要屏蔽底层OS的MM。
抽象线程和主内存之间的关系
主内存
所有的线程创建的所有实例对象都存放在主内存中,成员变量、局部变量、类信息、常量、静态变量全部都存放在主内存中。
为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
本地内存
每个线程都有一个私有的本地内存,存储了该线程读写共享变量的副本,每个线程都只能操作自己本地内存中的变量,无法访问其他线程的本地内存。
如果线程间需要通信,需要通过主内存进行。这是一个抽象概念。
1、线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
2、线程 2 到主存中读取对应的共享变量的值。
happens-before
设计思想:
- 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
- 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。
定义:
- 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序。
总结:happens-before 表示了钱一个操作对于后一个操作而言是可见的,无论两个操作是否在同一个线程中。
如果操作不满足 happens-before 的规则,那么两个操作没有顺序,JVM可以进行重排序
并发编程三大特性
原子性
一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取
有序性
由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序
在 Java 中,volatile
关键字可以禁止指令进行重排序优化
volatile关键字⭐
volatile
关键字可以保证变量的可见性(不可保证数据的原子性),如果将变量声明为这个关键字,就指示JVM着个变量是共享且不稳定的,每次都要到主存中去读取。
volatile不保证原子性
要使用利用 synchronized
、Lock
或者AtomicInteger
来将非原子操作组合成一个原子操作。
乐观锁和悲观锁
悲观锁
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。例如:synchronized
和 ReentrantLock
高并发的场景下,大量的阻塞线程会导致系统的上下文切换,增加系统开销甚至有可能死锁。
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
}
}
private Lock lock = new ReentrantLock();
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}
**加粗样式**```
### 乐观锁
只是在**提交修改的时候去验证对应的资源**(也就是数据)是否被其它线程修改了(具体方法可以使用**版本号机制**或 **CAS 算法**)
例如:java.util.concurrent.atomic包下面的**原子变量类**
高并发场景下,不会有锁阻塞线程的情况,不会死锁但是**写占比很多的情况下,会频繁失败和重试**,十分影响性能。