如何应对Android面试官->AQS原理和volatile详解,手写ReentrantLock核心实现

AQS原理

什么是AQS?

AbstractQueuedSynchronizer,JDK提供的一个抽象类;

这些全是 AbstractQueuedSynchronizer 的子类; AbstractQueuedSynchronizer 是 JDK 中用来构建同步并发的基础组件

AQS 中有一个比较重要的同步变量:private volatile int state // 同步状态;

不管是 JDK 还是我们自己,在实现一个同步类的时候,都要围绕着这个 state 来做文章,修改它的值来表示当前的同步状态发生了变化;

为什么我们在使用各种同步类的时候而没有感受到 AQS 的存在呢?因为 AQS 在使用方式上采用的是继承的方式,而且是在同步工具类的内部定义了一个静态内部类来继承 AQS,这个同步工具类把内部类暴露的方法进行了一层封装,使我们感受不到 AQS 的存在;

所以说 AQS 采用的是 模板方法 的设计模式来实现;

模板方法设计模式

在阎宏博士的《JAVA与模式》一书中开头是这样描述模板方法(Template Method)模式的:

模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。

代码示例:

模板抽象类

csharp 复制代码
public abstract class AbsCar {    
    public abstract void makeLight(); // 造车灯    
    public abstract void makeDoor(); // 造车门    
    public abstract void  makeGlass(); // 造车玻璃    
    public void make() {        
        makeLight();        
        makeDoor();        
        makeGlass();    
    }
}

抽象实现子类1

csharp 复制代码
public class AudiCar extends AbsCar {    
    @Override    
    public void makeLight() {        
        System.out.println("make audi light");    
    }    
    @Override    
    public void makeDoor() {        
        System.out.println("make audi door");    
    }    
    @Override    
    public void makeGlass() {        
        System.out.println("make audi glass");    
    }
}

抽象实现子类2

csharp 复制代码
public class BMWCar extends AbsCar {    
    @Override    
    public void makeLight() {        
        System.out.println("make bmw light");    
    }    
    @Override    
    public void makeDoor() {        
        System.out.println("make bmw door");    
    }    
    @Override    
    public void makeGlass() {        
        System.out.println("make bmw glass");    
    }
}

抽象实现子类。。。

具体调用

java 复制代码
public class MakeCar {    
    public static void main(String[] args) {        
        AbsCar car = new AudiCar();        
        AbsCar car1 = new BMWCar();        
        car.make();        
        car1.make();    
    }
}

Android 源码中的自定义 View 就是模板方法模式,onDraw() onMeasure() onLayout();

如果我们需要自己实现一些锁,那么就需要遵照这个模板方法模式来实现;比如你想实现独占锁,那么就需要实现 AQS 中的 tryAcquire,如果你想实现共享同步锁,那么就实现 AQS 中的 tryAcquireShared 方法;

AQS核心思想(CLH队列锁)

拿到锁的线程在执行的时候,另外的线程需要排队,那么所有要排队的线程都打包成一个QNode(所有线程都会放入一个链表(QNode)中)

QNode包含三个元素:1、当前线程本身,2、myPred,3、locked;

myPred:链表上的指针,指向前驱节点;

locked:表示当前需要获得锁;

假设QNode中的线程A要获得锁,于是采用类似 CAS 的算法,把自己加在已有的链表的尾巴上,让 myPred 指向前一个节点,同时 locked 变成 true

线程A把自己挂在链表的尾部,形成一个新的尾节点,这样线程 A 也支持能锁了;其他线程也是重复这样的操作;

那么节点 A 和节点 B 怎么才能拿到锁呢?A 节点和 B 节点都有一个指针(myPred)指向前一个节点,myPred 本身会不停的自旋,检测前一个节点有没有释放掉锁,如果前一个节点的 locked = false 了,说明前一个节点已经把锁释放了,当前节点的线程可以拿到锁了;

myPred不会一直自旋下去,而是自旋一定的次数(一般是2-3次)之后,如果还没拿到锁,就会把当前线程挂起,进入阻塞状态,并不会一直不停的自旋下去;

自旋逻辑源码如下:

compareAndSetTail 进行入队操作,如果入队不成功,就会调用 enq() 进行自旋操作;

当自旋一定的次数之后,如果还不成功,就会挂起;

ReenTrantLock 公平锁、非公平锁

公平锁说的是:老老实实的在 QNode 链表队尾排队;

非公平锁说的是:可以插队;

显示锁中的公平锁(FairSync)和非公平锁(NonFairSync)的实现;

FairSync -> 拿锁之前会判断下当前链表中是不是有元素在等待;

NonFairSync -> 拿锁之前不判断,直接进行 compareAndSetState 进行拿锁;

ReenTrantLock的可重入

什么是可重入? 可以递归调用就是可重入;

JMM(Java Memory Model)

概念:现代 CPU为了提高运行速度,引入了一批 Cache(L1 2 3)「高速缓存区」,为了管理这批 Cache,Java 中单独提出了 Java 内存模型。预先将 CPU 需要的数据读取到 Cache 中;

三级缓存:

  • 第一级存放 cpu 最快使用的指令或者数据,只需要1.2ns;

  • 自带第二级缓存,需要5.5ns;

  • 多核 CPU 共享三级缓存 需要15.9ns;

Java内存模型中引入了两个抽象概念

工作内存、主内存 这两个抽象概念是很多存储设备的一个综合,比如说 这个工作内存包括了 CPU 寄存器、CPU 高速缓存、甚至还包括了主内存;工作内存 99% 包括了 CPU 高速缓存,1%可能包括了主内存;

例如:多线程执行累加操作,当声明一个 count 变量的时候,它会创建在主内存中,然后每个线程都将 count 复制一个变量副本到自己独享的工作内存中,JMM规定每个线程的工作内存是独享的,都只能操作自己工作内存中的这个 count 变量副本,操作完之后再写回主内存中,不允许线程访问主内存,也不允许线程访问其他线程的工作内存;

JMM 导致的并发安全问题

java 复制代码
public class VolatileTest {    
    public int count;    
    public void add() {        
        count ++;    
    }    

    private static class Count extends Thread {        
        VolatileTest volatileTest;        
        public Count(VolatileTest volatileTest) {            
            this.volatileTest = volatileTest;        
        }        
        @Override        
        public void run() {            
            super.run();            
            for (int i = 0; i < 10000; i++) {                
                volatileTest.add();            
            }        
        }    
    }    
    public static void main(String[] args) throws InterruptedException {        
        VolatileTest volatileTest = new VolatileTest();        
        Count count1 = new Count(volatileTest);        
        Count count2 = new Count(volatileTest);        
        count2.start();        
        count1.start();        
        Thread.sleep(50);        
        System.out.println(volatileTest.count);    
    }
}

打印结果并不是期望的 20000;

原因:当我们有两个线程执行 count = count + 1 的操作的时候,线程 A 将 count 从主内存读到自己的工作内存中,线程 B 也将 count 从主内存读到自己的工作内存中,分别在自己的工作内存中进行 +1 操作,操作完成之后,线程 A 和 线程 B 都要将结果写回到主内存中,理想的结果是 2,但是出现结果为 1 的现象,这就产生了线程不安全的问题;

所以 JMM 模型会牵扯到开发中常见的两个问题:可见性、原子性;

可见性:线程 A 改了 count 的值,线程 B 也改了 count 的值,但是线程 A 和线程 B 之间是互相看不到对方对 count 的修改,这就是线程A和线程B 所存在的可见性问题;

如何解决可见性问题:volatile 关键字;

volatile 关键字有三个作用:

  1. 强迫从主内存中读取一次 ;
  2. 变量修改之后强迫马上刷新到主内存 ;
  • 其修饰的共享变量进行写操作的时候,会使用 CPU 提供的 Lock: 前缀指令;

  • 这个 Lock: 将当前处理器缓存的数据写回到系统内存;

  • 这个写回内存的操作会使其他在 CPU 里缓存了该内存地址的数据无效;

  1. 抑制重排序保证可见性;

但是针对的是:一个线程写,多个线程读的时候 volatile 才没有问题;

我们给 count 加上 volatile 关键字,继续执行;

arduino 复制代码
public volatile int count;

当我们使用 volatile 关键字之后,结果仍然不是我们想要的结果,这就是所说的原子性问题;

volatile 只是强迫从内存中读了以及算完之后强迫写回内存,但是我们的计算过程(count++)并不是一次就能搞定的;

原子性:线程 B 在执行 count++ 的时候,由于这个操作(count++)不是原子操作,那么这个过程是可以被打断的(比如上下文切换),当 B 被打断的时候,A 可能又继续执行了,当 A 将新的数据写回主内存的时候,B 继续执行 count++ 操作,当执行完写回主内存的时候,就发生了数据异常,因为操作不是原子操作,就存在着被打断的可能,这就是原子性问题;

如何解决原子性问题:加锁;

也就是说:synchronized 同时保证了原子性和可见性,而 volatile 可以说是 JDK 提供的最最轻量级的同步机制,只能保证它的可见性,保证不了它的原子性;

操作流水线、指令重排序

现代 CPU 中有各种高速缓存,CPU 需要把数据读到高速缓存中,就会有一批数据在这个高速缓存区,那么现代 CPU 就引入了这个操作流水线和指令重排序,换句话说,CPU并不像我们想象的那样一次只能执行一条指令,它可以同时一次执行多条指令,例如:

而且 CPU 还可能提前就把 volatileTest.count 进行了获取,并把它放到重排序缓存区,等到真正执行到System.out.println() 的时候,再把它从重排序缓存区中取出放到指定的指令位置;

这就是操作流水线和指令重排序;在单线程上不管是CPU还是JVM,无论它怎么重排序,结果一定是符合我们的要求的,CPU和JMM都进行了保证;但是在多线程上,这种重排序就可能造成一种混乱的现象;

所以 volatile 还有一个功能就是:抑制重排序;

有 volatile 关键字修饰的变量进行写操作的时候会使用CPU提供的Lock前缀指令;

手写 ReenTrantLock,可重入

独占锁也是显示锁,而显示锁我们有在 如何应对Android面试官->线程和进程,手写ThreadLocal 中讲解,显示锁都实现了 Lock 接口,所以我们自定义的独占锁需要实现 Lock 接口;

java 复制代码
/** 
  * 线程、进程手写ThreadLocal章节有介绍显示锁都是实现的 Lock 接口 
  * */
public class CustomReetrantLock implements Lock {
    /** 
     * 仅需要将操作代理到 sync 上即可 
     * */
    private final Sync sync = new Sync();
    
    private static class Sync extends AbstractQueuedSynchronizer { 
   
        /**     
         * 判断锁是否处于占用状态     
         * */    
        @Override    
        protected boolean isHeldExclusively() {        
            return getState() == 1;    
        }
    
        /**      
         * 获取锁     
         * */    
        @Override    
        protected boolean tryAcquire(int arg) {        
            // 原子交换,将0修改为1,抢占锁        
            if (compareAndSetState(0, 1)) {    
                // 告诉其他线程,当前线程抢到了锁    
                setExclusiveOwnerThread(Thread.currentThread());    
                return true;
            } else if (getExclusiveOwnerThread() == Thread.currentThread()) {    
                setState(getState() + 1);
            }                   
            return false;    
        }    
        
        /**     
         * 释放锁     
         * */    
        @Override    
        protected boolean tryRelease(int arg) { 
           if (getExclusiveOwnerThread() != Thread.currentThread()) {    
                throw new IllegalMonitorStateException();
            }

            if (getState() == 0) {        
                throw new IllegalMonitorStateException();
            }
        
            setState(getState() - 1);
            if (getState() == 0) {
                setExclusiveOwnerThread(null);
            }                       
            return true;    
        }    
        Condition newCondition() {        
            return new ConditionObject();    
        }
    }

    /** 
     * 锁的拿取,代理到 Sync 上 
     * */
    @Override
    public void lock() {    
        sync.acquire(1);
    }

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

    @Override
    public boolean tryLock() {    
        return sync.tryAcquire(1);
    }
    
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {    
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    
    /** 
     * 锁的释放,代理到 Sync 上 
     * */
    @Override
    public void unlock() {    
        sync.release(1);
    }

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

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

简历润色

简历上可写:深度理解 AQS 原理和 volatile 关键字,可手写 ReentrantLock 核心实现;

下一章预告

带你玩转 synchronized 关键字;

相关推荐
Ray Liang1 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Java水解1 小时前
Java 中间件:Dubbo 服务降级(Mock 机制)
java·后端
砖厂小工3 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心4 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心4 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
SimonKing6 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean6 小时前
Jackson View Extension Spring Boot Starter
java·后端
Kapaseker6 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴7 小时前
Android17 为什么重写 MessageQueue
android
Seven977 小时前
剑指offer-79、最⻓不含重复字符的⼦字符串
java