一些收藏

突击并发编程JUC系列-万字长文解密 JUC 面试题

2020-10-26  本文已影响0人  山间木匠1

突击并发编程JUC系列演示代码地址:
https://github.com/mtcarpenter/JavaTutorial

什么是 CAS 吗?

CAS(Compare And Swap)指比较并交换。CAS算法CAS(V, E, N)包含 3 个参数,V 表示要更新的变量,E 表示预期的值,N 表示新值。在且仅在 V 值等于 E值时,才会将 V 值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS 返回当前 V 的真实值。Concurrent包下所有类底层都是依靠CAS操作来实现,而sun.misc.Unsafe为我们提供了一系列的CAS操作。

CAS 有什么缺点?

对 CAS 中的 ABA 产生有解决方案吗?

什么是 ABA 问题呢?多线程环境下。线程 1 从内存的V位置取出 A ,线程 2 也从内存中取出 A,并将 V 位置的数据首先修改为 B,接着又将 V 位置的数据修改为 A,线程 1 在进行CAS操作时会发现在内存中仍然是 A,线程 1 操作成功。尽管从线程 1 的角度来说,CAS操作是成功的,但在该过程中其实 V 位置的数据发生了变化,线程 1 没有感知到罢了,这在某些应用场景下可能出现过程数据不一致的问题。

可以版本号(version)来解决 ABA 问题的,在 atomic 包中提供了AtomicStampedReference 这个类,它是专门用来解决 ABA 问题的。

直达链接: AtomicStampedReference ABA 案例链接

CAS 自旋导致的问题?

由于单次 CAS 不一定能执行成功,所以 CAS往往是配合着循环来实现的,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。

CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的。

CAS 范围不能灵活控制

不能灵活控制线程安全的范围。只能针对某一个,而不是多个共享变量的,不能针对多个共享变量同时进行 CAS操作,因为这多个变量之间是独立的,简单的把原子操作组合到一起,并不具备原子性。

什么是 AQS 吗?

AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的SynchronizedReentrantLockReentrantReadWriteLockSemaphoreCountDownLatch等。该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如RetreenLock)。

了解 AQS 共享资源的方式吗?

Atomic 原子更新

JavaJDK1.5 开始提供了 java.util.concurrent.atomic 包,方便程序员在多线程环 境下,无锁的进行原子操作。在 Atomic 包里一共有 12 个类,四种原子更新方式,分别是原子更新基本类型原子更新数组原子更新引用原子更新字段。在 JDK 1.8 之后又新增几个原子类。如下如:

针对思维导图知识点在前面的章节都进行了理论+实践的讲解,到达地址如下:

突击并发编程JUC系列-原子更新AtomicLong

突击并发编程JUC系列-数组类型AtomicLongArray

突击并发编程JUC系列-原子更新字段类AtomicStampedReference

突击并发编程JUC系列-JDK1.8 扩展类型 LongAdder

列举几个AtomicLong 的常用方法

说说 AtomicInteger 和 synchronized 的异同点?

相同点

不同点

原子类和 volatile 有什么异同?

AtomicLong 可否被 LongAdder 替代?

有了更高效的 LongAdder,那AtomicLong 可否不使用了呢?是否凡是用到 AtomicLong的地方,都可以用LongAdder替换掉呢?答案是不是的,这需要区分场景。

LongAdder 只提供了 addincrement 等简单的方法,适合的是统计求和计数的场景,场景比较单一,而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要CAS 的场景。

结论:如果我们的场景仅仅是需要用到加和减操作的话,那么可以直接使用更高效的 LongAdder,但如果我们需要利用 CAS 比如compareAndSet 等操作的话,就需要使用 AtomicLong 来完成。

直达链接:突击并发编程JUC系列-JDK1.8 扩展类型 LongAdder

并发工具

CountDownLatch

CountDownLatch基于线程计数器来实现并发访问控制,主要用于主线程等待其他子线程都执行完毕后执行相关操作。其使用过程为:在主线程中定义CountDownLatch,并将线程计数器的初始值设置为子线程的个数,多个子线程并发执行,每个子线程在执行完毕后都会调用countDown函数将计数器的值减1,直到线程计数器为0,表示所有的子线程任务都已执行完毕,此时在CountDownLatch上等待的主线程将被唤醒并继续执行。

突击并发编程JUC系列-并发工具 CountDownLatch

CyclicBarrier

CyclicBarrier(循环屏障)是一个同步工具,可以实现让一组线程等待至某个状态之后再全部同时执行。在所有等待线程都被释放之后,CyclicBarrier可以被重用。CyclicBarrier的运行状态叫作Barrier状态,在调用await方法后,线程就处于Barrier状态。

CyclicBarrier中最重要的方法是await方法,它有两种实现。

突击并发编程JUC系列-并发工具 CyclicBarrier

Semaphore

Semaphore指信号量,用于控制同时访问某些资源的线程个数,具体做法为通过调用acquire()获取一个许可,如果没有许可,则等待,在许可使用完毕后通过release()释放该许可,以便其他线程使用。

突击并发编程JUC系列-并发工具 Semaphore

CyclicBarrier 和 CountdownLatch 有什么异同?

相同点:都能阻塞一个或一组线程,直到某个预设的条件达成发生,再统一出发。
但是它们也有很多不同点,具体如下。

CountDownLatch、CyclicBarrier、Semaphore的区别如下。

locks

公平锁与非公平锁

ReentrantLock支持公平锁和非公平锁两种方式。公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。非公平锁指JVM遵循随机、就近原则分配锁的机制。ReentrantLock通过在构造函数ReentrantLock(boolean fair)中传递不同的参数来定义不同类型的锁,默认的实现是非公平锁。这是因为,非公平锁虽然放弃了锁的公平性,但是执行效率明显高于公平锁。如果系统没有特殊的要求,一般情况下建议使用非公平锁。

synchronized 和 lock 有什么区别?

synchronized 和 Lock 如何选择?

不同点:

使用

Lock接口的主要方法

tryLock、lock和lockInterruptibly的区别

tryLocklocklockInterruptibly的区别如下。

突击并发编程JUC系列-ReentrantLock

ReentrantReadWriteLock 读写锁的获取规则

要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。也可以总结为:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)

ReentrantLock 适用于一般场合,ReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率。

突击并发编程JUC系列-ReentrantReadWriteLock

读锁应该插队吗?什么是读写锁的升降级?

ReentrantReadWriteLock 的实现选择了“不允许插队”的策略,这就大大减小了发生“饥饿”的概率。

插队策略

升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁。

怎么防止死锁?

Condition 类和 Object 类锁方法区别区别

并发容器

为什么 ConcurrentHashMap 比 HashTable 效率要高?

ConcurrentHashMap JDK 1.7/JDK 1.8

JDK 1.7 结构

JDK 1.7 中的ConcurrentHashMap 内部进行了 Segment分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。
相比于之前的 Hashtable 每次操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁。
每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

JDK 1.8 结构

图中的节点有三种类型:

链表长度大于某一个阈值(默认为 8),满足容量从链表的形式转化为红黑树的形式。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色,红黑树的本质是对二叉查找树 BST 的一种平衡策略,我们可以理解为是一种平衡二叉查找树,查找效率高,会自动平衡,防止极端不平衡从而影响查找效率的情况发生,红黑树每个节点要么是红色,要么是黑色,但根节点永远是黑色的。

ConcurrentHashMap 中 get 的过程

ConcurrentHashMap 中 put 的过程

突击并发编程JUC系列-并发容器ConcurrentHashMap

什么是阻塞队列?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

列举几个常见的阻塞队列

突击并发编程JUC系列-阻塞队列 BlockingQueue

线程池

使用线程池的优势

Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。

线程池的实现原理

当提交一个新任务到线程池时,线程池的处理流程如下:

ThreadPoolExecutor执行execute()方法的示意图 如下:

ThreadPoolExecutor执行execute方法分下面 4 种情况:

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤 2,而步骤2不需要获取全局锁。

创建线程有三种方式:

线程有哪些状态?

线程池的状态有那些?

线程池中 sumbit() 和 execute() 方法有什么区别?

Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。

线程池创建的方式

上面 7 种创建方式中,前 6 种 通过Executors工厂方法创建,ThreadPoolExecutor 手动创建。

ThreadPollExecutor 构造方法

下面介绍下 ThreadPoolExecutor 接收 7 个参数的构造方法

/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列
                              ThreadFactory threadFactory,//线程工厂
                              RejectedExecutionHandler handler//拒绝策略
                               )

欢迎关注公众号 山间木匠 , 我是小春哥,从事 Java 后端开发,会一点前端、通过持续输出系列技术文章以文会友,如果本文能为您提供帮助,欢迎大家关注、在看、 点赞、分享支持,我们下期再见!<br />

上一篇下一篇

猜你喜欢

热点阅读