22-09-01 西安 JUC(04)java内存模型JMM、volatile关键字、原子性类、CAS比较并交换、AQS锁原理

 计算机运行架构图

1.读取代码到内存条  【代码是存储到硬盘的

2.CPU操作内存条的数据  【cpu和内存交互。

但是内存条比起CPU计算速度还是慢

3.引出cpu缓存,这层缓存空间比较小(相比于主内存RAM)

由于cpu的运行程序速度远大于主存储的速度,所以会在主存RAM和CPU之间加多级高速缓存缓存的速度接近cpu的运行速度,这样会大大提高计算机的运行速度。

任务栏右键--任务管理器--性能 

对于每⼀个线程来说,栈中的变量都是私有的,⽽堆是共有的。
这是因为现代计算机为了⾼效,往往会在⾼速缓存区中缓存共享变量,因为 cpu 访问缓存区⽐访问内存要快得多。

java内存模型(JMM)

在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。

1、JMM的理解

Java线程之间的通信由JMM控制。

JVM规范中试图定义一种Java内存模型(java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果

JMM能干什么?

1、通过JMM来实现线程和主内存之间的抽象关系。
2、屏蔽各个硬件平台和操作系统的内存访问差异以实现让Jva程序在各种平台下都能达到一致的内存访问效果。


2、JMM内存划分与规定

JMM规定了内存主要划分为主内存工作内存2种。线程之间的共享变量存在主内存中,每个线程都有⼀个私有的本地内存,存储了该线程以读、写共享变量的副本。

主内存:保存了所有的共享变量

不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递(线程通信)均需要通过主内存来完成

共享变量:共享变量指多个线程都能使用的变量。不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。

线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;

工作内存:每个线程都有自己的工作内存,线程独享,保存了线程用到的共享变量副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互。

本地内存只有线程自己可以访问。每个线程操作完之后就可以释放内存了

关于JMM内存划分注意两点:

1.此处JMM的主内存和工作内存  跟 JVM内存划分(堆、栈、方法区)是在不同的维度上进行的

2.从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存


3、JMM线程通信

如果线程A与线程B之间要通信的话,必须经历下⾯2个步骤:
i. 线程A将本地内存A中更新过的共享变量刷新到主内存中去。
ii. 线程B到主内存中去读取线程A之前已经更新过的共享变量。
线程 B 并不是直接去主内存中读取共享变量的值,⽽是先在本地内存 B 中找到 这个共享变量, 发现这个共享变量已经被更新了(JMM保证 操作共享变量的可⻅性 ,然后本地内存B 去主内存中读取这个共享变量的新值,并拷⻉到本地内存B 中,最后线程 B 再读取本地内存 B 中的新值。

4、JMM三大特性

JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的,也是JMM三大特性。

要保证多线程安全,要保证3大特性

  • 原子性:即不可分割性。比如 a=0 这个操作是不可分割的,那么我们说这个操作是原子操作。一个操作是原子操作,那么我们称它具有原子性。

再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线
程安全问题,需要使用同步技术(sychronized)或者锁(Lock)来让它变成一个原子操作。

java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。
比如:AtomicInteger、AtomicLong、AtomicReference等。
  • 可见性:每个线程都有自己的工作内存,所以当某个线程修改完某个共享变量之后,在其他的线程中,未必能立即观察到该变量已经被修改。JMM规定了所有的变量都存储到主内存中

​​​​​​​在 Java 中 volatile、synchronized 和 final 实现可见性。
volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。
  • 有序性:java的有序性跟线程相关。一个线程内部所有操作都是有序的,如果是多个线程所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。

volatile和synchronized可以保证程序的有序性,很多程序员只理解这两个关键字的执行互斥,
而没有很好的理解到volatile和synchronized也能保证指令不进行重排序。

线程脏读问题

系统主内存共享变量(没有被volatile 修饰)修改被写入的时机是不确定的,多线程并发下很可能出现"脏读"


volatile关键字

Java语言规范第三版中对volatile的定义如下:Java语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获取这个变量。

Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。


1、验证volatile可见性

内存可⻅性,指的是线程之间的可⻅性, 当⼀个线程 修改了共享变量时, 另⼀个线程 可以读取到这个修改后的值。
volatile可见性原理
当⼀个线程对 volatile 修饰的变量进⾏写操作时, JMM 会⽴即把该线程对应的本地内存中的共享变量的值刷新到主内 存;
当⼀个线程对 volatile 修饰的变量进⾏读操作 时, JMM 会把⽴即该线程对应的本地内存置为⽆效,从主内存中读取共享变量的值。

普通变量a,在多个线程之间是不可见的,如下:

static  int a = 0;
public static void main(String[] args) throws InterruptedException {
    System.out.println(Thread.currentThread().getName() + " 获取到的修改前的a : " + a);
    //子线程读取a的数据
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 获取到的修改前的a : " + a);
        while (a == 0) {
            //就是什么都不写,单纯的在a=0的时候,死循环而已
        }
        //子线程中死循环判断 如果a的值改掉了 输出修改后的结果
        System.out.println(Thread.currentThread().getName() + " 获取到的修改后的a : " + a);
    }, "AA").start();
    Thread.sleep(500);
    a = 1;
    System.out.println(Thread.currentThread().getName() + " 获取到的修改后的a : " + a);
}

控制台打印:符合预期,因为 默认普通的变量不具有可见性。 main线程将共享数据a改了,但是子线程AA获取不到修改后的a,所以一直走while循环,程序停不下来

怎么保证多线程并发变量具有可见性?

  1. 可以加锁:效率低。加锁sychronized 3种问题都能解决。
  2. 也可以使用volatile关键字
volatile 本质是告诉 JVM 当前变量在寄存器中的值是不确定的,需要从主存中读取,
synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住

volatile 不会造成线程阻塞,synchronized 可能会造成线程阻塞

使用volatile关键字

改动上面代码,只是加了一个volatile关键字

 控制台效果:符合预期。volatile他修饰的内容具有可见性


2、验证volatile有序性(禁止指令重排)

有序性:多线程并发时指令执行的顺序可能和我们编写的代码的逻辑顺序不一致

为什么要指令重排呢

为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序

两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,
执行顺序会被优化。

特殊场景:要禁止指令重排,从而达到程序执行的有序性

禁⽌volatile变量与普通变量重排序

通过一个反例来证明,不加volatile会出现代码的有序性问题。

期望的结果: 线程1先执行  a = 1  ,x =0 , b = 1,y=1
期望的结果: 线程2先执行  b = 1  ,y =0 , a = 1,x=1
期望的结果: 线程1执行一半线程2执行了:  a = 1,b=1,x=1,y=1
也就是能接受的结果有3种   x=0&y=1 , y=0&x=1 , x=1&y=1  

如果出现了  x=0&y=0  代表代码的逻辑没有按照编写的指令顺序执行 出现了指令重排

static int a,b,x,y;
    public static void main(String[] args) throws Exception {
        int i = 0;//表示遍历执行的次数
        while(true){
            i++;
            a = b = x = y = 0;
            //线程1: 设置a = 1,x=b;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            //线程2:设置b = 1;y=a;
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(String.format("第 %d 轮遍历,x = %s, y = %s", i,x,y) );
            //如果出现了  x=0&y=0  代表代码的逻辑没有按照编写的指令顺序执行 出现了指令重排
            while(x==0 && y==0){
                break;
            }
        }
    }

控制台效果:当然我是没出来效果,连x=1,y=1的效果都没出来,更别说x=0,y=0的情况了

保证有序性:
1. volatile关键字声明的变量可以保证代码的有序性,不会出现指令重排

2. synchronized也可以保证程序的有序性


3、验证volatile不具备原子性

原子性: 一个操作不会被分隔

会出现原子性问题的程序:

class AtomicDemo01 {
    private int num = 0;

    public int incr() {
        return ++num;
    }
}

public class VolatileDemo {

    public static void main(String[] args){
        AtomicDemo01 a = new AtomicDemo01();
        for (int i = 0; i < 100000; i++) {
            new Thread(() -> {
                //原子性:
                //一个操作不会被分隔
                System.out.println(a.incr());
            }).start();
        }
    }
}

控制台打印:

100000个线程执行++num操作,如果++num操作具备原子性,最后的值应该是100000。说明++num不具备原子性

特别注意:就算是给num加了volatile,num++和++num也不具备原子性

private volatile int num = 0;

下面是加了volatile后的运行效果


4、怎么保证原子性

1.加锁(保证代码块原子性):synchornized/Lock

给incr方法加锁,保证了整个incr方法内部的代码是具有原子性的

public synchronized int incr() {
    return ++num;
}

控制台运行效果:终于看到了100000,很帅

2.原子性类(保证一个操作具有原子性):juc Atomic包下提供的类

子线程的意义:主线程委托cpu做某些异步操作

class AtomicDemo01 {
    //初始化原子类对象
    private AtomicInteger ai = new AtomicInteger(0);

    public int incr(){
        return ai.incrementAndGet();
    }
}

public class VolatileDemo {

    public static void main(String[] args){
        AtomicDemo01 a = new AtomicDemo01();
        for (int i = 0; i < 100000; i++) {
            new Thread(() -> {
                //原子性:
                //一个操作不会被分隔
                System.out.println(a.incr());
            }).start();
        }
    }
}

控制台运行效果:


5、volatile原理

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制

当一个变量定义为 volatile 之后,将具备两种特性

  • 保证此变量对所有的线程的可见性

  • 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。

  • 不保证变量的原子性

volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

扩展:【真的很脑阔疼】

Happen-Before被翻译成先行发生原则,意思就是当A操作先行发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。

volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作


6、多线程先行发生原则happens-before

开发者只要遵循happens-before规则,JVM就能保证指令在多线程之间的顺序性符合开发者的预期。

JMM使⽤happens-before的概念来定制两个操作之间的执⾏顺序。这两个操作可以在⼀个线程以内,也可以是不同的线程之间。举例

如果操作 A happens-before 操作 B ,那么操作 A 在内存上所做的操作对操作 B 都是可⻅的,不管它们在不在⼀个线程。

我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这得益于Java语言中JMM原则下有一个“先行发生”(Happens-Before)的原则限制和规矩

Happens-Before总原则

顺序

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,
而且第一个操作的执行顺序排在第二个操作之前。

重排

两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。
如果重排序之后的执行结果与按照happens--before关系来执行的结果一致,那么这种重排序并不非法。
Java 中,有以下天然的 happens-before 关系:
程序顺序规则:⼀个线程中的每⼀个操作, happens-before 于该线程中的任意后续操作。
监视器锁规则:对⼀个锁的解锁, happens-before 于随后对这个锁的加锁。
volatile 变量规则:对⼀个 volatile 域的写, happens-before 于任意后续对这个volatile域的读。
传递性:如果 A happens-before B ,且 B happens-before C ,那么 A happens before C。
start 规则:如果线程 A 执⾏操作 ThreadB.start() 启动线程 B ,那么 A 线程的ThreadB.start()操作 happens-before 于线程 B 中的任意操作
join 规则:如果线程 A 执⾏操作 ThreadB.join ()并成功返回,那么线程 B 中的任意操作happens-before 于线程 A ThreadB.join() 操作成功返回。

原子操作类

1、CAS 比较并交换

CAS:compare and swap的缩写,中文翻译成比较并交换

CAS操作有3个基本参数:内存位置A,预期原值B,更新值C。

执行CAS操作的时候,将内存位置的值A与预期原值B比较,
如果相匹配,那么处理器会自动将该位置值更新为新值C,
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

CAS是解决多线程并发安全问题的一种乐观锁算法。因为它在对共享变量更新之前,会先比较当前值是否与更新前的值一致,如果一致则更新,如果不一致则循环执行(称为自旋锁),直到当前值与更新前的值一致为止,才执行更新。

好处:

比起用synchronized重量级锁,CAS的排他时间要短很多,所以在多线程情况下性能会比较好。

缺点

1.如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2.ABA问题

2、CAS原理Unsafe类

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

Unsafe提供的 CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问。 

Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。 

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断。


3、atomic包

在JUC下有个atomic包,有很多原子操作的包装类:它们都是基于CAS解决并发安全问题的(类似MybatisPlus中的version版本号)

基本类型原子类 

Atomiclnteger
AtomicBoolean
AtomicLong


4、AtomicInteger类compareAndSet()

AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

原子性类的方法:

public static void main(String[] args){
    //初始化原子性类对象:并设置初始化的值为100
    AtomicInteger ai = new AtomicInteger(100);
    // 参数1: 原本的值, 参数2:希望设置的值
    // 相当于将原来的值当做版本号使用
    System.out.println(ai.compareAndSet(1, 2));
    System.out.println("ai="+ai.get());
    System.out.println(ai.compareAndSet(100, 200));
    System.out.println("ai="+ai.get());
 }

 控制台打印:符合预期效果

修改数据时,会检查期望值和原子性对象value属性值是否一致,如果一致使用希望更新的值修改value的值。否则不进行修改。

//AtomicInteger类的方法
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

//Unsafe类的方法,是个native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

Unsafe类是CAS的核心类,提供硬件级别的原子操作(目前所有CPU基本都支持硬件级别的CAS操作

valueOffset:对象的属性地址偏移量

//以下为AtomicInteger源码部分
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

public final int get() {
    return value;
}

5、原子性类存在的3个问题

原子性类的问题:(AtomicInteger为例)
1、不能保证代码块的原子性

Unsafe类只提供了少量的几个原子性的操作方法 只有调用他们其中一个才可以保证此行代码有原子性

2、并发更新时: 效率低

获取到原子性对象的value值相同的N多个线程,更新时版本号冲突!


等待其他线程操作完成后进行更新,但是自己调用原子方法时由于版本号被改了,所以更新失败,高并发的写,会浪费大量的CPU计算

3、ABA问题: 数据修改后又被改回来  无法判断数据之前是否被修改过。ABA问题的解决思路是在变量前⾯追加上版本号或者时间戳

多个线程并发:2个线程都希望将数据从初始值改为指定值(100,2)

// 演示ABA问题
public static void main(String[] args){
    //初始化原子性类对象:并设置初始化的值为100
    AtomicInteger ai = new AtomicInteger(100);
    // 参数1: 原本的值, 参数2:希望设置的值
    //并发问题
    //同时来了2个线程,线程1和线程2都是想把100改为2,按道理说它俩只能有一个能修改成功
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()+":"+ai.compareAndSet(100, 2));
    },"线程1").start();
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()+":"+ai.compareAndSet(100, 2));
    },"线程2").start();
 }

控制台打印:

反正是一个true,一个false...,此时是没有问题的

但是ABA问题他就来了

并发的线程中有一个又修改了值(2,100)。此时就是线程1,2,3,了。

// 演示ABA问题
public static void main(String[] args){
    //初始化原子性类对象:并设置初始化的值为100
    AtomicInteger ai = new AtomicInteger(100);
    // 参数1: 原本的值, 参数2:希望设置的值
    //并发问题
    //同时来了2个线程,线程1和线程2都是想把100改为2,按道理说它俩只能有一个能修改成功
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()+":"+ai.compareAndSet(100, 2));
    },"线程1").start();
    new Thread(()->{
        try {
            //线程1和和线程2一起来的,睡眠是为了保证线程3执行优先于线程2
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+ai.compareAndSet(100, 2));
    },"线程2").start();
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()+":"+ai.compareAndSet(2, 100));
    },"线程3").start();
 }

运行结果就不对劲了,但是true。。。线程1和线程2都修改成功了,造成了并发的线程1和线程2居然都能把值从100修改成2。这就是ABA问题 


6、版本号原子引用--解决ABA问题

AtomicStampedReference:版本号原子引用

AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该 stamped 的变化

public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

stamped 需要应用程序自身去负责,AtomicStampedReference并不提供,一般使用时间戳作为版本号)

解决ABA问题

// 解决ABA问题
public static void main(String[] args){
    //参数1:初始化的值    参数2:初始化的值的版本号
    AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(100,1);
    //并发问题
    //同时来了2个线程,线程1和线程2都是想把100改为2,按道理说它俩只能有一个能修改成功
    new Thread(()->{
        //参数1:原数据引用  参数2:要更新数据的引用  参数3:原数据的版本  参数4:更新后的版本号
        boolean b = asr.compareAndSet(100, 2, 1, 2);
        System.out.println(Thread.currentThread().getName()+":"+b);
    },"线程1").start();
    new Thread(()->{
        try {
            //线程1和和线程2一起来的,睡眠是为了保证线程3执行优先于线程2
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //参数1:原数据引用  参数2:要更新数据的引用  参数3:原数据的版本  参数4:更新后的版本号
        boolean b = asr.compareAndSet(100, 2, 1, 2);
        System.out.println(Thread.currentThread().getName()+":"+b);
        System.out.println(asr.getStamp());//输出版本号
        System.out.println(asr.getReference());//输出引用
    },"线程2").start();
    new Thread(()->{
        //参数1:原数据引用  参数2:要更新数据的引用  参数3:原数据的版本  参数4:更新后的版本号
        boolean b = asr.compareAndSet(2, 100, 2, 3);
        System.out.println(Thread.currentThread().getName()+":"+b);
    },"线程3").start();
 }

控制台打印效果:

此时,线程1和线程2就只有一个打印true了。3是版本号,100是原子引用


7、自定义封装一个具有原子操作的类

//自定义原子性类
class AtomicDemo03{

    private volatile int num = 1;
    static Unsafe unsafe;
    static long valueOffset;
    static{
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);//设置私有的可以访问
            unsafe = (Unsafe) field.get(null);
            valueOffset = unsafe.objectFieldOffset  //获取AtomicDemo03类中的num属性的偏移量
                    (AtomicDemo03.class.getDeclaredField("num"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    public int incr(){
        return unsafe.getAndAddInt(this,valueOffset,1);
    }


    public final boolean compareAndSet (int expect, int update)throws Exception {

        //参数1:要修改属性值的对象
        //参数2:要修改的属性在对象中的偏移量(基于偏移量+拥有该属性的对象的地址可以获取该属性的内存地址)
        //参数3:希望值
        //参数4:要修改的值
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    public int getNum() {
        return num;
    }
}

使用一下它的原型性

public class VolatileDemo {
        public static void main(String[] args){
            AtomicDemo03 ad = new AtomicDemo03();
            for (int i = 0; i < 100000; i++) {
                new Thread(()->{
                    System.out.println(ad.incr());
                }).start();
            }
         }
}

控制台打印:


AQS 抽象队列同步器

AbstractQueuedSynchronizer抽象队列同步器简称AQS,是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题

JUC下面Lock的实现以及一些并发工具类(Semaphore、CountDownLatch、CyclicBarrier等)都是通过AQS来实现的。具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。

1、双向队列FIFO

AQS维护了一个volatile语义的共享资源变量state和一个FIFO线程等待队列(多线程竞争state资源被阻塞时,会进入此队列)。

//AbstractQueuedSynchronizer类源码
private volatile int state;

注:指针 head tail ⽤于标识队列的头部和尾部

如果共享资源state被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。锁的分配机制就是AQS.

​​​​​​​AQS类本身实现的是⼀些排队和阻塞的机制

2、Node节点

FIFO队列并不是直接储存线程,⽽是储存拥有线程的Node节点。

AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作;将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对是state值的修改。

由于一个共享资源同一时间只能由一条线程持有,也可以被多个线程持有,因此AQS中存在两种模式,如下:

  • 1、独占模式

    独占模式表示共享状态值state每次能由一条线程持有,其他线程如果需要获取,则需要阻塞,如JUC中的ReentrantLock

  • 2、共享模式

    共享模式表示共享状态值state每次可以由多个线程持有,如JUC中的CountDownLatch

Node的等待状态waitState成员变量

volatile int waitstatus


3、自定义锁 Mutex

AQS将大部分的同步逻辑均已经实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以,主要包括下面方法:

  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

也就是说:

通过AQS可以实现独占锁(只有一个线程可以获取到锁,如:ReentrantLock),也可以实现共享锁(多个线程都可以获取到锁Semaphore/CountDownLatch等)

自定义锁 Mutex

这个类纯属复制 AQS中的某一段注释。。

//基于AQS的自定义Lock锁:和ReentrantLock类似
class Mutex implements Lock, java.io.Serializable {

    // Our internal helper class
    private static class Sync extends AbstractQueuedSynchronizer {
        // Reports whether in locked state
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // Acquires the lock if state is zero
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Releases the lock by setting state to zero
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Provides a Condition
        Condition newCondition() {
            return new ConditionObject();
        }

        // Deserializes properly
        private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

    // The sync object does all the hard work. We just forward to it.
    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

测试我们自定义的这把锁

public class VolatileDemo {
        public static void main(String[] args){
            AtomicDemo04 ad = new AtomicDemo04();
            //通过上锁(自定义的锁)实现++num的原子性
            for (int i = 0; i < 100000; i++) {
                new Thread(()->{
                    System.out.println(ad.incr());
                }).start();
            }
         }
}

/*
基于AQS 同步器自定义锁
*/
class AtomicDemo04{
    private int num = 0;
    Mutex mutex = new Mutex();
    public int incr(){
        mutex.lock();
        try {
            return ++num;
        } finally {
            mutex.unlock();
        }
    }
}

控制台打印如下:


4、ReentrantLock底层原理

创建一个锁,根据传入的构造器参数判断创建的是不是公平锁,不传默认创建不公平锁

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

public ReentrantLock() {
    sync = new NonfairSync();
}

 在ReentrantLock类中包含了3个AQS的实现类:

  1. 抽象类Sync

  2. 非公平锁实现类NonfaireSync

  3. 公平锁实现类FairSync

加锁: 同步器使用cas让当前线程更新同步器的state值从0到1,如果更新成功获取到锁,如果更新失败将当前线程创建为Node对象添加到等待锁的队列中

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
//同步器
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
    abstract void lock();

    /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

    protected final boolean isHeldExclusively() {
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    // Methods relayed from outer class

    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }

    final boolean isLocked() {
        return getState() != 0;
    }

    /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

/**
不公平同步器
     * Sync object for non-fair locks
     */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
    //sync.lock();
    final void lock() {
        if (compareAndSetState(0, 1))// 锁的状态为空闲 ,从0改为1成功代表获取到锁 返回true
            setExclusiveOwnerThread(Thread.currentThread());//设置当前线程拥有锁
        else //锁状态更新失败  state的值不为0
            acquire(1);
    }
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && //获取锁失败,将当前线程对象添加到获取锁队列中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
    final boolean nonfairTryAcquire(int acquires) {
        //获取 正在获取锁的线程对象
        final Thread current = Thread.currentThread();
        //获取state值:
        int c = getState();
        if (c == 0) {//刚使用锁的线程释放了锁 
            //更新state的值为1
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //锁被占用:判断使用锁的线程是不是当前的线程对象
        else if (current == getExclusiveOwnerThread()) {
            //当前线程正在使用锁
            int nextc = c + acquires;//将state的值在之前的基础上+1   表示重入
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);//设置state的值为重入的次数
            return true;
        }
        //锁被占用 但是不是当前线程占用的  获取锁失败
        return false;
    }
    //释放锁:=============
    public final boolean release(int arg) {
        if (tryRelease(arg)) {//如果为true代表锁释放成功
            Node h = head;
            //释放锁之后获取队列中的线程获取锁
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        //锁释放失败
        return false;
    }
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;//获取state的值-1
        //判断如果不是当前线程拥有锁 抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        
        
        boolean free = false;
        if (c == 0) {//c=0代表锁释放成功
            free = true;
            setExclusiveOwnerThread(null);
        }
        //c无论是否为0  下面的代码都会执行
        //c为0 代表锁完全释放,free的值修改为true  return free为true代表完全释放锁
        //c不为0 代表锁重入了  未释放成功 将c的值设置给state  返回false//如果为true代表锁释放成功
        setState(c);
        return free;
    }
}
//公平的同步器
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
    protected final boolean tryAcquire(int acquires) {
        //获取当前线程对象
        final Thread current = Thread.currentThread();
        int c = getState();//获取锁的状态
        if (c == 0) {//锁空闲
            //hasQueuedPredecessors判断队列中是否有线程等待获取锁 如果有让他们先获取锁
            if (!hasQueuedPredecessors() &&
                //如果没有线程等待  自己去获取锁
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //当前线程已经拥有锁了  state!=0  重入
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        //获取锁失败
        return false;
    }
}

猜你喜欢

转载自blog.csdn.net/m0_56799642/article/details/126638876