Java面试系列-Java 并发(中)

3.3 JUC全局观
  • JUC - 类汇总和学习指南

JUC框架包含几个部分?

五个部分:

a2a4e5b494034853a325d5b466a23173.png

主要包含: (注意: 上图是网上找的图,无法表述一些继承关系,同时少了部分类;但是主体上可以看出其分类关系也够了)

  • Lock框架和Tools类(把图中这两个放到一起理解)

  • Collections: 并发集合

  • Atomic: 原子类

  • Executors: 线程池

Lock框架和Tools哪些核心的类?
d880e41462654bbcbb613055e16d7ae9.png
  • 接口: Condition, Condition为接口类型,它将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。可以通过await(),signal()来休眠/唤醒线程。

  • 接口: Lock,Lock为接口类型,Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。

  • 接口ReadWriteLock ReadWriteLock为接口类型, 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

  • 抽象类: AbstractOwnableSynchonizer AbstractOwnableSynchonizer为抽象类,可以由线程以独占方式拥有的同步器。此类为创建锁和相关同步器(伴随着所有权的概念)提供了基础。AbstractOwnableSynchronizer 类本身不管理或使用此信息。但是,子类和工具可以使用适当维护的值帮助控制和监视访问以及提供诊断。

  • 抽象类(long): AbstractQueuedLongSynchronizer AbstractQueuedLongSynchronizer为抽象类,以 long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本。此类具有的结构、属性和方法与 AbstractQueuedSynchronizer 完全相同,但所有与状态相关的参数和结果都定义为 long 而不是 int。当创建需要 64 位状态的多级别锁和屏障等同步器时,此类很有用。

  • 核心抽象类(int): AbstractQueuedSynchronizer AbstractQueuedSynchronizer为抽象类,其为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。

  • 锁常用类: LockSupport LockSupport为常用类,用来创建锁和其他同步类的基本线程阻塞原语。LockSupport的功能和"Thread中的 Thread.suspend()和Thread.resume()有点类似",LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程。但是park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。

  • 锁常用类: ReentrantLock ReentrantLock为常用类,它是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

  • 锁常用类: ReentrantReadWriteLock ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。

  • 锁常用类: StampedLock 它是java8在java.util.concurrent.locks新增的一个API。StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

  • 工具常用类: CountDownLatch CountDownLatch为常用类,它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

  • 工具常用类: CyclicBarrier CyclicBarrier为常用类,其是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

  • 工具常用类: Phaser Phaser是JDK 7新增的一个同步辅助类,它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。

  • 工具常用类: Semaphore Semaphore为常用类,其是一个计数信号量,从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。

  • 工具常用类: Exchanger Exchanger是用于线程协作的工具类, 主要用于两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了。

JUC并发集合哪些核心的类?
2b8fca61d7994d9482fb97ccd4186be5.png
  • Queue: ArrayBlockingQueue 一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。

  • Queue: LinkedBlockingQueue 一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。

  • Queue: LinkedBlockingDeque 一个基于已链接节点的、任选范围的阻塞双端队列。

  • Queue: ConcurrentLinkedQueue 一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。

  • Queue: ConcurrentLinkedDeque 是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。

  • Queue: DelayQueue 延时无界阻塞队列,使用Lock机制实现并发访问。队列里只允许放可以“延期”的元素,队列中的head是最先“到期”的元素。如果队里中没有元素到“到期”,那么就算队列中有元素也不能获取到。

  • Queue: PriorityBlockingQueue 无界优先级阻塞队列,使用Lock机制实现并发访问。priorityQueue的线程安全版,不允许存放null值,依赖于comparable的排序,不允许存放不可比较的对象类型。

  • Queue: SynchronousQueue 没有容量的同步队列,通过CAS实现并发访问,支持FIFO和FILO。

  • Queue: LinkedTransferQueue JDK 7新增,单向链表实现的无界阻塞队列,通过CAS实现并发访问,队列元素使用 FIFO(先进先出)方式。LinkedTransferQueue可以说是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集, 它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。

  • List: CopyOnWriteArrayList ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。

  • Set: CopyOnWriteArraySet 对其所有操作使用内部CopyOnWriteArrayList的Set。即将所有操作转发至CopyOnWriteArayList来进行操作,能够保证线程安全。在add时,会调用addIfAbsent,由于每次add时都要进行数组遍历,因此性能会略低于CopyOnWriteArrayList。

  • Set: ConcurrentSkipListSet 一个基于ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法。

  • Map: ConcurrentHashMap 是线程安全HashMap的。ConcurrentHashMap在JDK 7之前是通过Lock和segment(分段锁)实现,JDK 8 之后改为CAS+synchronized来保证并发安全。

  • Map: ConcurrentSkipListMap 线程安全的有序的哈希表(相当于线程安全的TreeMap);映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法。

JUC原子类哪些核心的类?

其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。

  • 原子更新基本类型

  • AtomicBoolean: 原子更新布尔类型。

  • AtomicInteger: 原子更新整型。

  • AtomicLong: 原子更新长整型。

  • 原子更新数组

  • AtomicIntegerArray: 原子更新整型数组里的元素。

  • AtomicLongArray: 原子更新长整型数组里的元素。

  • AtomicReferenceArray: 原子更新引用类型数组里的元素。

  • 原子更新引用类型

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。

  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。

  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。

  • AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述

  • 原子更新字段类

  • AtomicReference: 原子更新引用类型。

  • AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。

  • AtomicMarkableReferce: 原子更新带有标记位的引用类型。

JUC线程池哪些核心的类?
71e36e1815ec4a92b0a513ff25e6e749.png
  • 接口: Executor Executor接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。

  • ExecutorService ExecutorService继承自Executor接口,ExecutorService提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以关闭 ExecutorService,这将导致其停止接受新任务。关闭后,执行程序将最后终止,这时没有任务在执行,也没有任务在等待执行,并且无法提交新任务。

  • ScheduledExecutorService ScheduledExecutorService继承自ExecutorService接口,可安排在给定的延迟后运行或定期执行的命令。

  • AbstractExecutorService AbstractExecutorService继承自ExecutorService接口,其提供 ExecutorService 执行方法的默认实现。此类使用 newTaskFor 返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll 方法,默认情况下,RunnableFuture 是此包中提供的 FutureTask 类。

  • FutureTask FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。

  • 核心: ThreadPoolExecutor ThreadPoolExecutor实现了AbstractExecutorService接口,也是一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。 线程池可以解决两个不同问题: 由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。

  • 核心: ScheduledThreadExecutor ScheduledThreadPoolExecutor实现ScheduledExecutorService接口,可安排在给定的延迟后运行命令,或者定期执行命令。需要多个辅助线程时,或者要求 ThreadPoolExecutor 具有额外的灵活性或功能时,此类要优于 Timer。

  • 核心: Fork/Join框架 ForkJoinPool 是JDK 7加入的一个线程池类。Fork/Join 技术是分治算法(Divide-and-Conquer)的并行实现,它是一项可以获得良好的并行性能的简单且高效的设计技术。目的是为了帮助我们更好地利用多处理器带来的好处,使用所有可用的运算能力来提升应用的性能。

  • 工具类: Executors Executors是一个工具类,用其可以创建ExecutorService、ScheduledExecutorService、ThreadFactory、Callable等对象。它的使用融入到了ThreadPoolExecutor, ScheduledThreadExecutor和ForkJoinPool中。

3.4 JUC原子类
  • JUC原子类: CAS, Unsafe和原子类详解

线程安全的实现方法有哪些?

线程安全的实现方法包含:

  • 互斥同步: synchronized 和 ReentrantLock

  • 非阻塞同步: CAS, AtomicXXXX

  • 无同步方案: 栈封闭,Thread Local,可重入代码

什么是CAS?

CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。   简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。

相信sql大家都熟悉,类似sql中的条件更新一样:update set id=3 from table where id=2。因为单条sql执行具有原子性,如果有多个线程同时执行此sql语句,只有一条能更新成功。

CAS使用示例,结合AtomicInteger给出示例?

如果不使用CAS,在高并发下,多线程同时修改一个变量的值我们需要synchronized加锁(可能有人说可以用Lock加锁,Lock底层的AQS也是基于CAS进行获取锁的)。


public class Test {     private int i=0;     public synchronized int add(){         return i++;     } }

java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。


public class Test {     private  AtomicInteger i = new AtomicInteger(0);     public int add(){         return i.addAndGet(1);     } }
CAS会有哪些问题?

CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。

但使用 CAS 方式也会有几个问题:

  • ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。

从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  • 循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。

  • 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。

从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

AtomicInteger底层实现?
  • CAS+volatile

  • volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值CAS 保证数据更新的原子性。

请阐述你对Unsafe类的理解?

UnSafe类总体功能:

b58554b7b63246bbb540428ec59a4392.png

如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。

说说你对Java原子类的理解?

包含13个,4组分类,说说作用和使用场景。

  • 原子更新基本类型

  • AtomicBoolean: 原子更新布尔类型。

  • AtomicInteger: 原子更新整型。

  • AtomicLong: 原子更新长整型。

  • 原子更新数组

  • AtomicIntegerArray: 原子更新整型数组里的元素。

  • AtomicLongArray: 原子更新长整型数组里的元素。

  • AtomicReferenceArray: 原子更新引用类型数组里的元素。

  • 原子更新引用类型

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。

  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。

  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。

  • AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述

  • 原子更新字段类

  • AtomicReference: 原子更新引用类型。

  • AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。

  • AtomicMarkableReferce: 原子更新带有标记位的引用类型。

AtomicStampedReference是怎么解决ABA的?

AtomicStampedReference主要维护包含一个对象引用以及一个可以自动更新的整数"stamp"的pair对象来解决ABA问题。

3.5 JUC锁
  • JUC锁: LockSupport详解

  • JUC锁: 锁核心类AQS详解

  • JUC锁: ReentrantReadWriteLock详解

为什么LockSupport也是核心基础类?

AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)

通过wait/notify实现同步?

class MyThread extends Thread {          public void run() {         synchronized (this) {             System.out.println("before notify");                         notify();             System.out.println("after notify");             }     } }  public class WaitAndNotifyDemo {     public static void main(String[] args) throws InterruptedException {         MyThread myThread = new MyThread();                     synchronized (myThread) {             try {                         myThread.start();                 // 主线程睡眠3s                 Thread.sleep(3000);                 System.out.println("before wait");                 // 阻塞主线程                 myThread.wait();                 System.out.println("after wait");             } catch (InterruptedException e) {                 e.printStackTrace();             }                     }             } }

运行结果


before wait before notify after notify after wait

说明: 具体的流程图如下

45752ca8cb644647b59265d7abc74dda.png

使用wait/notify实现同步时,必须先调用wait,后调用notify,如果先调用notify,再调用wait,将起不了作用。具体代码如下


class MyThread extends Thread {     public void run() {         synchronized (this) {             System.out.println("before notify");                         notify();             System.out.println("after notify");             }     } }  public class WaitAndNotifyDemo {     public static void main(String[] args) throws InterruptedException {         MyThread myThread = new MyThread();                 myThread.start();         // 主线程睡眠3s         Thread.sleep(3000);         synchronized (myThread) {             try {                         System.out.println("before wait");                 // 阻塞主线程                 myThread.wait();                 System.out.println("after wait");             } catch (InterruptedException e) {                 e.printStackTrace();             }                     }             } }

运行结果:


before notify after notify before wait

说明: 由于先调用了notify,再调用的wait,此时主线程还是会一直阻塞。

通过LockSupport的park/unpark实现同步?

import java.util.concurrent.locks.LockSupport;  class MyThread extends Thread {     private Object object;      public MyThread(Object object) {         this.object = object;     }      public void run() {         System.out.println("before unpark");         try {             Thread.sleep(1000);         } catch (InterruptedException e) {             e.printStackTrace();         }         // 获取blocker         System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));         // 释放许可         LockSupport.unpark((Thread) object);         // 休眠500ms,保证先执行park中的setBlocker(t, null);         try {             Thread.sleep(500);         } catch (InterruptedException e) {             e.printStackTrace();         }         // 再次获取blocker         System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));          System.out.println("after unpark");     } }  public class test {     public static void main(String[] args) {         MyThread myThread = new MyThread(Thread.currentThread());         myThread.start();         System.out.println("before park");         // 获取许可         LockSupport.park("ParkAndUnparkDemo");         System.out.println("after park");     } }

运行结果:


before park before unpark Blocker info ParkAndUnparkDemo after park Blocker info null after unpark

说明: 本程序先执行park,然后在执行unpark,进行同步,并且在unpark的前后都调用了getBlocker,可以看到两次的结果不一样,并且第二次调用的结果为null,这是因为在调用unpark之后,执行了Lock.park(Object blocker)函数中的setBlocker(t, null)函数,所以第二次调用getBlocker时为null。

上例是先调用park,然后调用unpark,现在修改程序,先调用unpark,然后调用park,看能不能正确同步。具体代码如下


import java.util.concurrent.locks.LockSupport;  class MyThread extends Thread {     private Object object;      public MyThread(Object object) {         this.object = object;     }      public void run() {         System.out.println("before unpark");                 // 释放许可         LockSupport.unpark((Thread) object);         System.out.println("after unpark");     } }  public class ParkAndUnparkDemo {     public static void main(String[] args) {         MyThread myThread = new MyThread(Thread.currentThread());         myThread.start();         try {             // 主线程睡眠3s             Thread.sleep(3000);         } catch (InterruptedException e) {             e.printStackTrace();         }         System.out.println("before park");         // 获取许可         LockSupport.park("ParkAndUnparkDemo");         System.out.println("after park");     } }

运行结果:


before unpark after unpark before park after park

说明: 可以看到,在先调用unpark,再调用park时,仍能够正确实现同步,不会造成由wait/notify调用顺序不当所引起的阻塞。因此park/unpark相比wait/notify更加的灵活。

Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别? 重点
  • Thread.sleep()和Object.wait()的区别

首先,我们先来看看Thread.sleep()和Object.wait()的区别,这是一个烂大街的题目了,大家应该都能说上来两点。

  1. Thread.sleep()不会释放占有的锁,Object.wait()会释放占有的锁;

  1. Thread.sleep()必须传入时间,Object.wait()可传可不传,不传表示一直阻塞下去;

  1. Thread.sleep()到时间了会自动唤醒,然后继续执行;

  1. Object.wait()不带时间的,需要另一个线程使用Object.notify()唤醒;

  1. Object.wait()带时间的,假如没有被notify,到时间了会自动唤醒,这时又分好两种情况,一是立即获取到了锁,线程自然会继续执行;二是没有立即获取锁,线程进入同步队列等待获取锁;

其实,他们俩最大的区别就是Thread.sleep()不会释放锁资源,Object.wait()会释放锁资源。

  • Object.wait()和Condition.await()的区别

Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。

实际上,它在阻塞当前线程之前还干了两件事,一是把当前线程添加到条件队列中,二是“完全”释放锁,也就是让state状态变量变为0,然后才是调用LockSupport.park()阻塞当前线程。

  • Thread.sleep()和LockSupport.park()的区别 LockSupport.park()还有几个兄弟方法——parkNanos()、parkUtil()等,我们这里说的park()方法统称这一类方法。

  1. 从功能上来说,Thread.sleep()和LockSupport.park()方法类似,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;

  1. Thread.sleep()没法从外部唤醒,只能自己醒过来;

  1. LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;

  1. Thread.sleep()方法声明上抛出了InterruptedException中断异常,所以调用者需要捕获这个异常或者再抛出;

  1. LockSupport.park()方法不需要捕获中断异常;

  1. Thread.sleep()本身就是一个native方法;

  1. LockSupport.park()底层是调用的Unsafe的native方法;

  • Object.wait()和LockSupport.park()的区别

二者都会阻塞当前线程的运行,他们有什么区别呢? 经过上面的分析相信你一定很清楚了,真的吗? 往下看!

  1. Object.wait()方法需要在synchronized块中执行;

  1. LockSupport.park()可以在任意地方执行;

  1. Object.wait()方法声明抛出了中断异常,调用者需要捕获或者再抛出;

  1. LockSupport.park()不需要捕获中断异常;

  1. Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;

  1. LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;

park()/unpark()底层的原理是“二元信号量”,你可以把它相像成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证。

如果在wait()之前执行了notify()会怎样?

如果当前的线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时抛出IllegalMonitorStateException异常;

如果当前线程是此对象锁的所有者,wait()将一直阻塞,因为后续将没有其它notify()唤醒它。

如果在park()之前执行了unpark()会怎样?

线程不会被阻塞,直接跳过park(),继续执行后续内容

什么是AQS? 为什么它是核心?

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AbstractQueuedSynchronizer类底层的数据结构是使用CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。

f024842b5c91401eb2c8be8a031bd478.png
AQS的核心思想是什么?

底层数据结构: AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS有哪些核心的方法?

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
AQS定义什么样的资源获取方式?

AQS定义了两种资源获取方式:

  • 独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁非公平锁,如ReentrantLock)

  • 共享(多个线程可同时访问执行,如Semaphore、CountDownLatch、 CyclicBarrier )。ReentrantReadWriteLock可以看成是组合式,允许多个线程同时对某一资源进行读。

AQS底层使用了什么样的设计模式?

模板, 共享锁和独占锁在一个接口类中。

  • JUC锁: ReentrantLock详解

什么是可重入,什么是可重入锁? 它用来解决什么问题?

可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?

ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

3291e8c358dd43dfa5b7f3cc64a22a3e.png

说明: ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

ReentrantLock是如何实现公平锁的?

FairSync

ReentrantLock是如何实现非公平锁的?

UnFairSync

ReentrantLock默认实现的是公平还是非公平锁?

非公平锁

为了有了ReentrantLock还需要ReentrantReadWriteLock?

读锁和写锁分离:ReentrantReadWriteLock表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁ReadLock和写锁WriteLock,可以通过这两种锁实现线程间的同步。

ReentrantReadWriteLock底层实现原理?

ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。

f998924be8364738afd354be2b311b8f.png

说明: 如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类;ReadLock实现了Lock接口、WriteLock也实现了Lock接口。

ReentrantReadWriteLock底层读写状态如何设计的?

高16位为读锁,低16位为写锁

读锁和写锁的最大数量是多少?

2的16次方-1

本地线程计数器ThreadLocalHoldCounter是用来做什么的?

本地线程计数器,与对象绑定(线程-》线程重入的次数)

写锁的获取与释放是怎么实现的?

tryAcquire/tryRelease

读锁的获取与释放是怎么实现的?

tryAcquireShared/tryReleaseShared

什么是锁的升降级?

RentrantReadWriteLock为什么不支持锁升级? RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值