1. 内存模型概念
(1)内存模型(Java Memory Model)和内存结构(堆栈那些)不是一个层面的概念,JMM 定义了一套在多线程读写共享数据(成员变量,静态变量等,而不是局部变量这种线程私有的)时,对数据的可见性、有序性、和原子性的规则和保障。
(2)JMM规定了所有的变量都存储在主内存(虚拟机内存的一部分,但可以和操作系统的类比)中;
每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量;
不同的线程之间也无法直接访问对方的工作内存中的变量,线程间变量的值传递均需要通过主内存来完成。
(3)如果非要把主内存工作内存,和内存结构的堆栈方法区对应,那么主内存应该对应堆中的对象的实例数据部分,而工作内存对应栈。
2. 原子性
要执行就执行完,不能执行一半
违背原子性例子:两个线程对一个静态变量 i=0 执行 i++ 和 i--(每个对应四条JVM字节码指令),可能导致结果不是0
解决:用synchronized加锁,加锁位置应尽量减少代码获取释放锁的次数
synchronized( 对象 ) { 要作为原子操作代码 }
3. 可见性
指一个对象能看到或访问另一个对象的能力
违背可见性的例子:t线程看不到主线程run变量的改变
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while(run){ // .... } }); t.start(); Thread.sleep(1000); run = false; // 线程t不会如预想的停下来 }
解决:volatile(易变关键字),它可以用来修饰成员变量和静态成员变量,线程操作 volatile 变量都是直接操作主存
因此上面的run被修改后会存到主内存,t线程访问主内存内容会看到改变
4. 有序性
代码按顺序执行
违背有序性的例子:由于即时编译器在运行时会有指令重排的优化,多线程情况下可能出现非预期结果
解决:volatile 修饰的变量,可以禁用指令重排
因此volatile可以保证可见性和有序性,不能保证原子性,但属于轻量级并发控制;而synchronized可以保证三者,但更重量级
5. CAS
CAS 即 Compare and Swap ,它的实现用的是乐观锁的思想
// 需要不断尝试 while(true) { int 旧值 = 共享变量 ; // 比如拿到了当前值 0 int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1 if( compareAndSwap ( 旧值, 结果 )) { // 成功,退出循环 } }
- 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰
- 结合 CAS 配合 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下
- 因为没有使用 synchronized(悲观锁),所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
- CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
- 原子操作类例如:AtomicInteger、AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的