一、缓存一致性

1、总线加锁处理

CPU 从主内存读数据到缓存,会先加锁 lock,其他 cpu 没法读或写,直到使用完释放后其他 cpu 才能读取。

2、MESI 缓存一致性协议

多个 cpu 从主内存读取同一个数据到各自的高速缓存,当某个 cpu 改了自家缓存数据,再立刻同步回主内存时,其他 cpu 通过“总线嗅探机制”可以感知到数据的变化从而将自己缓存里的数据失效掉,重新到主内存读。

状态 描述 监听任务
M 修改(Modified) 该 Cache line 有效,数据被修改 了,和主内存中的数据不一致, 数据只存在于本 Cache 中。 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成 S(共享)状态之前被延迟执行。
E 独享 互斥(Exclusive) 该 Cache line 有效,数据和内存 中的数据一致,数据只存在于本 Cache 中。 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成 S(共享)状态。
S 共享(Shared) 该 Cache line 有效,数据和内存 中的数据一致,数据存在于很多 Cache 中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I 无效(Invalid) 该 Cache line 无效。

https://blog.csdn.net/m18870420619/article/details/82431319

二、JMM

1、Java 内存模型

image.png

2、Java 并发编程

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看
得到修改的值。
有序性: 即程序执行的顺序按照代码的先后顺序执行,程序顺序和我们的编译运行的执行一定是一样(编译优化,指令重排)。
**

3、内存同步

image.png

三、各类锁

1、Volatile

volatile 算是 JVM 提供的最轻量级的同步机制,当一个变量定义为 volatile,它具有内存可见性以及禁止指令重排序两大特性。
⚫ 可见性:意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
⚫ 禁止指令重排:程序执行顺序是按代码的先后顺序执行。

它的使用场景是:状态标记。

  1. public class Volatile01 {
  2. volatile boolean stop = false;
  3. public void shutDown() {
  4. stop = true;
  5. System.out.println("准备关闭了 " + stop);
  6. }
  7. public void doWork() {
  8. while (!stop) { }
  9. System.out.println("已关闭");
  10. }
  11. public static void main(String[] args) throws InterruptedException{
  12. Volatile01 volatile01 = new Volatile01();
  13. new Thread( () -> {
  14. volatile01.doWork();
  15. }).start();
  16. Thread.sleep(1000);
  17. new Thread( ()-> {
  18. volatile01.shutDown();
  19. }).start();
  20. }
  21. }

volatile 的底层原理是依靠汇编的 lock 前缀指令,第一它会将当前处理器缓存的数据立即写回到系统内存,第二是这个写回内存操作会让其他 cpu 里缓存了该内存地址的数据失效(MESI协议)。
volatile 变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写到系统内存。 Lock 前缀指令实际上相当于一个内存屏障(也叫内存栅栏),它确保指令重排序时不会把其后面的指令排 到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

  1. public class Volatile02 implements Runnable {
  2. static volatile int i = 1;
  3. @Override
  4. public void run() {
  5. /***
  6. * i++;该操作并非为原子性操作。什么是原子性操作?
  7. * 简单来说就是一个操作不能再分解。i++操作实际上分为3步:
  8. * 1、读取i变量的值;2、增加is变量的值;3、把新的值写到内存中。
  9. */
  10. System.out.println(Thread.currentThread().getName() + ": " + i + ", " + (++i));
  11. }
  12. public static void main(String[] args) {
  13. Thread t1 = new Thread(new Volatile02(), "A");
  14. Thread t2 = new Thread(new Volatile02(), "B");
  15. Thread t3 = new Thread(new Volatile02(), "C");
  16. Thread t4 = new Thread(new Volatile02(), "D");
  17. t1.start();
  18. t2.start();
  19. t3.start();
  20. t4.start();
  21. }
  22. }

由于i++该类操作 volatile 无法处理,这里就需要使用锁🔒。

2、Synchronized

Synchronized 锁是一个重量级锁、重入锁、jvm 级别锁。

  1. public class Sync01 implements Runnable {
  2. static int i = 0;
  3. @Override
  4. public void run() {
  5. add();
  6. }
  7. private static synchronized void add() {
  8. for (int j = 0; j < 10000; j++) {
  9. i++;
  10. }
  11. }
  12. public static void main(String[] args)throws Exception {
  13. Sync01 sync01 = new Sync01();
  14. Sync01 sync02 = new Sync01();
  15. Thread thread = new Thread(sync01);
  16. Thread thread2 = new Thread(sync02);
  17. thread.start();thread2.start();
  18. thread.join();thread2.join();
  19. System.out.println(i);
  20. }
  21. }

方法和代码块(对象锁和类锁)
⚫ 对于普通同步方法,锁是当前实例对象;
⚫ 对于静态同步方法,锁是当前类的 Class 对象;
⚫ 对于同步方法块,锁是 Synchonized 括号里配置的对象。

该锁的使用场景是:资源竞争。

3、Lock & ReentrantLock

Lock 代码如下:
image.png

关于 ReentrantLock 的使用方式:一定要把加锁和解锁放在try块和finally块中。

  1. try {
  2. lock.lock();
  3. } finally {
  4. lock.unlock();
  5. }

Synchronized 和 ReentrantLock 对比:
Synchronized:jvm 层级的锁 自动加锁自动释放锁;
Lock:依赖特殊的 cpu 指令,代码实现、手动加锁和释放锁、Condition(生产消费模式);
ReentrantReadWriteLock 读写锁的细粒度问题,读是共享的、写是独占的;
java8 增加了对读写锁的优化:StampedLock

4、AbstractQueuedSynchronizer

队列同步器,AbstractQueuedSynchronizer。