JUC知识点总结(一)JMM与volatile关键字

JUC复习笔记(java.util.concurrent)

1. 并发的两个关键问题:线程间通信和线程间同步

线程通信机制有两种:

  • 共享内存:隐式通信,显式同步;

  • 消息传递:显式通信,隐式同步。(Java的并发采用的是共享内存模型。)

2. JAVA内存模型:JMM

Java虚拟机规范试图定义一个Java内存模型(JMM),以屏蔽所有类型的硬件和操作系统内存访问差异,让Java程序在不同的平台上能够达到一致的内存访问效果。简单地说,由于CPU执行指令的速度很快,但是内存访问速度很慢,于是在CPU里加了好几层高速缓存。在Java内存模型中,对上述优化进行了一波抽象。JMM规定所有的变量都在主内存中,类似于上面提到的普通内存,每个线程又包含自己的工作内存,为了便于理解可以看成CPU上的寄存器或者高速缓存。因此,线程的操作都是以工作内存为主,它们只能访问自己的工作内存,并且在工作之前和之后,该值被同步回主内存。JMM是一种抽象的概念,它定义了线程和主内存之间的关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有本地内存,本地内存中存储了该线程读/写共享变量的副本。

JUC知识点总结(一)JMM与volatile关键字_第1张图片

JMM与JVM的区别:

  • JMM描述的是一组规则,围绕原子性、有序性和可见性展开;
  • 相似点:存在共享区域和私有区域

JMM对于同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存。

  • 线程加锁前,必须读取主内存的最新值到自己的工作内存。

  • 加锁解锁是同一把锁。

由于JVM运行的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称之为栈空间),工作内存是每个线程的私有数据区域。而Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的内存空间,然后对变量进行操作,操作完成后,再将变量写回主内存,不能直接操作主内存中的变量各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

JMM通过控制主内存与每个线程的本地内存之间的交互来为java程序员提供内存可见性保证。

3. 指令重排序

源码
编译器优化重排序
指令级并行重排序
内存系统重排序
最终的指令序列

编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。在单线程程序中,对存在控制依赖的操作重排序不会改变执行结果;但在多线程程序中,对存在控制 依赖的操作重排序,可能会改变程序的执行结果。

对于编译器,JMM的编译器会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定的内存屏障指令,从而禁止特定类型的处理器重排序。

  • 并发模型分类

    四种内存屏障:LoadLoad, StoreStore, LoadStore, StoreLoad

  • 先行发生(happens-before)

    JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

4. 原子操作的实现原理

  • 处理器实现原子操作

    (1)通过总线锁保证原子性:当一个处理器在总线上输出LOCK #信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存

    (2)使用缓存锁保证原子性:内存区域如果被缓存在处理器的缓存行中,并且在LOCK期间被锁定,那么当他执行锁操作会写到内存时,处理器不在总线上声言LOCK #信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效

  • Java实现原子操作(锁和循环CAS)

    循环CAS机制(Compare and Swap) 自旋CAS:循环进行CAS操作直至成功为止

    CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。CAS 是一种无锁的非阻塞算法的实现。

    CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    • CAS实现原子操作的三大问题:

      (1)ABA问题:A到B再到A,CAS检查值时会以为没有发生变化,实际却发生了变化。(解决方式:在变量前面追加版本号,时间戳原子引用:AtomicStampedReference类)

      (2)循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

      (3)只能保证一个共享变量的原子操作(此时用锁或者将几个共享变量合并)

  • 锁机制:偏向锁、轻量级锁和互斥锁,除了偏向锁,另外两种锁都使用了循环CAS机制,即当一个线程进入同步块的时候使用循环CAS的方式 获取锁,当他退出同步块的时候使用循环CAS释放锁。

5.死锁

代码示例:

public void deadLock() throw Exception{
     
    Object A = new Object();
    Object B = new Object();
	new Thread(()->{
     
        synchronized(A){
     
        	System.out.println("get LockA");
            try{
     
                Thread.sleep(1000);
            }catch(InterruptedException e){
     
                e.printStackTrace();
            }            
        	synchronized(B){
     
            	System.out.println("get LockA and LockB");
        	}
    	}
    },"thread-1").start();
    new Thread(()->{
     
        synchronized(B){
     
        	System.out.println("get LockB");
            try{
     
                Thread.sleep(1000);
            }catch(InterruptedException e){
     
                e.printStackTrace();
            }   
        	synchronized(A){
     
            	System.out.println("get LockB and LockA");
        	}
    	}
    },"thread-2").start();
}

避免死锁的几个常见方法:

  • 避免一个线程获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

6. volatile关键字

volatile是一种轻量级的同步方法,只能保证可见性,比synchronized的使用和执行成本更低, 因为它不会引起线程上下文的切换和调度

  • volatile的特性

    保证了不同线程对这个变量进行操作时的可见性。(实现可见性

    禁止进行指令重排序。(实现有序性)

    volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

  • volatile写-读的内存语义

    写:当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值刷新到主内存;

    读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来从主内存中读取共享变量。

    内存语义的实现:为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

    JMM内存屏障插入策略:在每个volatile写操作前面插入StoreStore屏障;在每个volatile写操作后面插入StoreLoad屏障;在每个volatile读操作后面插入一个LoadLoad、一个LoadStore

volatile实现原理

有volatile变量修饰符的共享变量进行写操作的时候会多出一个lock前缀的指令,lock前缀的指令在多核处理器中引发两件事情

  • 将当前处理器缓存行的数据写回内存
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

为了提高处理速度,处理器不直接和内存通信,而是先将内存中的数据读到cache中再进行操作,但操作完全不知道何时会写到内存。如果对声明了volatile变量的进行写操作 ,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。 但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再进行计算操作就会有问题。所以,多处理器下,要实行缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,如果过期,就将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。

处理器指令 — Lock前缀:

​ (1) 确保对内存的读-改-写操作原子执行,使用缓存锁定来保证

​ (2) 禁止该指令与之前和之后的读和写指令重排序

​ (3) 把写缓冲区的所有数据刷新到内存中

解决volatile不保证原子性的办法: java.util.concurrent.atomic

下一篇
JUC知识点总结(二)Synchronized底层原理总结

你可能感兴趣的:(Java并发编程,多线程,java,并发编程)