[Java复习04] 并发 JUC

时间:2021-12-06 17:05:56

Q1:为什么非常高的并发请求下AtomicLong的性能会有很大影响?有没有什么更好的替代方案?

虽然AtomicLong使用CAS但是CAS失败后还是通过无限循环的自旋锁不断尝试的,在高并发下N多线程同时去操作一个变量会造成大量线程CAS失败然后处于自旋状态,这大大浪费了CPU资源,降低了并发性。

JDK8提供的LongAdder。该类也可以保证Long类型操作的原子性,相对于AtomicLong,LongAdder有着更高的性能和更好的表现,可以完全替代AtomicLong的来进行原子操作。

低竞争下直接更新base,类似AtomicLong。高并发下,会将每个线程的操作hash到不同的cells数组中,从而将AtomicLong中更新一个value的行为优化之后,分散到多个value中从而降低更新热点,而需要得到当前值的时候,直接将所有cell中的value与base相加即可。

 

Q2: LockSupport工具类的作用?与Object的wait/notify的区别?

LockSupport提供一组公共静态方法,提供最基本的线程阻塞和唤醒功能。park()阻塞, unpark()唤醒。

区别:1.LockSupport不需要在同步代码块中,所以线程也不需要维护一个共享同步对象,实现线程解耦。

           2. Unpark可以先于park调用,不用担心线程间的执行先后顺序。

 

Q3: 锁是基于AQS实现,什么是AQS(抽象队列同步器),AQS框架的核心思想,实现流程?

AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的框架。

JUC包中几乎所有关于锁、多线程并发以及线程同步器等重要组件的实现都是基于AQS框架。

AQS提供一个模板框架维护了一个资源state(volatile int)和一个同步队列。

核心思想(原理简述):

1. 同步状态(synchronization state)管理

    基于volatile int state的变量表示同步状态,配合Unsafe工具类对其原子性操作来实现对当前锁状态的修改。暴露出getState、setState以及compareAndSetState操作来读取和更新这个状态。

2. 阻塞/唤醒线程的操作

    JUC包LockSupport类来作为线程阻塞和唤醒的工具

3. 通过FIFO队列来完成资源获取线程的排队工作

AQS框架包含两种可供选择的实现方式:独占(Exclusive)和共享(Share)。

AQS框架支持中断、超时,支持Condition条件等待。

独占锁流程:

首先调用acquire(1), 之后进入tryAcquire(1)尝试获取锁,若成功则返回。

若失败则将当前线程构造为Node节点,通过addWaiter(mode)里enq(node)自旋+CAS插入到同步队列尾部。

自旋时判断其前驱节点是否为头节点,并且是否成功获取同步状态,二者皆成立则当前节点设置为头节点,否则挂起当前线程等待被前驱节点唤醒。

共享锁流程:

首先调用acquireShared(1), 之后进入tryAcquireShared(1)获取同步状态,返回值不小于0则说明同步状态有剩余,获取成功直接返回。

若返回值小于0则说明获取同步状态失败,构造Node节点,自旋+CAS插入同步队列尾部,并检查前驱节点是否为头节点且成功获取同步状态, 若是则当前节点设置为头结点,否则挂起等待被前驱节点唤醒。

释放时调用releaseShared(acquires)释放同步状态,之后遍历整个队列唤醒所有后继节点。

独占锁和共享锁实现区别:

1. 独占锁的state=1,同一时刻只有一个线程成功获取同步状态。共享锁state>1,取值由自定义同步器决定。

2.独占锁队列头节点运行完毕释放锁后唤醒直接后继节点,共享锁唤醒所有后继节点。

 

Q4: 可重入锁(ReentrantLock)的实现?公平锁fair和非公平锁nofair?

ReentrantLock和synchronized都是可重入锁,synchronized由JVM实现,重入锁实现时最主要的逻辑是判断上次获取锁的线程是否为当前线程,ReentrantLock基于AQS实现,提供公平锁和非公平锁两种方式。

公平锁的实现逻辑如下,与非公平锁的区别为判断当前节点是否存在前驱节点,只有等待前驱节点释放后才能获取锁。

原理是将state变量的高16位和低16位拆分,高16位表示读锁(共享锁),低16位表示写锁(独占锁)。

 

Q5: ReentrantReadWriteLock读写锁的实现?

写锁实现:

获取同步状态,分离低16位的写锁状态。同步状态不为0,则存在读锁或写锁。若存在读锁则不能获取写锁,若当前线程不是上次获取写锁的线程,也不能获取写锁。通过以上判断后对低16位(写锁state)进行CAS修改,并把当前线程设置为写锁获取线程。

读锁实现:

获取同步状态,计算高16位位读锁状态+1后的值,若存在写锁且当前线程不是写锁获取者,则获取读锁失败。若上述判断都通过,则利用CAS重新设置读锁的同步状态。

锁降级:当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。

 

Q6:JDK8新增StampedLock?

StampedLock: 读写锁的改进版。读写锁虽然读写分离,但是读和写之间依然冲突,读锁会完全阻塞写锁,使用的是悲观锁策略。如果有大量读线程,写线程很少时,容易引起写线程饥饿。

StampedLock提供一种乐观锁的读策略,类似于无锁操作,完全不会阻塞写线程。

StampedLock是不可重入的。

StampedLock三种访问模式:Reading, Writing, Optimistic reading(乐观读模式):优化的读模式。

StampedLock提供读锁和写锁的相互转换。

 

Q6: CopyOnWriteArrayList为什么是线程安全的容器(相对于ArrayList)?底层实现原理?

CopyOnWriteArrayList 有什么优点?如何保证写时线程安全的?适用场景是什么?

 

是ArrayList的并发实现。底层是用volatile transient声明地数组 array , 保证读可见性。

 

ReentrantLock重入锁保证写操作同步。

 

写时复制,读写分离,写的时候复制出一个新数组,完成增删改后将新数组赋值给array。

 

利用了快照的概念从而使读和迭代器遍历操作无须同步加锁。读取是当前数组快照,所以不会出现ConcurrentModificationException异常。

 

增删改都需要获得锁,并且锁只有一把,而读操作不需要获得锁,支持并发读。

 

Q7:为什么增删改中都需要创建一个新的数组,操作完成之后再赋值给原来的引用?

 

为了保证get的时候,都能获取到元素,如果在增删改的过程直接修改原来的数组,可能会造成执行读操作获取不到数据。

 

适用场景:读取和遍历多,并不适合高并发写的场景,因为数组拷贝非常耗时。

 

Q8: 假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?

1. 用join  2. 利用JUC包下的CountDownLatch. 3. 利用JUC包下的CyclicBarrer

 

Q9:ConcurrentHashMap原理?

原理概述:

在ConcurrentHashMap中通过一个Node<K,V>[]数组来保存添加到map中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化(懒加载),否则如果只是初始化一个ConcurrentHashMap对象的话,只是设定了一个sizeCtl变量,这个变量用来判断对象的一些状态和是否需要扩容。

Put方法: 第一次添加元素的时候,默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,在会将该节点的链表转换成树。

Get方法:取元素的时候,通过计算hash来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断key和key的hash,取出value值。

扩容:通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过hash值的数组长度位来区分,是还是放在原来的位置还是放到扩容的长度的相同位置去 。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。

 

Q10:线程池的作用?

1. 不使用线程池的时候,线程的创建和销毁都需要开销。使用线程池,池里的线程可以复用,不用每次都重新创建和销毁线程。

2. 提供资源限制和管理手段,可以限制线程个数、动态新增线程等。

 

Q11: ThreadPoorExecutor的数据结构,原理?

1. ctl:线程池状态, AtomicInteger原子对象,记录“线程池中任务数量"和”线程池状态”,共32位,高3位表示"线程池状态",低29位表示"线程池中的任务数量"。

有RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。

corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;

maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;

keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;

unit:keepAliveTime的单位;

workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、*任务队列、优先任务队列几种;

threadFactory:线程工厂,用于创建线程,一般用默认即可;

handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

AbortPolicy策略(抛异常),CallerRunsPolicy策略(重试添加当前的任务),DiscardOledestPolicy策略(抛弃旧的任务 ),DiscardPolicy策略(抛弃当前的任务)

 

execute方法:若使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。