当前位置:   article > 正文

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

android aqs

AQS原理

什么是AQS?

AbstractQueuedSynchronizer,JDK提供的一个抽象类

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

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

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

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

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

模板方法设计模式

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

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

代码示例:

模板抽象类

  1. public abstract class AbsCar {
  2. public abstract void makeLight(); // 造车灯
  3. public abstract void makeDoor(); // 造车门
  4. public abstract void makeGlass(); // 造车玻璃
  5. public void make() {
  6. makeLight();
  7. makeDoor();
  8. makeGlass();
  9. }
  10. }

抽象实现子类1

  1. public class AudiCar extends AbsCar {
  2. @Override
  3. public void makeLight() {
  4. System.out.println("make audi light");
  5. }
  6. @Override
  7. public void makeDoor() {
  8. System.out.println("make audi door");
  9. }
  10. @Override
  11. public void makeGlass() {
  12. System.out.println("make audi glass");
  13. }
  14. }

抽象实现子类2

  1. public class BMWCar extends AbsCar {
  2. @Override
  3. public void makeLight() {
  4. System.out.println("make bmw light");
  5. }
  6. @Override
  7. public void makeDoor() {
  8. System.out.println("make bmw door");
  9. }
  10. @Override
  11. public void makeGlass() {
  12. System.out.println("make bmw glass");
  13. }
  14. }

抽象实现子类。。。

具体调用

  1. public class MakeCar {
  2. public static void main(String[] args) {
  3. AbsCar car = new AudiCar();
  4. AbsCar car1 = new BMWCar();
  5. car.make();
  6. car1.make();
  7. }
  8. }

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 导致的并发安全问题

  1. public class VolatileTest {
  2. public int count;
  3. public void add() {
  4. count ++;
  5. }
  6. private static class Count extends Thread {
  7. VolatileTest volatileTest;
  8. public Count(VolatileTest volatileTest) {
  9. this.volatileTest = volatileTest;
  10. }
  11. @Override
  12. public void run() {
  13. super.run();
  14. for (int i = 0; i < 10000; i++) {
  15. volatileTest.add();
  16. }
  17. }
  18. }
  19. public static void main(String[] args) throws InterruptedException {
  20. VolatileTest volatileTest = new VolatileTest();
  21. Count count1 = new Count(volatileTest);
  22. Count count2 = new Count(volatileTest);
  23. count2.start();
  24. count1.start();
  25. Thread.sleep(50);
  26. System.out.println(volatileTest.count);
  27. }
  28. }

打印结果并不是期望的 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 里缓存了该内存地址的数据无效;
  3. 抑制重排序保证可见性;

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

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

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 接口;

  1. /**
  2. * 线程、进程手写ThreadLocal章节有介绍显示锁都是实现的 Lock 接口
  3. * */
  4. public class CustomReetrantLock implements Lock {
  5. /**
  6. * 仅需要将操作代理到 sync 上即可
  7. * */
  8. private final Sync sync = new Sync();
  9. private static class Sync extends AbstractQueuedSynchronizer {
  10. /**
  11. * 判断锁是否处于占用状态
  12. * */
  13. @Override
  14. protected boolean isHeldExclusively() {
  15. return getState() == 1;
  16. }
  17. /**
  18. * 获取锁
  19. * */
  20. @Override
  21. protected boolean tryAcquire(int arg) {
  22. // 原子交换,将0修改为1,抢占锁
  23. if (compareAndSetState(0, 1)) {
  24. // 告诉其他线程,当前线程抢到了锁
  25. setExclusiveOwnerThread(Thread.currentThread());
  26. return true;
  27. } else if (getExclusiveOwnerThread() == Thread.currentThread()) {
  28. setState(getState() + 1);
  29. }
  30. return false;
  31. }
  32. /**
  33. * 释放锁
  34. * */
  35. @Override
  36. protected boolean tryRelease(int arg) {
  37. if (getExclusiveOwnerThread() != Thread.currentThread()) {
  38. throw new IllegalMonitorStateException();
  39. }
  40. if (getState() == 0) {
  41. throw new IllegalMonitorStateException();
  42. }
  43. setState(getState() - 1);
  44. if (getState() == 0) {
  45. setExclusiveOwnerThread(null);
  46. }
  47. return true;
  48. }
  49. Condition newCondition() {
  50. return new ConditionObject();
  51. }
  52. }
  53. /**
  54. * 锁的拿取,代理到 Sync 上
  55. * */
  56. @Override
  57. public void lock() {
  58. sync.acquire(1);
  59. }
  60. @Override
  61. public void lockInterruptibly() throws InterruptedException {
  62. sync.acquireInterruptibly(1);
  63. }
  64. @Override
  65. public boolean tryLock() {
  66. return sync.tryAcquire(1);
  67. }
  68. @Override
  69. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  70. return sync.tryAcquireNanos(1, unit.toNanos(time));
  71. }
  72. /**
  73. * 锁的释放,代理到 Sync 上
  74. * */
  75. @Override
  76. public void unlock() {
  77. sync.release(1);
  78. }
  79. @Override
  80. public Condition newCondition() {
  81. return sync.newCondition();
  82. }
  83. public boolean isLocked() {
  84. return sync.isHeldExclusively();
  85. }
  86. public boolean hasQueuedThreads() {
  87. return sync.hasQueuedThreads();
  88. }
  89. }

简历润色

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

下一章预告

带你玩转 synchronized 关键字;

欢迎三连

来都来了,点个关注、点个赞吧~~ 你的支持是我最大的动力

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/271679
推荐阅读
相关标签
  

闽ICP备14008679号