JUC并发包与容器类 - 面试题(一网打净,持续更新)


JUC 高并发工具类(3文章)与高并发容器类(N文章) :

内存可见性、指令有序性 理论

Java内存模型

重排序与数据依赖性

为何代码会重排序?

在执行程序时,为了提供性能,处理器和编译器经常会对指令进行重排序,可是不能随意重排序,不是你想怎么排序就怎么排序,它须要知足如下两个条件:面试

  • 在单线程环境下不能改变程序运行的结果;
  • 存在数据依赖关系的不容许重排序

须要注意的是:重排序不会影响单线程环境的执行结果,可是会破坏多线程的执行语义。算法

as-if-serial规则和happens-before规则的区别

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  • as-if-serial语义和happens-before这么作的目的,都是为了在不改变程序执行结果的前提下,尽量地提升程序执行的并行度。

volatile 内存可见性

volatile 关键字的做用

对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其余线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会当即被更新到主存,当有其余线程须要读取时,它会去内存中读取新值。数据库

从实践角度而言,volatile 的一个重要做用就是和 CAS 结合,保证了原子性,详细的能够参见 java.util.concurrent.atomic 包下的类,好比 AtomicInteger。编程

volatile 经常使用于多线程环境下的单次操做(单次读或者单次写)。api

Java 中能建立 volatile 数组吗?

能,Java 中能够建立 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,若是改变引用指向的数组,将会受到 volatile 的保护,可是若是多个线程同时改变数组的元素,volatile 标示符就不能起到以前的保护做用了。数组

volatile 变量和 atomic 变量有什么不一样?

volatile 变量能够确保先行关系,即写操做会发生在后续的读操做以前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操做就不是原子性的。缓存

而 AtomicInteger 类提供的 atomic 方法可让这种操做具备原子性如getAndIncrement()方法会原子性的进行增量操做把当前值加一,其它数据类型和引用变量也能够进行类似操做。

volatile 能使得一个非原子操做变成原子操做吗?

关键字volatile的主要做用是使变量在多个线程间可见,但没法保证原子性,对于多个线程访问同一个实例变量须要加锁进行同步。

虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double能够保证其操做原子性。

因此从Oracle Java Spec里面能够看到:

  • 对于64位的long和double,若是没有被volatile修饰,那么对其操做能够不是原子的。在操做的时候,能够分红两步,每次对32位操做。
  • 若是使用volatile修饰long和double,那么其读写都是原子操做
  • 对于64位的引用地址的读写,都是原子操做
  • 在实现JVM时,能够自由选择是否把读写long和double做为原子操做
  • 推荐JVM实现为原子操做

volatile 修饰符的有过什么实践?

单例模式

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:较复杂

描述:对于Double-Check这种可能出现的问题(固然这种几率已经很是小了,但毕竟仍是有的嘛~),解决方案是:只须要给instance的声明加上volatile关键字便可volatile关键字的一个做用是禁止指令重排,把instance声明为volatile以后,对它的写操做就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成以前,就不用会调用读操做。注意:volatile阻止的不是singleton = newSingleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操做([1-2-3])完成以前,不会调用读操做(if (instance == null))。

public class Singleton7 {

    private static volatile Singleton7 instance = null;

    private Singleton7() {}

    public static Singleton7 getInstance() {
        if (instance == null) {
            synchronized (Singleton7.class) {
                if (instance == null) {
                    instance = new Singleton7();
                }
            }
        }

        return instance;
    }

}
12345678910111213141516171819

synchronized 和 volatile 的区别是什么?

synchronized 表示只有一个线程能够获取做用对象的锁,执行代码,阻塞其余线程。

volatile 表示变量在 CPU 的寄存器中是不肯定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

区别

  • volatile 是变量修饰符;synchronized 能够修饰类、方法、变量。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则能够保证变量的修改可见性和原子性。
  • volatile 不会形成线程的阻塞;synchronized 可能会形成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化。
  • volatile关键字是线程同步的轻量级实现,因此volatile性能确定比synchronized关键字要好。可是volatile关键字只能用于变量而synchronized关键字能够修饰方法以及代码块。synchronized关键字在JavaSE1.6以后进行了主要包括为了减小得到锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各类优化以后执行效率有了显著提高,实际开发中使用 synchronized 关键字的场景仍是更多一些

final

什么是不可变对象,它对写并发应用有什么帮助?

不可变对象(Immutable Objects)即对象一旦被建立它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。

不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。

只有知足以下状态,一个对象才是不可变的;

  • 它的状态不能在建立后再被修改;
  • 全部域都是 final 类型;而且,它被正确建立(建立期间没有发生 this 引用的逸出)。

不可变对象保证了对象的内存可见性,对不可变对象的读取不须要进行额外的同步手段,提高了代码执行效率。

GC

Java中垃圾回收有什么目的?何时进行垃圾回收?

垃圾回收是在内存中存在没有引用的对象或超过做用域的对象时进行的。

垃圾回收的目的是识别而且丢弃应用再也不使用的对象来释放和重用资源。

若是对象的引用被置为null,垃圾收集器是否会当即释放对象占用的内存?

不会,在下一个垃圾回调周期中,这个对象将是被可回收的。

也就是说并不会当即被垃圾收集器马上回收,而是在下一次垃圾回收时才会释放其占用的内存。

finalize()方法何时被调用?析构函数(finalization)的目的是什么?

1)垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;
finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { }
在垃圾回收器执行时会调用被回收对象的finalize()方法,能够覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,而且下一次垃圾回收动做发生时,才真正回收对象占用的内存空间

2)GC原本就是内存回收了,应用还须要在finalization作什么呢? 答案是大部分时候,什么都不用作(也就是不须要重载)。只有在某些很特殊的状况下,好比你调用了一些native的方法(通常是C写的),能够要在finaliztion里去调用C的释放函数。

CAS原子操做

什么是 CAS

CAS 是 compare and swap 的缩写,即咱们所说的比较交换。

cas 是一种基于锁的操做,并且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个以前得到锁的线程释放锁以后,下一个线程才能够访问。而乐观锁采起了一种宽泛的态度,经过某种方式不加锁来处理资源,好比经过给记录加 version 来获取数据,性能较悲观锁有很大的提升。

CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。若是内存地址里面的值和 A 的值是同样的,那么就将内存里面的值更新成 B。CAS是经过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程须要自旋,到下次循环才有可能机会执行。

java.util.concurrent.atomic 包下的类大可能是使用 CAS 操做来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

CAS 的会产生什么问题?

一、ABA 问题:

好比说一个线程 one 从内存位置 V 中取出 A,这时候另外一个线程 two 也从内存中取出 A,而且 two 进行了一些操做变成了 B,而后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操做发现内存中仍然是 A,而后 one 操做成功。尽管线程 one 的 CAS 操做成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

二、循环时间长开销大:

对于资源竞争严重(线程冲突严重)的状况,CAS 自旋的几率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

三、只能保证一个共享变量的原子操做:

当对一个共享变量执行操做时,咱们可使用循环 CAS 的方式来保证原子操做,可是对多个共享变量操做时,循环 CAS 就没法保证操做的原子性,这个时候就能够用锁。

Lock显示锁

Lock 接口(Lock interface)是什么?对比同步它有什么优点?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操做。他们容许更灵活的结构,能够具备彻底不一样的性质,而且能够支持多个相关类的条件对象。

它的优点有:

(1)可使锁更公平

(2)可使线程在等待锁的时候响应中断

(3)可让线程尝试获取锁,并在没法获取锁的时候当即返回或者等待一段时间

(4)能够在不一样的范围,以不一样的顺序获取和释放锁

总体上来讲 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操做。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,固然,在大部分状况下,非公平锁是高效的选择。

乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁:老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。再好比 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库提供的相似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式:

一、使用版本标识来肯定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时能够采起丢弃和再次尝试的策略。

二、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。 CAS 操做中包含三个操做数 —— 须要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。若是内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。不然处理器不作任何操做。

ReentrantLock(重入锁)实现原理与公平锁非公平锁区别

什么是可重入锁(ReentrantLock)?

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示可以对共享资源可以重复加锁,即当前线程获取该锁再次获取不会被阻塞。

在java关键字synchronized隐式支持重入性,synchronized经过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完彻底全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。

重入性的实现原理

要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,若是已经获取锁的线程是当前线程的话则直接再次获取成功;2. 因为锁会被获取n次,那么只有锁在被释放一样的n次以后,该锁才算是彻底释放成功

ReentrantLock支持两种锁:公平锁非公平锁何谓公平性,是针对获取锁而言的,若是一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,知足FIFO

读写锁ReentrantReadWriteLock源码分析

ReadWriteLock 是什么

首先明确一下,不是说 ReentrantLock 很差,只是 ReentrantLock 某些时候有局限。若是使用 ReentrantLock,可能自己是为了防止线程 A 在写数据、线程 B 在读数据形成的数据不一致,但这样,若是线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,可是仍是加锁了,下降了程序的性能。由于这个,才诞生了读写锁 ReadWriteLock。

ReadWriteLock 是一个读写锁接口,读写锁是用来提高并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提高了读写的性能。

而读写锁有如下三个重要的特性:

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量仍是非公平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁可以降级成为读锁。

AQS抽象同步队列

AQS 介绍

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。

AQS类

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用普遍的大量的同步器,好比咱们提到的ReentrantLock,Semaphore,其余的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。固然,咱们本身也能利用AQS很是轻松容易地构造出符合咱们本身需求的同步器。

AQS 原理分析

下面大部份内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话能够看看源码。

AQS 原理概览

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

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

看个AQS(AbstractQueuedSynchronizer)原理图:

AQS原理图

AQS使用一个int成员变量来表示同步状态,经过内置的FIFO队列来完成获取资源线程的排队工做。AQS使用CAS对该同步状态进行原子操做实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性
1

状态信息经过protected类型的getState,setState,compareAndSetState进行操做

//返回同步状态的当前值
protected final int getState() {  
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操做)将同步状态值设置为给定值update若是当前同步状态的值等于expect(指望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
123456789101112

AQS 对资源的共享方式

AQS定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 咱们都会在后面讲到。

ReentrantReadWriteLock 能够当作是组合式,由于ReentrantReadWriteLock也就是读写锁容许多个线程同时对某一资源进行读。

不一样的自定义同步器争用共享资源的方式也不一样。自定义同步器在实现时只须要实现共享资源 state 的获取与释放方式便可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

AQS底层使用了模板方法模式

同步器的设计是基于模板方法模式的,若是须要自定义同步器通常的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

这和咱们以往经过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

AQS使用了模板方法模式,自定义同步器时须要重写下面几个AQS提供的模板方法:

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

123456

默认状况下,每一个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,而且一般应该简短而不是阻塞。AQS类中的其余方法都是final ,因此没法被其余类使用,只有这几个方法能够被其余类使用。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其余线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。固然,释放锁以前,A线程本身是能够重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每一个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到全部子线程都执行完后(即state=0),会unpark()主调用线程,而后主调用线程就会从await()函数返回,继续后余动做。

通常来讲,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种便可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

高并发容器

并发容器之ConcurrentHashMap详解(JDK1.8版本)与源码分析

什么是ConcurrentHashMap?

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发若是要用map结构,那第一时间想到的就是它。相对于hashmap来讲,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提升了并发度。

那么它究竟是如何实现线程安全的?

JDK 1.6版本关键要素:

  • segment继承了ReentrantLock充当锁的角色,为每个segment提供了线程安全的保障;
  • segment维护了哈希散列表的若干个桶,每一个桶由HashEntry构成的链表。

JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性

Java 中 ConcurrentHashMap 的并发度是什么?

ConcurrentHashMap 把实际 map 划分红若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度得到的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程状况下就能避免争用。

在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提升并发度,具体内容仍是查看源码吧。

什么是并发容器的实现?

何为同步容器:能够简单地理解为经过 synchronized 来实现同步的容器,若是有多个线程调用同步容器的方法,它们将会串行执行。好比 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。能够经过查看 Vector,Hashtable 等这些同步容器的实现代码,能够看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在须要同步的方法上加上关键字 synchronized。

并发容器使用了与同步容器彻底不一样的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,能够称为分段锁,在这种锁机制下,容许任意数量的读线程并发地访问 map,而且执行读操做的线程和写操做的线程也能够并发的访问 map,同时容许必定数量的写操做线程并发地修改 map,因此它能够在并发环境下实现更高的吞吐量。

Java 中的同步集合与并发集合有什么区别?

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 以前程序员们只有同步集合来用且在多线程并发的时候会致使争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不只提供线程安全还用锁分离和内部分区等现代技术提升了可扩展性。

SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保证线程安全,因此每次只能有一个线程来访为 map。

ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等经常使用操做只锁当前须要用到的桶。

这样,原来只能一个线程进入,如今却能同时有 16 个写线程执行,并发性能的提高是显而易见的。

另外 ConcurrentHashMap 使用了一种不一样的迭代方式。在这种迭代方式中,当iterator 被建立后集合再发生改变就再也不是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可使用原来老的数据,而写线程也能够并发的完成改变。

并发容器之CopyOnWriteArrayList详解

CopyOnWriteArrayList 是什么,能够用于什么应用场景?有哪些优缺点?

CopyOnWriteArrayList 是一个并发容器。有不少人称它是线程安全的,我认为这句话不严谨,缺乏一个前提条件,那就是非复合场景下操做它是线程安全的。

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将致使建立整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操做能够安全地执行。

CopyOnWriteArrayList 的使用场景

经过源码分析,咱们看出它的优缺点比较明显,因此使用场景也就比较明显。就是合适读多写少的场景。

CopyOnWriteArrayList 的缺点

  1. 因为写操做的时候,须要拷贝数组,会消耗内存,若是原数组的内容比较多的状况下,可能致使 young gc 或者 full gc。
  2. 不能用于实时读的场景,像拷贝数组、新增元素都须要时间,因此调用一个 set 操做后,读取到数据可能仍是旧的,虽然CopyOnWriteArrayList 能作到最终一致性,可是仍是无法知足实时性要求。
  3. 因为实际使用中可能无法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要从新复制数组,这个代价实在过高昂了。在高性能的互联网应用中,这种操做分分钟引发故障。

CopyOnWriteArrayList 的设计思想

  1. 读写分离,读和写分开
  2. 最终一致性
  3. 使用另外开辟空间的思路,来解决并发冲突

并发容器之BlockingQueue详解

什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列(BlockingQueue)是一个支持两个附加操做的队列。

这两个附加的操做是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列经常使用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列。分别是:

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

Java 5 以前实现同步存取时,可使用普通的一个集合,而后在使用线程的协做和线程同步能够实现生产者,消费者模式,主要的技术就是用好,wait,notify,notifyAll,sychronized 这些关键字。而在 java 5 以后,可使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。

BlockingQueue 接口是 Queue 的子接口,它的主要用途并非做为容器,而是做为线程同步的的工具,所以他具备一个很明显的特性,当生产者线程试图向 BlockingQueue 放入元素时,若是队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,若是队列为空,则该线程会被阻塞,正是由于它所具备这个特性,因此在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它能够很好的控制线程之间的通讯。

阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,而后解析线程不断从队列取数据解析。

并发容器之ConcurrentLinkedQueue详解

ConcurrentLinkedQueue非阻塞无界链表队列

ConcurrentLinkedQueue是一个线程安全的队列,基于链表结构实现,是一个无界队列,理论上来讲队列的长度能够无限扩大。

与其余队列相同,ConcurrentLinkedQueue也采用的是先进先出(FIFO)入队规则,对元素进行排序。 (推荐学习:java面试题目)

当咱们向队列中添加元素时,新插入的元素会插入到队列的尾部;而当咱们获取一个元素时,它会从队列的头部中取出。

由于ConcurrentLinkedQueue是链表结构,因此当入队时,插入的元素依次向后延伸,造成链表;而出队时,则从链表的第一个元素开始获取,依次递增;

值得注意的是,在使用ConcurrentLinkedQueue时,若是涉及到队列是否为空的判断,切记不可以使用size()==0的作法,由于在size()方法中,是经过遍历整个链表来实现的,在队列元素不少的时候,size()方法十分消耗性能和时间,只是单纯的判断队列为空使用isEmpty()便可。

BlockingQueue拯救了生产者、消费者模型的控制逻辑

经典的“生产者”和“消费者”模型中,在concurrent包发布之前,在多线程环境下,咱们每一个程序员都必须去本身控制这些细节,尤为还要兼顾效率和线程安全,而这会给咱们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给咱们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些状况下会挂起线程(即阻塞),一旦条件知足,被挂起的线程又会自动被唤醒)

BlockingQueue的成员介绍

由于它隶属于集合家族,本身又是个接口。因此是有不少成员的,下面简单介绍一下

1. ArrayBlockingQueue

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个经常使用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。 ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着二者没法真正并行运行,这点尤为不一样于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue彻底能够采用分离锁,从而实现生产者和消费者操做的彻底并行运行。Doug Lea之因此没这样去作,也许是由于ArrayBlockingQueue的数据写入和获取操做已经足够轻巧,以致于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上彻底占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不一样之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,然后者则会生成一个额外的Node对象。这在长时间内须要高效并发地处理大批量数据的系统中,其对于GC的影响仍是存在必定的区别。而在建立ArrayBlockingQueue时,咱们还能够控制对象的内部锁是否采用公平锁,默认采用非公平锁。

2.LinkedBlockingQueue

基于链表的阻塞队列,同ArrayListBlockingQueue相似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者当即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue能够经过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于一样的原理。而LinkedBlockingQueue之因此可以高效的处理并发数据,还由于其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的状况下生产者和消费者能够并行地操做队列中的数据,以此来提升整个队列的并发性能。 做为开发者,咱们须要注意的是,若是构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个相似无限大小的容量(Integer.MAX_VALUE),这样的话,若是生产者的速度一旦大于消费者的速度,也许尚未等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

3. DelayQueue 延迟队列

DelayQueue中的元素只有当其指定的延迟时间到了,才可以从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,所以往队列中插入数据的操做(生产者)永远不会被阻塞,而只有获取数据的操做(消费者)才会被阻塞,因此必定要注意内存的使用。 使用场景:   DelayQueue使用场景较少,但都至关巧妙,常见的例子好比使用一个DelayQueue来管理一个超时未响应的链接队列。

4. PriorityBlockingQueue

基于优先级的阻塞队列(优先级的判断经过构造函数传入的Compator对象来决定),但须要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。所以使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,不然时间一长,会最终耗尽全部的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

5. SynchronousQueue

一种无缓冲的等待队列,相似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,若是一方没有找到合适的目标,那么对不起,你们都在集市等待。相对于有缓冲的BlockingQueue来讲,少了一个中间经销商的环节(缓冲区),若是有经销商,生产者直接把产品批发给经销商,而无需在乎经销商最终会将这些产品卖给那些消费者,因为经销商能够库存一部分商品,所以相对于直接交易模式,整体来讲采用中间经销商的模式会吞吐量高一些(能够批量买卖);但另外一方面,又由于经销商的引入,使得产品从生产者到消费者中间增长了额外的交易环节,单个产品的及时响应性能可能会下降。

小结

BlockingQueue不光实现了一个完整队列所具备的基本功能,同时在多线程环境下,他还自动管理了多线间的自动等待于唤醒功能,从而使得程序员能够忽略这些细节,关注更高级的功能。

原子操做类

什么是原子操做?

原子操做(atomic operation)意为”不可被中断的一个或一系列操做” 。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操做。在 Java 中能够经过锁和循环 CAS 的方式来实现原子操做。 CAS 操做——Compare & Set,或是 Compare & Swap,如今几乎全部的 CPU 指令都支持 CAS 的原子操做。

原子操做是指一个不受其余操做影响的操做任务单元。原子操做是在多线程环境下避免数据不一致必须的手段。

int++并非一个原子操做,因此当一个线程读取它的值并加 1 时,另一个线程有可能会读到以前的值,这就会引起错误。

为了解决这个问题,必须保证增长操做是原子的,在 JDK1.5 以前咱们可使用同步技术来作到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类,它们能够自动的保证对于他们的操做是原子的而且不须要使用同步。

在 Java Concurrency API 中有哪些原子类(atomic classes)?

java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具备排他性,即当某个线程进入方法,执行其中的指令时,不会被其余线程打断,而别的线程就像自旋锁同样,一直等到该方法执行完成,才由 JVM 从等待队列中选择另外一个线程进入,这只是一种逻辑上的理解。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(经过引入一个 boolean来反映中间有没有变过),AtomicStampedReference(经过引入一个 int 来累加来反映中间有没有变过)

说一下 atomic 的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操做时,具备排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程能够向自旋锁同样,继续尝试,一直等到执行成功。

AtomicInteger 类的部分源码:

// setup to use Unsafe.compareAndSwapInt for updates(更新操做时提供“比较并替换”的做用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
	try {
		valueOffset = unsafe.objectFieldOffset
		(AtomicInteger.class.getDeclaredField("value"));
	} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;
123456789101112

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操做,从而避免 synchronized 的高开销,执行效率大为提高。

CAS的原理是拿指望的值和本来的一个值做比较,若是相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,所以 JVM 能够保证任什么时候刻任何线程总能拿到该变量的最新值。

同步工具类

并发工具之CountDownLatch与CyclicBarrier

经常使用的并发工具类有哪些?

  • Semaphore(信号量)-容许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只容许一个线程访问某个资源,Semaphore(信号量)能够指定多个线程同时访问某个资源。
  • CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具一般用来控制线程等待,它可让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 很是相似,它也能够实现线程间的技术等待,可是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 相似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要作的事情是,让一组线程到达一个屏障(也能够叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,全部被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每一个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,而后当前线程被阻塞。

在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?

CountDownLatch与CyclicBarrier都是用于控制并发的工具类,均可以理解成维护的就是一个计数器,可是这二者仍是各有不一样侧重点的:

  • CountDownLatch通常用于某个线程A等待若干个其余线程执行完任务以后,它才执行;而CyclicBarrier通常用于一组线程互相等待至某个状态,而后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等你们都完成,再携手共进。
  • 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程所有都到达了指定点的时候,才能继续往下执行;
  • CountDownLatch方法比较少,操做比较简单,而CyclicBarrier提供的方法更多,好比可以经过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,而且CyclicBarrier的构造方法能够传入barrierAction,指定当全部线程都到达时执行的业务功能;
  • CountDownLatch是不能复用的,而CyclicLatch是能够复用的。

并发工具之Semaphore与Exchanger

Semaphore 有什么做用

Semaphore 就是一个信号量,它的做用是限制某段代码块的并发数。Semaphore有一个构造函数,能够传入一个 int 型整数 n,表示某段代码最多只有 n 个线程能够访问,若是超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此能够看出若是 Semaphore 构造函数中传入的 int 型整数 n=1,至关于变成了一个 synchronized 了。

Semaphore(信号量)-容许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只容许一个线程访问某个资源,Semaphore(信号量)能够指定多个线程同时访问某个资源。

什么是线程间交换数据的工具Exchanger

Exchanger是一个用于线程间协做的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程可以交换数据。交换数据是经过exchange方法来实现的,若是一个线程先执行exchange方法,那么它会同步等待另外一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就能够交换数据。

疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备


回到◀疯狂创客圈

疯狂创客圈 - Java高并发研习社群,为你们开启大厂之门

相关文章
相关标签/搜索