java并发-缓存一致性

硬件缓存一致性

是 CPU、内存、以及 I/O 设备(磁盘)读取性能不一致,为了平衡三者的速度差异,最大化的利用 CPU 提升性能,从硬件、操作系统、编译器等方面都做出了很多的优化

  1. CPU 增加了高速缓存
  2. 操作系统增加了进程、线程。通过 CPU 的时间片切换最大化的提升 CPU 的使用率
  3. 编译器的指令优化
    在这里插入图片描述
    通过高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但带来了缓存一致性问题

为了解决缓存不一致的问题,在 CPU 层面做了很多事情,主要提供了两种解决办法

  • 总线锁
  • 缓存锁

总线锁,简单来说就是,在多 cpu 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住
了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,这种机制显然是不合适的
如何优化呢?最好的方法就是控制锁的保护粒度,我们只需要保证对于被多个 CPU 缓存的同一份数据是一致的就行。所以引入了缓存锁,它核心机制是基于缓存一致性协议来实现的。

JMM

Java Memory Model

JMM 属于语言级别的抽象内存模型,可以简单理解为对硬件模型的抽象,它定义了共享内存中多线程程序读写操作的行为规范:在虚拟机中把共享变量存储
到内存以及从内存中取出共享变量的底层实现细节

通过这些规则来规范对内存的读写操作从而保证指令的正确性,它解决了 CPU 多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可见性。

JMM 并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序,也就是说在 JMM 中,也会存在缓存一致性问题和指令重排序问题。只是 JMM 把底层的问题抽象到 JVM 层面,再基于 CPU 层面提供的内存屏障指令,以及限制编译器的重排序来解决并发问题

在这里插入图片描述
JMM 抽象模型分为主内存、工作内存;主内存是所有线程共享的,一般是实例对象、静态字段、数组对象等存储在堆内存中的变量。工作内存是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主
内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成
在这里插入图片描述
Java 内存模型底层实现可以简单的认为:通过内存屏障(memory barrier)禁止重排序,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于 volatile,编译器将在 volatile 字段的读写操作前后各插入一些内存屏障

JMM解决可见性有序性问题:
volatile、synchronized、final;
以及通过happenbefore规则解决顺序一致性问题

synchronized

在这里插入图片描述
Java中的每个对象都可以把它当作一个同步锁来使用,叫作监视器锁
内存语义:
解决共享变量的内存可见性,原子性,但会引起线程上下文切换并带来线程调度开销

HappenBefore

程序顺序规则

	class VolatileDemo {
		int a = 0; 
		volatile boolean flag = false;
		public void writer() {
			a = 1 ; // 1
			flag  true; //
		}
		public void reader() {
			if(flag) { 	  // 3
				int i = a; // 4
			}
		}
	}
  1. 顺序性规则
    一个线程中的每个操作,happens-before 于该线程中的
    任意后续操作; 可以简单认为是 as-if-serial。单个线程
    中的代码顺序不管怎么变,对于结果来说是不变的
    顺序规则表示 1 happenns-before 2; 3 happensbefore 4
  2. volatile变量规则
    对于volatile修饰的变量的写的操作,一定happen-before后续对于volatile变量的读操作;根据volatile规则,2happens before 3
  3. 传递性规则
    如果1 happens-before 2; 3happens-before 4;
    那么传递性规则表示:1 happens-before 4;
  4. start规则,如果线程A执行操作ThreadB.start(),那么线程A的ThreadB.start()操作happens-before线程B中的任意操作
  5. join规则
    如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  6. 监视器锁的规则
    对一个锁的解锁,happens-before于随后对这个锁的加锁

可见性解决

volatile

volatile int i = 0;
i++; // 不保证原子性

解决变量的可见性问题,不解决原子性,案例:单例的双检查

重排序

为了提高程序的执行性能,编译器和处理器都会对指令做重排序,其中处理器的重排序在前面已经分析过了。所谓的重排序其实就是指执行的指令顺序。

编译器的重排序指的是程序编写的指令在编译之后,指令可能会产生重排序来优化程序的执行性能。从源代码到最终执行的指令,可能会经过三种重排序。

在这里插入图片描述
2 和 3 属于处理器重排序。这些重排序可能会导致可见性问题。

编译器的重排序,JMM 提供了禁止特定类型的编译器重排序。

处理器重排序,JMM 会要求编译器生成指令时,会插入内存屏障来禁止处理器重排序

导致可见性问题的根本原因是缓存以及重排序

CAS操作

CAS即Compareand Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性

JDK里面的Unsafe类提供了一系列的compareAndSwap*方法

/**
 * @param obj 需要操作的对象
 * @param valueOffset 对象obj中内存偏移量
 * @param expect 变量预期值
 * @param longupdate 新的值
 */
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, longupdate);

ABA问题:AtomicStampedReference解决

Unsafe

JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++ 实现库

错误使用

public class TestUnSafe {
    static final Unsafe unsafe = Unsafe.getUnsafe();
    // 记录变量state在类TestUnSafe中的偏移值
    static long stateOffset;
    // 变量
    private volatile long state = 0;
    static {
        try {
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        TestUnSafe test = new TestUnSafe();
        Boolean success = unsafe.compareAndSwapInt(test,stateOffset,0,1);
        System.out.println(success);
    }
}

输出

java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at com.drank.TestUnSafe.<clinit>(TestUnSafe.java:6)

原因:

// sun.misc.Unsafe#getUnsafe
@CallerSensitive
  public static Unsafe getUnsafe() {
  	  // 获取调用getUnsafe这个方法的对象Class
      Class localClass = Reflection.getCallerClass();
      // 判断是不是Bootstrap类加载器加载的localClass
      // 在这里 TestUnSafe.class 是使用了AppClassLoader加载,所以抛异常
      if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
          throw new SecurityException("Unsafe");
      } else {
          return theUnsafe;
      }
  }

正确打开方式

import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestUnSafe {
    static final Unsafe unsafe;
    // 记录变量state在类TestUnSafe中的偏移值
    static final long stateOffset;
    // 变量
    private volatile long state = 0;
    static {
        try {
            // 使用反射获取Unsafe的成员变量theUnsafe
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (Exception e) {
            e.printStackTrace();
            throw new Error(e);
        }
    }
    public static void main(String[] args) {
        TestUnSafe test = new TestUnSafe();
        Boolean success = unsafe.compareAndSwapInt(test,stateOffset,0,1);
        System.out.println(success);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值