《2021春招复习8》JUC(java util concurrent)《Java并发》

14、简述线程池 ☆

ThreadPoolExecutor线程池及线程扩展策略

1、为什么要用线程池?

​ **降低资源消耗。**通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

​ **提高响应速度。**当任务到达时,任务可以不需要等待线程创建就能立即执行。

​ **提高线程可管理性。**线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2、JUC中的线程池体系

ThreadPoolExecutor类和ScheduledThreadPoolExecutor实现了ExecutorService接口和Executor接口,并有Executors([ɪɡˈzekjətər])类扮演线程池工厂的角色(工厂设计模式)

Executor类方法:

image-20210430222845280

3、ThreadPoolExecutor的创建参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
corePoolSize:

​ 核心线程池大小(核心运行的线程个数)。

maximumPoolSize:

​ 最大线程池大小(最大线程个数)。当通过newFixedThreadPool()创建时,corePoolSize和maximumPoolSize是一样的。

keepAliveTime:

​ 线程池中超过corePoolSize数目的空闲线程最大存活时间。不推荐使用。

TimeUnit:

​ keepAliveTime时间单位。

workQueue:

​ 阻塞任务队列。当达到corePoolSize的时候就向该等待队列放入线程信息。(默认为一个LinkedBlockingQueue);

threadFactory:

​ 新建线程工厂。是构造Thread的方法,一个接口类,可以使用默认的default实现,也可以自己去包装和传递,主要实现newThread()方法即可。

RejectedExecutionHandler:

​ 当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理。java提供了5种丢弃处理的方法。java默认使用的是AbortPolicy,作用是当出现这种情况的时候抛出一个异常。

其中比较容易让人误解的是:corePoolSize,maximumPoolSize,workQueue之间关系:

1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 (除了利用提交新任务来创建和启动线程(按需构造),也可以通过prestartCoreThread()或者prestartAllCoreThreads()方法来提前启动线程池中的基本线程。)。
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

引申:线程池的参数如何设置?
  1. 如果任务是耗时IO型,比如读取数据库,文件读写以及网络通信的话这些任务不会占据很多CPU的资源但会比较耗时:线程数量设置为2倍CPU及以上,充分的来利用CPU资源。

  2. 如果任务为CPU密集型的话,比如大量计算、解压、压缩等这些操作都会占据大量的cpu,这种情况一般设置线程数为:1倍cpu+1,为什么要+1,很多说法是备份线程。

  3. 如果既有IO密集型任务又有CPU密集型任务,该怎么设置线程大小?这种的话最好分开用线程池来处理,IO密集型任务用IO密集型线程池处理,CPU密集型用CPU密集型线程池来处理。具体情况具体分析。

4、Executors工厂类实现的线程池。

1、newFixedThreadPool定长线程池

​ 构造一个固定线程数目的线程池,配置的corePoolSize与maximumPoolSize大小相同,同时使用了一个无界LinkedBlockingQueue存放阻塞任务,(实际线程数量永远维持在nThreads,因此keepAliveTime将无效)因此多余的任务将存在再阻塞队列,不会由RejectedExecutionHandler处理。

//Java代码:   
public static ExecutorService newFixedThreadPool(int nThreads) {  
        return new ThreadPoolExecutor(nThreads, nThreads,  
                                      0L, TimeUnit.MILLISECONDS,  
                                      new LinkedBlockingQueue<Runnable>());  
    }  
2、newCachedThreadPool可缓存

​ 构造一个缓冲功能的线程池,配置corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s,以及一个无容量的阻塞队列 SynchronousQueue,因此任务提交之后,将会创建新的线程执行;线程空闲超过60s将会销毁,比较适合处理执行时间比较小的任务。

//Java代码:    
public static ExecutorService newCachedThreadPool() {  
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
                                      60L, TimeUnit.SECONDS,  
                                      new SynchronousQueue<Runnable>());  
    }  
3、newSingleThreadExecutor单一线程池

​ 构造一个只支持一个线程的线程池,配置corePoolSize=maximumPoolSize=1,无界阻塞队列LinkedBlockingQueue;保证任务由一个线程串行执行

//Java代码:    
public static ExecutorService newSingleThreadExecutor() {  
        return new FinalizableDelegatedExecutorService  
            (new ThreadPoolExecutor(1, 1,  
                                    0L, TimeUnit.MILLISECONDS,  
                                    new LinkedBlockingQueue<Runnable>()));  
    }  
4、ScheduledThreadPool 可调度线程池

​ 构造有定时功能的线程池,配置corePoolSize,无界延迟阻塞队列DelayedWorkQueue;有意思的是:maximumPoolSize=Integer.MAX_VALUE,由于DelayedWorkQueue是无界队列,所以这个值是没有意义的 。实现周期性线程调度,比较常用。

scheduleAtFixedRate或者scheduleWithFixedDelay区别:

​ scheduleAtFixedRate表示已固定频率执行的任务,如果当前任务耗时较多,超过定时周期period,则当前任务结束后会立即执行;sheduleWithFixedDelay表示以固定延时执行任务,延时是相对当前任务结束为起点计算开始时间。

//Java代码
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {  
        return new ScheduledThreadPoolExecutor(corePoolSize);  
    }  

public static ScheduledExecutorService newScheduledThreadPool(  
            int corePoolSize, ThreadFactory threadFactory) {  
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);  
    }  

public ScheduledThreadPoolExecutor(int corePoolSize,  
                             ThreadFactory threadFactory) {  
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,  
              new DelayedWorkQueue(), threadFactory);  
    }  
5、Excutors工厂类实现的线程池的问题:

​ newFixedThreadPool,newSingleThreadExecutor:无界LinkedBlockingQueue队列,可能会堆积大量请求,造成OutOfMemory错误。

​ newCachedThreadPool:允许创建的线程数为Integer.MAX_VALUE,可能会创建大量线程,从而引起OutOfMemory异常。

5、线程池中使用的BlockingQueue

线程池的阻塞队列的选择
同步移交队列

​ 如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列(没有容量),而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。

有界队列

​ 遵循FIFO原则的队列如ArrayBlockingQueue。

无界队列

​ 队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,而楼主踩到的就是这个坑,当QPS很高,发送数据很大,大量的任务被添加到这个无界LinkedBlockingQueue 中,导致cpu和内存飙升服务器挂掉。(maximumPoolSize无效,最大线程数是corePoolSize)

优先级队列:
并发队列-无界阻塞优先级队列PriorityBlockingQueue原理探究

​ PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现。其中的优先级由任务的Comparator决定。

15、线程池的增长策略(任务调度)☆

当一个任务通过execute( Runnable)方法欲添加到线程池时:

  1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  2. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

  3. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。

​ 也就是:处理任务的优先级为:核心线程corePoolSize、 任务队列workQueue、 最大线程maximumPoolSize。如果三者都满了,使用RejectedExecutionHandlier处理被拒绝的任务。(具体怎么处理,属于拒绝策略的范畴)。

​ 当然,具体增长策略得看你使用什么workQueue。

回收策略

​ 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。(不区分核心和非核心,当线程数达到corePoolSize的时候,回收过程就会终止)

​ 对于线程池的核心线程数中的线程,也有回收的办法,可以通过 allowCoreThreadTimeOut(true) 方法设置,在核心线程空闲的时候,一旦超过 keepAliveTime&unit 配置的时间,也将其回收掉。

16、线程池的拒绝策略 ☆

​ JDK主要提供了4种饱和策略供选择。4种策略都做为静态内部类在ThreadPoolExcutor中进行实现。

1、AbortPolicy中止策略

​ 该策略是默认饱和策略。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() + 
                                                " rejected from " +
                                                 e.toString()); } 

​ 使用该策略时在饱和时会抛出RejectedExecutionException(继承自RuntimeException),调用者可捕获该异常自行处理。

2、 DiscardPolicy抛弃策略

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}

​ 如代码所示,不做任何处理直接抛弃任务

3 、DiscardOldestPolicy抛弃旧任务策略

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r); 
           }} 

​ 如代码,先将阻塞队列中的头元素出队删除,再尝试提交任务。如果此时阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃,因此不建议将该种策略配合优先级队列使用。

4、 CallerRunsPolicy调用者运行

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 
           if (!e.isShutdown()) { 
               r.run(); 
           }} 

​ 既不抛弃任务也不抛出异常,直接运行任务的run方法,换言之将任务回退给调用者来直接运行。使用该策略时线程池饱和后将由调用线程池的主线程自己来执行任务,因此在执行任务的这段时间里主线程无法再提交新任务,从而使线程池中工作线程有时间将正在处理的任务处理完成。

17、实现一个阻塞队列☆

1、阻塞队列简介

​ 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。即在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待对垒可用。

阻塞队列API:
抛异常特定值阻塞超时
插入add(o)offer(o)put(o)offer(o,timeout,timeunit)
移除remove(0)poll(o)take(o)poll(timeout,timeunit)
检查element(o)peek(o)
异常:

​ 是指当阻塞队列满的时候,再往队列里插入元素,会抛出IlllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常。

返回特殊值:

​ 插入方法会返回是否成功,成功true,失败false。移除方法,则是从队列出拿出一个元素,如果没有返回为空。

一直阻塞:

​ 当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到消费者拿到数据,队列容量有空余,或者响应中断退出。当队列为空时,消费者线程试图从队列里take元素,队列也会阻塞消费者进程,直到队列可用。

超时退出:

​ 当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

2、如何实现一个阻塞队列

​ 直接看JUC中的BlockingQueue源码。

Java.util.concurrent.BlockingQueue接口有以下阻塞队列实现,基于ReentrantLock:

​ FIFO队列:LinkedBlockingQueue、ArrayBlockingQueue(固定长度)

​ 优先级队列:PriorityBlockingQueue(用于实现最小堆和最大堆)

​ 提供了阻塞的take()和put()方法:如果队列为空,take()将阻塞,直到队列中有内容,如果队列为满put()将阻塞,直到队列有空闲位置。

1、ArrayBlockingQueue

​ ArrayBlockingQueue是一个用数组实现的有界阻塞队列。提供FIFO的功能。队列头上的元素实在队列里呆的时间最长的元素,队列尾上的元素是在队列中呆了时间最短的元素。新元素会插在队列尾部,从队列获取元素是会从队列头上获取。

(1)成员变量
/** The queued items */
//一个Object类型的数组用于保存阻塞队列中的元素
final Object[] items;

/** items index for next take, poll, peek or remove */
//从队列获取元素的位置
int takeIndex;

/** items index for next put, offer, or add */
//往队列里放元素的位置
int putIndex;

/** Number of elements in the queue */
//队列中元素的个数
int count;

/*
 * Concurrency control uses the classic two-condition algorithm
 * found in any textbook.
 */

/** Main lock guarding all access */
//可重入锁
final ReentrantLock lock;

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

​ 最主要的成员变量是items,它是一个Object类型的数组用于保存阻塞队列中的元素。其次是takeIndex,putIndex,count,分别表示了从队列获取元素的位置,往队列里放元素的位置和队列中元素的个数。然后是lock,notEmpty和notFull三个和锁相关的成员变量。lock是一个可重入锁,而notEmpty和notFull是和lock绑定的2个Condition。

(2)构造函数
/**
 * Creates an {@code ArrayBlockingQueue} with the given (fixed)
 * capacity and the specified access policy.
 *
 * @param capacity the capacity of this queue
 * @param fair if {@code true} then queue accesses for threads blocked
 *        on insertion or removal, are processed in FIFO order;
 *        if {@code false} the access order is unspecified.
 * @throws IllegalArgumentException if {@code capacity < 1}
 */
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

(3)入队方法:put入队,满则等待,offer入队,满则返回
/**
 * Inserts the specified element at the tail of this queue if it is
 * possible to do so immediately without exceeding the queue's capacity,
 * returning {@code true} upon success and {@code false} if this queue
 * is full.  This method is generally preferable to method {@link #add},
 * which can fail to insert an element only by throwing an exception.
 *
 * @throws NullPointerException if the specified element is null
 */
public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //区别 不过源代码中也有lockInterruptibly();
    lock.lock();
    try {
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

/**
 * Inserts the specified element at the tail of this queue, waiting
 * for space to become available if the queue is full.
 *
 * @throws InterruptedException {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 */
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //区别
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

/**
  * Inserts element at current put position, advances, and signals.
  * Call only when holding lock.
  */
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}

​ ReentrantLock的中断和非中断加锁模式的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其它线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。

(4)出队:take和put相对应,offer和poll相对应

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

/**
  * Extracts element at current take position, advances, and signals.
  * Call only when holding lock.
  */
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}
2、LinkedBlockingQueue

​ LinkedBlockingQueue:基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回。只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。

(1)成员变量

​ 最大特点是两把锁和一个原子类的count(为啥是原子类,因为读写都会操作)

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

/**
 * Head of linked list.
 * Invariant: head.item == null
 */
transient Node<E> head;

/**
 * Tail of linked list.
 * Invariant: last.next == null
 */
private transient Node<E> last;

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

​ 其它操作不展开了,和ArrayBlockingQueue类型,都是加锁(ReentrantLock)——,判断是否空或者满,然后唤醒操作。

18、ArrayBlockingQueue和LinkedBlockingQueue的区别

1、队列大小初始化方式不同

ArrayBlockingQueue是有界的,必须指定队列的大小;

LinkedBlockingQueue是无界的,可以不指定队列的大小,默认是Integer.MAX_VALUE。当然也可以指定队列大小,从而成为有界的。但是在使用LinkedBlockingQueue时,若用默认大小且当生产速度大于消费速度时候,有可能会内存溢出。

2、数据存储容器不一样

​ ArrayBlockingQueue采用的是数组,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。

3、在生产或消费时操作不同

ArrayBlockingQueue基于数组,在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例;

LinkedBlockingQueue基于链表,在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会生成一个额外的Node对象,这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。

4、队列中的锁的实现不同

​ ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个ReentrantLock锁;

​ LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock,这样可以大大提高系统的吞吐量,对于高并发情况,生产者和消费者可以并行的操作队列的数据,从而提高并发性能。

注意:

  • 在使用ArrayBlockingQueue和LinkedBlockingQueue分别对1000000个简单字符做入队操作时,

    ​ LinkedBlockingQueue的消耗是ArrayBlockingQueue消耗的10倍左右,即LinkedBlockingQueue消耗在1500毫秒左右,而ArrayBlockingQueue只需150毫秒左右。

  • 按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

19、生产者和消费者模式(本质还是使用阻塞队列)

生产者消费者的五种实现

​ 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。

生产者消费者模式的优点:

  • 解耦
  • 支持并发
  • 支持忙闲不均

解决方法可分为两类:

(1)用信号量和锁机制实现生产者和消费者之间的同步;

  • 用synchronized对存储进行加锁,然后用object原生的wait() / notify()方法做同步。
  • 用concurrent.locks.Lock ,然后用Condition的await() / signal()方法做同步。
  • 用concurrent.BlockingQueue阻塞队列方法
  • 用Semaphore( [ˈsɛməˌfɔr])(信号量)方法

(2)在生产者和消费者之间建立一个管道。(一般不使用,缓冲区不易控制、数据不易封装和传输)

  • PipedInputStream / PipedOutputStream

Synchronized实现生产者消费者模式:

说明
  • Object.wait()使当前的线程进入到等待状态(进入到等待队列)
  • Object.notifyAll() 唤醒等待中的全部线程
  • Object.notify() 随机唤醒一个线程
代码
consumer.java
复制成功public class Consumer extends Thread {
    List<Object> container;
    /*表示当前线程共生产了多少件物品*/
    private int count;

    public Consumer(String name, List<Object> container) {
        super(name);
        this.container = container;
    }

    @Override
    public void run() {

        while(true){
            synchronized (container) {
                try {
                    if (container.isEmpty()) { //仓库已空,不能消 只能等
                        container.wait(20);

                    } else {
                        // 消费
                        container.remove(0);
                        this.count++;
                        System.out.println("消费者:" + getName() + " 共消费了:" + this.count + "件物品,当前仓库里还有" + container.size() + "件物品");
                        container.notifyAll();  // 唤醒等待队列中所有线程
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Producer.java
复制public class Producer extends Thread{

    List<Object> container;
    /*表示当前线程共生产了多少件物品*/
    private int count;

    public Producer(String name, List<Object> container) {
        super(name);
        this.container = container;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (container) {
                try {
                    // 如果某一个生产者能执行进来,说明此线程具有container对象的控制权,其它线程(生产者&消费者)都必须等待
                    if (container.size() == 10) { // 假设container最多只能放10个物品,即仓库已满
                        container.wait(10); //表示当前线程需要在container上进行等待
                    } else {
                        // 仓库没满,可以放物品
                        container.add(new Object());
                        this.count++;
                        System.out.println("生产者:" + getName() + " 共生产了:" + this.count + "件物品,当前仓库里还有" + container.size() + "件物品");
                        // 生产者生产了物品后应通知(唤醒)所有在container上进行等待的线程(生产者&消费者)
                        //   生:5, 消:5
                        // container.notify();  // 随机唤醒一个在等待队列中的线程
                        container.notifyAll();  // 唤醒等待队列中所有线程
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } //

            try {
                sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Test.java
复制public class Test {

    public static void main(String[] args) {
        // 仓库
        List<Object> container = new ArrayList<>();

        new Producer("老王", container).start();
        new Consumer("小芳", container).start();
        new Producer("老李", container).start();
        new Consumer("小荷", container).start();
        new Producer("老张", container).start();

        new Consumer("小花", container).start();
        new Producer("老刘", container).start();
        new Consumer("小妞", container).start();
        new Consumer("小米", container).start();
        new Producer("老马", container).start();

    }
}

20、ConcurrentHashMap的实现原理 ☆

深入理解HashMap和CurrentHashMap
HashMap和ConcurrentHashMap的知识总结

1、哈希表和链式哈希表

​ 哈希表就是key-value存储的数据结构。

​ 链式HashMap表采用的是

  • JDK7 使用了数组+链表的方式
  • JDK8 使用了数组+链表+红黑树的方式

JDK1.7

jdk1.8:

区别:

  • 使用一个Node数组取代了JDK7的Entry数组来存储数据,这个Node可能是链表结构,也可能是红黑树结构;
  • 如果插入的元素key的hashcode值相同,那么这些key也会被定位到Node数组的同一个格子里,如果不超过8个使用链表存储;
  • 超过8个,会调用treeifyBin函数,将链表转换为红黑树。那么即使所有key的hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(logn)的开销。

2、ConcurrentHashMap出现的原因

[HashMap并发导致死循环 CurrentHashMap
ConcurrentHashMap的实现原理与使用(一):多线程中HashMap的死循环分析
(1)线程不安全的HashMap:

​ 在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,这是1.8之前HashMap的扩容并发Bug,若当前线程(1)此时获得entry节点,但是被线程中断无法继续执行,此时线程2进入transfer()函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程1获得执行权继续执行,因为并发transfer,所以两者都是扩容同一个链表,当线程1执行到e.next=new table[i]的时候,由于线程2之前数据迁移的原因导致此时new table[i]上就有entry存在,所以线程1执行的时候,会将next节点,设置为自己,导致自己互相使用next引用对方,因此产生链表,导致死循环。

​ 换言之,HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

(2)效率低下的HashMap:

​ HashMap使用synchronized来保证线程安全,但在线程竞争激励的情况下HashMap的效率低下。

3、1.7和1.8的ConcurrentHashMap

ConcurrentHashMap jdk1.7:

ReentrantLock+Segment数组+HashEntry(数组+链表)

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

ConcurrentHashMap jdk1.8:

​ Synchronized+CAS+HashEntry+红黑树(数组+链表+红黑树),主要区别以下几点:

  1. 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程的安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
  3. 锁的粒度:原来是对需要进行数据操作的Segment加锁,先调整为对每个数组元素加锁(Node)。
  4. 链表转化为红黑树:定位节点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8的时候,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从原来遍历链表O(n),变成遍历红黑树O(logN)。

4、put过程

/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * <p>The value can be retrieved by calling the {@code get} method
 * with a key that is equal to the original key.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with {@code key}, or
 *         {@code null} if there was no mapping for {@code key}
 * @throws NullPointerException if the specified key or value is null
 */
public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;

  1. 如果没有初始化就先调用initTable()方法来进行初始化过程,
  2. 如果没有Hash冲突,就直接CAS插入。
  3. 如果还在进行扩容操作就先进行扩容(引申:重点看扩容操作transfer())
  4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。
  5. 最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时,会将链表结构转换为红黑树的结构,break再一次进入循环。
  6. 如果添加成功就调用addCount方法统计size,并且检查是否需要扩容。
/**
 * Moves and/or copies the nodes in each bin to new table. See
 * above for explanation.
 */
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

5、JDK1.8为什么使用内置锁Synchronized来代替重入锁ReentrantLock?

  1. 因为锁的粒度降低了,在粒度较低的情况下,Synchronized不一定比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加灵活,但在低粒度里,Condition的优势就没有了。
  2. JVM团队从未放弃snychronized(引申:锁的优化),而基于JVM的synchronized优化空间更大,使用内嵌关键字比使用API更加自然。
  3. 在大量数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存(毕竟包了一层,sunchronized更接近底层),虽然不是瓶颈,但也是选择依据。

6、日常套路

其实这块也是面试的重点内容,通常的套路是:

  1. 谈谈你理解的 HashMap,讲讲其中的 get put 过程。
  2. 1.8 做了什么优化?
  3. 是线程安全的嘛?
  4. 不安全会导致哪些问题?
  5. 如何解决?有没有线程安全的并发容器?
  6. ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

这一串问题相信大家仔细看完都能怼回面试官。

除了面试会问到之外平时的应用其实也蛮多,像之前谈到的 Guava 中 Cache 的实现就是利用 ConcurrentHashMap 的思想。

21、快速失败和安全失败☆

一:快速失败(fail—fast)

​ 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

​ 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

二:安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

​ 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

22、CAS☆

什么是CAS机制?
面试必问的CAS,你懂了吗?

​ CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS是一种基于锁的操作,而且是乐观锁。synchronzied是悲观锁,它将资源锁住,等之前获得锁的线程释放锁后,下一个线程才可以访问,而乐观锁乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新,(不加锁),性能较悲观锁有很大的提升。

​ CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V中的实际值与预期值A相等时,将内存地址V的值修改为B并返回true,否则就什么都不做并返回false。整个比较并替换的操作是一个原子操作。

CAS的缺点:

1) CPU开销过大

​ 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

​ CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

​ 这是CAS机制最大的问题所在。(后面有介绍)

什么是ABA问题?ABA问题怎么解决?

CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。

但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?

​ 如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步(Synchronized和Lock)可能会比原子类更高效。

应用:

​ 所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。而Atomic操作类的底层正是用到了“CAS机制”。

23、AQS(AbstractQueuedSynchronizer)原理。☆

Java并发之AQS详解

谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!

类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch等。

1、AQS核心实现原理

​ AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。该队列是由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。

​ 本质上就是一个双端双向链表。(CLH指的是该算法的三位作者:Craig、Landin和Hagersten名字首字母的缩写。 )

​ 当线程获取资源失败(比如tryAcquire时试图设置state状态失败)时,会被构造成一个结点加入CLH队列中,同时当前线程会被阻塞在队列中(通过LockSupport.park实现,其实是等待态)。当持有同步状态的线程释放同步状态时,会唤醒后续结点,使其加入到对同步状态的争夺中。

2、AQS源码层面分析

(1)state和CLH等待队列

​ 它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。

state的访问方式有三种:

  • getState()

  • setState()

  • compareAndSetState()

/**
 * The synchronization state.
 */
private volatile int state;

/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
    return state;
}

/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
    state = newState;
}

/**
 * Atomically sets synchronization state to the given updated
 * value if the current state value equals the expected value.
 * This operation has memory semantics of a {@code volatile} read
 * and write.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that the actual
 *         value was not equal to the expected value.
 */
//原子地(CAS操作)将同步状态值设置为给定值update,如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
(2)自定义同步器实现方法

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

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

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

// Main exported methods

/**
 * Attempts to acquire in exclusive mode. This method should query
 * if the state of the object permits it to be acquired in the
 * exclusive mode, and if so to acquire it.
 *
 * <p>This method is always invoked by the thread performing
 * acquire.  If this method reports failure, the acquire method
 * may queue the thread, if it is not already queued, until it is
 * signalled by a release from some other thread. This can be used
 * to implement method {@link Lock#tryLock()}.
 *
 * <p>The default
 * implementation throws {@link UnsupportedOperationException}.
 *
 * @param arg the acquire argument. This value is always the one
 *        passed to an acquire method, or is the value saved on entry
 *        to a condition wait.  The value is otherwise uninterpreted
 *        and can represent anything you like.
 * @return {@code true} if successful. Upon success, this object has
 *         been acquired.
 * @throws IllegalMonitorStateException if acquiring would place this
 *         synchronizer in an illegal state. This exception must be
 *         thrown in a consistent fashion for synchronization to work
 *         correctly.
 * @throws UnsupportedOperationException if exclusive mode is not supported
 */
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * Attempts to set the state to reflect a release in exclusive
 * mode.
 *
 * <p>This method is always invoked by the thread performing release.
 *
 * <p>The default implementation throws
 * {@link UnsupportedOperationException}.
 *
 * @param arg the release argument. This value is always the one
 *        passed to a release method, or the current state value upon
 *        entry to a condition wait.  The value is otherwise
 *        uninterpreted and can represent anything you like.
 * @return {@code true} if this object is now in a fully released
 *         state, so that any waiting threads may attempt to acquire;
 *         and {@code false} otherwise.
 * @throws IllegalMonitorStateException if releasing would place this
 *         synchronizer in an illegal state. This exception must be
 *         thrown in a consistent fashion for synchronization to work
 *         correctly.
 * @throws UnsupportedOperationException if exclusive mode is not supported
 */
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * Attempts to acquire in shared mode. This method should query if
 * the state of the object permits it to be acquired in the shared
 * mode, and if so to acquire it.
 *
 * <p>This method is always invoked by the thread performing
 * acquire.  If this method reports failure, the acquire method
 * may queue the thread, if it is not already queued, until it is
 * signalled by a release from some other thread.
 *
 * <p>The default implementation throws {@link
 * UnsupportedOperationException}.
 *
 * @param arg the acquire argument. This value is always the one
 *        passed to an acquire method, or is the value saved on entry
 *        to a condition wait.  The value is otherwise uninterpreted
 *        and can represent anything you like.
 * @return a negative value on failure; zero if acquisition in shared
 *         mode succeeded but no subsequent shared-mode acquire can
 *         succeed; and a positive value if acquisition in shared
 *         mode succeeded and subsequent shared-mode acquires might
 *         also succeed, in which case a subsequent waiting thread
 *         must check availability. (Support for three different
 *         return values enables this method to be used in contexts
 *         where acquires only sometimes act exclusively.)  Upon
 *         success, this object has been acquired.
 * @throws IllegalMonitorStateException if acquiring would place this
 *         synchronizer in an illegal state. This exception must be
 *         thrown in a consistent fashion for synchronization to work
 *         correctly.
 * @throws UnsupportedOperationException if shared mode is not supported
 */
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * Attempts to set the state to reflect a release in shared mode.
 *
 * <p>This method is always invoked by the thread performing release.
 *
 * <p>The default implementation throws
 * {@link UnsupportedOperationException}.
 *
 * @param arg the release argument. This value is always the one
 *        passed to a release method, or the current state value upon
 *        entry to a condition wait.  The value is otherwise
 *        uninterpreted and can represent anything you like.
 * @return {@code true} if this release of shared mode may permit a
 *         waiting acquire (shared or exclusive) to succeed; and
 *         {@code false} otherwise
 * @throws IllegalMonitorStateException if releasing would place this
 *         synchronizer in an illegal state. This exception must be
 *         thrown in a consistent fashion for synchronization to work
 *         correctly.
 * @throws UnsupportedOperationException if shared mode is not supported
 */
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * Returns {@code true} if synchronization is held exclusively with
 * respect to the current (calling) thread.  This method is invoked
 * upon each call to a non-waiting {@link ConditionObject} method.
 * (Waiting methods instead invoke {@link #release}.)
 *
 * <p>The default implementation throws {@link
 * UnsupportedOperationException}. This method is invoked
 * internally only within {@link ConditionObject} methods, so need
 * not be defined if conditions are not used.
 *
 * @return {@code true} if synchronization is held exclusively;
 *         {@code false} otherwise
 * @throws UnsupportedOperationException if conditions are not supported
 */
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

(3)举例

​ 以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减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

3、总结

​ AQS是JUC中很多同步组件的构建基础,比如ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、FutureTask等等皆是基于AQS的,简单的说,内部实现主要是同步状态state和一个FIFO队列来实现,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个节点(或共享式或独占式)加入到同步队列的尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后序结点,使其加入到对同步状态的争夺中。

​ AQS定义了顶层的处理实现逻辑,我们在使用AQS构建符合我们要求的同步组件时,只需重写tryAcquire,tryAcquireShared,tryRelease,tryReleaseShared几个方法,来决定状态的获取和释放即可,至于背后复杂的线程排队,线程阻塞和唤醒,如何保证线程安全,都由AQS为我们完成了,这就是非常典型的模板方法的应用。

24、JUC同步工具(CountDownLatch、CyclicBarrier、Semaphore)

1、CountDownlatch(闭锁)

(1)概念

​ CountDownlatch是一个同步工具类,用来协调多个线程之间的同步。

​ 让一些线程阻塞,直到另外一些完成后才被唤醒。

​ 计数器初始值为线程数量。

  • ​ CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
  • ​ 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
  • ​ 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
(2)CountDownLatch的用法
场景1:

​ 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕后,将讲讲计数器减1 countdownLatch.countDown(),当计数器的值变为0的时候,在CountDownLatch()上await()的线程就会被唤醒。一个典型的应用场景,就是启动一个服务是,主线程需要等待多个组件加载完毕之后再继续执行。

场景2:

​ 实现多个线程开始执行任务的最大并行性。注意是并行,而不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),多个线程在开始执行任务前首先countdownlatch.await(),档主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

(3)不足

​ 一次性的,计数器的值只能在构造方法里初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

2、CyclicBarrier

(1)概念

​ CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)

​ CyclicBarrier要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程是通过CyclicBarrier的await()方法进入的。(比赛时要运动员都上场才能开始)

(2)与CountDownLatch的区别
CountDownLatchCyclicBarrier
计数方式
释放条件计数为0时释放所有等待的线程计数达到指定值时释放所有等待线程
可重复利用不可,计数为0时,无法重置计数器达到指定值时,计数置为0,重新开始
方法countDown()减一,await()阻塞,不影响计数await()方法+1,若加1后的值不等于构造方法的值,则线程阻塞。

3、Semaphore([ˈseməfɔːr] 信号量)

(1)概念

​ 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

(2)方法

​ void acquire(): 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时中断。

​ void release():实际上会将信号量的值加1,然后唤醒等待的线程。

​ int availablePermits():返回此信号量中当前可用的许可数。

​ boolean hasQueuedThreads():查询是否有线程正在等待获取。

25、JUC(java并发包)的所有问题小结。

1、简述JUC体系

​ 并发集合,阻塞队列,线程池,并发锁,原子类等工具。

外层框架:

​ Lock(ReentrantLock、ReadWriteLock等)、同步器(semaphores)、阻塞队列(BlockingQueue等)、Executor(线程池)、并发容器(ConcurrentHashMap等)、还有Fork/Join框架:

内层:

​ AQS(AbstractQueuedSynchronizer类,锁功能都由他实现)、非阻塞数据结构、原子变量类(AtomicInteger等无锁线程安全类)三种。

底层:

​ volatile和CAS两种思想。

image-20210502204649777

并发集合:

ConcurrentHashMap,其它集合有ConcurrentSkipListMap、CopyOnWriteArrayList和CopyOnWriteArraySet等。

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWrite并发容器用于读多写少的并发场景。CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题(复制导致)和数据一致性问题(最终一致)。

原子类:

​ Atomic类主要利用CAS+volatile和native方法来保证原子操作,从而避免Synchronized的高开销,执行效率大为提升。

同步工具:

(countDownLatch,CyclicBarrier、Semaphore)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值