一、 JMM(Java 内存模型)

JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

特点:

  1. 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量类变量(堆中)不包含局部变量(栈中),因为局部变量是线程私有的,因此不存在竞争问题。

  2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本

  3. 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

二、 CAS 操作

1. 概述及基本工作流程

CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作

  • AbstractQueuedSynchronizer(AQS框架)

  • AtomicXXX类

例子:

我们还是基于刚才学习过的JMM内存模型进行说明

  • 线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中

一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功

  • 线程1操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 101 (a++)

    • 线程1拿A的值与主内存V的值进行比较,判断是否相等

    • 如果相等,则把B的值101更新到主内存中

  • 线程2操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 99(a--)

    • 线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值99)

    • 不相等,则线程2更新失败

  • 自旋锁操作

    • 因为没有加锁,所以线程不会陷入阻塞,效率较高

    • 如果竞争激烈,重试频繁发生,效率会受影响

需要不断尝试获取共享内存V中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功

2. CAS 底层实现

CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令

都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现

image-20230504182447552.png

在java中比较常见使用有很多,比如ReentrantLock和Atomic开头的线程安全类,都调用了Unsafe中的方法

  • ReentrantLock中的一段CAS代码

3. 乐观锁和悲观锁

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

三 、 volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证可见性

  • 禁止重排序

volatile 变量的读写操作都会伴随内存屏障。对 volatile 变量的写操作会在其后插入一个存储屏障(Store Barrier),确保对该变量的修改立即同步到主内存,同时防止写操作与之后的读写操作重排序。对 volatile 变量的读操作会在其前插入一个加载屏障(Load Barrier),确保读操作获取的是最新的数据,同时防止读操作与之前的读写操作重排序。

1. 保证线程间的可见性

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

一个典型的例子:永不停止的循环

 package com.itheima.basic;
 ​
 ​
 // 可见性例子
 // -Xint
 public class ForeverLoop {
     static boolean stop = false;
 ​
     public static void main(String[] args) {
         new Thread(() -> {
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             stop = true;
             System.out.println("modify stop to true...");
         }).start();
         foo();
     }
 ​
     static void foo() {
         int i = 0;
         while (!stop) {
             i++;
         }
         System.out.println("stopped... c:"+ i);
     }
 }

当执行上述代码的时候,发现foo()方法中的循环是结束不了的,也就说读取不到共享变量的值结束循环。

主要是因为在JVM虚拟机中有一个JIT(即时编辑器)给代码做了优化。

上述代码

 while (!stop) {
 i++;
 }

在很短的时间内,这个代码执行的次数太多了,当达到了一个阈值,JIT就会优化此代码,如下:

 while (true) {
 i++;
 }

当把代码优化成这样子以后,及时stop变量改变为了false也依然停止不了循环

解决方案:

第一:

在程序运行的时候加入vm参数-Xint表示禁用即时编辑器,不推荐,得不偿失(其他程序还要使用)

第二:

在修饰stop变量的时候加上volatile,表示当前代码禁用了即时编辑器,问题就可以解决,代码如下:

 static volatile boolean stop = false;

2. 禁止进行指令重排序

用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

在去获取上面的结果的时候,有可能会出现4种情况

情况一:先执行actor2获取结果--->0,0(正常)

情况二:先执行actor1中的第一行代码,然后执行actor2获取结果--->0,1(正常)

情况三:先执行actor1中所有代码,然后执行actor2获取结果--->1,1(正常)

情况四:先执行actor1中第二行代码,然后执行actor2获取结果--->1,0(发生了指令重排序,影响结果)

解决方案

在变量上添加volatile,禁止指令重排序,则可以解决问题

屏障添加的示意图

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下

  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

其他补充

我们上面的解决方案是把volatile加在了int y这个变量上,我们能不能把它加在int x这个变量上呢?

下面代码使用volatile修饰了x变量

屏障添加的示意图

这样显然是不行的,主要是因为下面两个原则:

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下

  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

所以,现在我们就可以总结一个volatile使用的小妙招:

  • 写变量让volatile修饰的变量的在代码最后位置

  • 读变量让volatile修饰的变量的在代码最开始位置

四、 什么是AQS?

1. 概述

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架

AQS与Synchronized的区别

synchronized

AQS

关键字,c++ 语言实现

java 语言实现

悲观锁,自动释放锁

悲观锁,手动开启和关闭

锁竞争激烈都是重量级锁,性能差

锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类

  • ReentrantLock 阻塞式锁

  • Semaphore 信号量

  • CountDownLatch 倒计时锁

2. 工作机制

  • 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁,如果持有这把锁的人再次持有这把锁,那么state就会+1,代表锁重入

  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList

  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

  • 线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功

  • 线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持有锁,它们都会到FIFO队列中进行等待,

  • FIFO是一个双向队列,head属性表示头结点,tail表示尾结点

如果多个线程共同去抢这个资源是如何保证原子性的呢?

在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待

AQS是公平锁吗,还是非公平锁?

  • 新的线程与队列中的线程共同来抢资源,是非公平锁

  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源

五、导致并发程序出现问题的根本原因是什么

Java并发编程三大特性

  • 原子性

  • 可见性

  • 有序性

(1)原子性

一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行

比如,如下代码能保证原子性吗?

以上代码会出现超卖或者是一张票卖给同一个人,执行并不是原子性的

解决方案:

1.synchronized:同步加锁

2.JUC里面的lock:加锁

(3)内存可见性

内存可见性:让一个线程对共享变量的修改对另一个线程可见

比如,以下代码不能保证内存可见性

解决方案:

  • synchronized

  • volatile(推荐)

  • LOCK

(3)有序性

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

还是之前的例子,如下代码:

解决方案:

  • volatile