如何应对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 关键字;

相关推荐
ok!ko1 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2402_857589361 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰2 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer2 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
哎呦没2 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
编程、小哥哥3 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程4 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
网络研究院4 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
杨哥带你写代码4 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端