AQS

什么是AQS

AbstractQueueSynchronizer(抽象队列同步器)

是用来构建锁和其他同步组件的基础架构,比如ReentrantLock、ReetrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用一个int成员变量state标识同步状态,通过内置的FIFO(队列)来完成获取资源线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现两种同步方式:独占式、共享式
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方式来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要其中几个可重写的方法,如tryAcquire、tryRelease、tryReleaseShared等等。
这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者和同步组件交互的接口(比如可以允许两个线程并访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者锁需关注的领域。
在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node节点组成,每个Node节点维护一个pre引用和next引用,分别指向自己的前序和后继,构成一个双端双向链表。同时与Condition相关的等待队列,节点类型也是Node,构成了一个单向链表。

AQS两种资源共享方式

1.Exclusive->独占,只有一个线程能执行,如ReentrantLock
2.Share->共享,多个线程可同时执行,如Semaphore/CountDownLatch

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取和释放方式即可。
同步器实现时主要实现以下几种方法:

  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true。
  • tryAcquireShared(int):共享方式。尝试获取资源;负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则返回false。
  • isHeldExclusively:该线程是否正在独占资源。只有用到condition才需要去实现它。

AQS重要源码预览

image.png
重要的参数state
image.png

Lock源码

image.png
image.png
image.png
image.png
image.png
独占式的
image.png
image.png

image.png
image.png

AQS的基本思想CLH队列锁

image.png
CLH队列锁
1.基于链表的自旋锁
2.想要获得锁,通过CAS操作将QNode挂在链表的尾部
3.每个QNode都在自旋检查(有自旋次数限制,超过后当前线程将进入阻塞状态)myPred是否释放锁
image.png
image.png
image.png

公平锁和非公平锁的介绍

image.png
唯一的区别
公平锁中会判断队列中是都有线程在等待
image.png

锁的可重入

我们再回头看看Lock的实现
image.png
什么时候需要锁的可重入?

当我们需要再次进入该方法时

实现锁的可重入,否则会自己锁住自己
获取锁的时候state+1,释放锁的时候state-1
image.png

浅谈JMM

计算机在执行程序时,每条指令都是在CPU中执行的。而执行指令的过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程,跟CPU执行指令的速度比起来要慢的多(硬盘 < 内存 <缓存cache < CPU)。因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时,就可以直接从它的高速缓存中读取数据或向其写入数据了。当运算结束之后,再将高速缓存中的数据刷新到主存当中。
image.png
image.png
线程不允许直接访问主内存,工作内存之间相互独立(类似于ThreadLocal),工作内存中存主内存变量的副本。
但是如此构建会带来数据同步方面的问题。
image.png

其实,JMM具可见性、原子性、有序性,那么这是怎么实现的呢?

  • volatile关键字 使变量保持可见性
  • synchronized关键字 为操作加锁(只是用synchronized同样能保证安全)
  • 一个线程写,多个线程读,使用volatile是OK的。

volatile详解

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似count++这种复合操作不具有原子性。
  • volatile还能防止指令重排序 关于流水线和重排序 intel十级流水线 比如下例可以先算t=b再执行if,提高性能。 但是在多线程中可能会引发多线程安全问题,所以volatile的抑制重排序就有了作用,如在DCL (双重检测锁定)中的作用?->防止指令重排序
    image.png

volatile的实现原理

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

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

synchronized实现原理

相信大家都用过synchronized,特别是单例设计模式中很常见

原理:使用monitorenter和monitorexit指令实现的

  • monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
  • 每个monitorenter必须有对应的monitorexit与之匹配
  • 任何对象都有一个monitor与之关联
    image.png

synchronized做了哪些优化

锁消除:和逃逸分析比较紧密,如果在编译过程中发现某个代码块不会发生共享数据竞争,那么就会取消锁。
锁粗化:如下图所示,未被加锁的代码块会被包裹进synchronized,减少线程上下文切换带来的开销。
image.png
逃逸分析:发现加锁的对象只在某个方法内部,对变量做一些相关优化

主要部分:
我们来了解锁的四种状态

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

偏向锁

大多数情况下,锁不存在多线程竞争,而且总是由同一线程多次获得,为了让锁的代价更低而引入了偏向锁。无锁竞争时不需要进行CAS操作来加锁和释放锁。
第一次还是需要CAS完成Mark Word的替换 替换如下信息
image.png

轻量级锁

image.png
如果发生锁的竞争,线程2会先stop the world,然后更改线程1的信息(由偏向锁升级为轻量级锁),这就导致性能降低。这就需要禁止偏向锁。
轻量级锁会自旋,当自旋到一定次数后就会发生膨胀,膨胀为重量级锁。

重量级锁

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间。 同步块执行速度非常快。
重量级 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间满。 追求吞吐量。同步块执行速度较快。

其他

synchronized修饰普通方法和静态方法的区别?什么是可见性?

  • 锁的对象实例
    image.png
  • 锁的当前类唯一的.class对象 ->类锁
    1654312536892
  • 假如有两个线程分别执行上面各个方法,可以同时执行,拿的不是同一个对象锁。
  • 注意:加锁加的是对象锁,只要不是同一个对象锁,并行执行就不会有问题。

单例模式中的懒汉式-延迟初始化占位类模式

为什么饿汉式与延迟占位类模式是线程安全的?

这是由类加载机制所保证的,一个类加载只会在虚拟机中执行一次,所以虚拟机为了保证执行性一次在类加载过程中已经实现了线程安全。
image.png

Lock和synchronized的区别?

1、lock是一个接口,而synchronized是java的一个关键字。

2、synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;而lock发生异常时,不会主动释放占有的锁,必须手动来释放锁,可能引起死锁的发生。

什么是守护线程?你是如何退出一个线程的?

守护(Daemon)线程是一种支持性线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
image.png
线程的停止:

  • run方法执行完成
  • 执行过程中出现异常,终止
  • 不建议使用stop方法,不安全(不考虑线程当前的执行状态直接结束)
  • interrupt,协议机制 ,被中断的线程内部方法isInterrupted()或Thread.interrupted()判断线程是否进行中断。

sleep、wait、yield的区别,wait的线程如何唤醒它?(notify,notifyAll)

  • yield方法让出CPU的执行权,什么时候拿回来由操作系统决定,使当前线程进入就绪状态(注意:yield()方法只会给优先级相同,或优先级更高的线程执行机会)。(让出CPU的执行权)

  • sleep让当前线程 进行休眠,使当前线程进入阻塞状态,直到经过阻塞时间才会转入就绪状态(通常用于暂停线程的执行)

  • wait让当前线程等待,这三者只有wait方法会释放锁,唤醒后重新竞争锁。(主要用于线程之间的交互)

    wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)

sleep是可中断的吗?

是,调用Thread.sleep()会抛出一个中断异常,说明是可以中断的。

有三个线程T1、T2、T3,怎么确保它们按顺序执行?

加入执行顺序:T1->T2->T3
T3方法中T2.join()
T2方法中T1.join()

线程的六种状态

  • sleep和wait都可设置时间
  • 只有synchronized才会使线程进入阻塞态
  • Lock进入等待或超时等待态

image.png

手写死锁

image.png