并发编程面试题6

简介: 并发编程面试题6

以ReentrantLock为例分析

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


以CountDownLatch以例分析

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


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


AQS的简单应用

Mutex:不可重入互斥锁,锁资源(state)只有两种状态:0:未被锁定;1:锁定

class Mutex implements Lock, java.io.Serializable {
    // 自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判断是否锁定状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // 尝试获取资源,立即返回。成功则返回true,否则false。
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 这里限定只能为1个量
            if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
                return true;
            }
            return false;
        }
        // 尝试释放资源,立即返回。成功则为true,否则false。
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定为1个量
            if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//释放资源,放弃占有状态
            return true;
        }
    }
    // 真正同步类的实现都依赖继承于AQS的自定义同步器!
    private final Sync sync = new Sync();
    //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
    public void lock() {
        sync.acquire(1);
    }
    //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    //unlock<-->release。两者语文一样:释放资源。
    public void unlock() {
        sync.release(1);
    }
    //锁是否占有状态
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}


公平锁非公平锁区别?各自的优缺点?

公平锁非公平锁区别?

区别就是公平锁是每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。 非公平锁是多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。简单来说就是公平锁就是先到先得,而非公平锁就是谁先抢到归谁,一个是排队,然后一个个来,一个是不排队,谁能挤到最前面谁就获取锁


各自的优缺点?

公平锁


优点:所有的线程都能得到资源,不会饿死在队列中。


缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。


非公平锁


优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。


缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。


深入了解:阿里面试官:说一下公平锁和非公平锁的区别?_敖 丙的博客-CSDN博客_公平锁和非公平锁区别


ReadWriteLock 是什么

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

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

  • 公平选择性:支持非公平(默认)和公平的锁获取方式,非公平的方式性能。
  • 可重入:读锁和写锁都支持线程重进入。
  • 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。


为什么会有读写锁?


首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。


并发工具/容器/原子类

什么是同步容器?什么是并发容器?并发容器的实现?

Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map,大家熟知的这些集合类ArrayList、LinkedList、HashMap这些容器都是非线程安全的。


如果有多个线程并发地访问这些容器时,就会出现问题。因此,在编写程序时,在多线程环境下必须要求程序员手动地在任何访问到这些容器的地方进行同步处理,这样导致在使用这些容器的时候非常地不方便。


所以,Java先提供了同步容器供用户使用。


什么是同步容器?

可以简单地理解为通过 synchronized 来实现线程安全的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector、Stack、Hashtable、Collections.synchronizedSet、Collections.synchronizedList 等方法返回的容器。


可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized。这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低。


例如: HashTable只要有一条线程获取了容器的锁之后,其他所有的线程访问同步函数都会被阻塞,因此同一时刻只能有一条线程访问同步函数。


因此为了解决同步容器的性能问题,所以才有了并发容器。


什么是并发容器?

java.util.concurrent包中提供了多种并发类容器。


并发类容器(俗称JUC容器)是专门针对多线程、高并发专门设计的一些类,用来替代性能较低的同步容器。最经典的并发容器是ConcurrentHashMap


在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法+synchronize


常见的并发容器

ConcurrentHashMap

对应的非并发容器:HashMap


目标:代替Hashtable、synchronizedMap,支持复合操作


原理:JDK6中采用一种更加细粒度的加锁机制Segment“分段锁”,JDK8中采用CAS无锁算法。


CopyOnWriteArrayList

对应的非并发容器:ArrayList


目标:代替Vector、synchronizedList


原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。


CopyOnWriteArraySet

对应的非并发容器:HashSet


目标:代替synchronizedSet


原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。


并发容器的实现

CopyOnWriteArrayList - 线程安全的 ArrayList

CopyOnWriteArraySet - 线程安全的 Set,因此本质上是由 CopyOnWriteArrayList 实现的。

ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。

ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。

ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。

ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。

ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。

ArrayBlockingQueue - 数组实现的阻塞队列。

LinkedBlockingQueue - 链表实现的阻塞队列。

LinkedBlockingDeque - 双向链表实现的双端阻塞队列。


Java 中的同步容器与并发容器有什么区别?

不管是同步容器还是并发容器他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。


同步容器比并发容器在性能上差很多。并发容器的出现就是为了解决同步容器性能问题,造成如此慢的主要原因是锁, 同步容器会把整个容器都锁起来,例如HashTable只要有一条线程获取了容器的锁之后,其他所有的线程访问同步函数都会被阻塞,因此同一时刻只能有一条线程访问同步函数。而并发容器不会。并发集合实现线程安全是通过使用像分段锁、CAS算法等先进的和成熟的技术。例如ConcurrentHashMap 在JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法


Condition源码分析与等待通知机制

java中Condition类的详细介绍(详解)_普通网友的博客-CSDN博客_condition类

Condition源码分析与等待通知机制_ThinkWon的博客-CSDN博客

BlockingQueue是什么?

并发容器之BlockingQueue详解_ThinkWon的博客-CSDN博客_blockingqueue 并发

BlockingQueue是什么?

BlockingQueue其实就是阻塞队列,是基于阻塞机制实现的线程安全的队列。而阻塞机制的实现是通过在入队和出队时加锁的方式避免并发操作。

BlockingQueue不同于普通的Queue的区别

  • 通过在入队和出队时进行加锁,保证了队列线程安全
  • 支持阻塞的入队和出队方法:当队列满时,会阻塞入队的线程,直到队列不满;当队列为空时,会阻塞出队的线程,直到队列中有元素。


BlockingQueue常用于生产者-消费者模型中,往队列里添加元素的是生产者,从队列中获取元素的是消费者;通常情况下生产者和消费者都是由多个线程组成

BlockingQueue继承了Queue接口,在Queue接口基础上,又提供了若干其他方法,其定义源码如下:

public interface BlockingQueue<E> extends Queue<E> {
    /**
     * 入队一个元素,如果有空间则直接插入,并返回true;
     * 如果没有空间则抛出IllegalStateException
     */
    boolean add(E e);
    /**
     * 入队一个元素,如果有空间则直接插入,并返回true;
     * 如果没有空间返回false
     */
    boolean offer(E e);
    /**
     * 入队一个元素,如果有空间则直接插入,如果没有空间则一直阻塞等待
     */
    void put(E e) throws InterruptedException;
    /**
     * 入队一个元素,如果有空间则直接插入,并返回true;
     * 如果没有空间则等待timeout时间,插入失败则返回false
     */
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
    /**
     * 出队一个元素,如果存在则直接出队,如果没有空间则一直阻塞等待
     */
    E take() throws InterruptedException;
    /**
     * 出队一个元素,如果存在则直接出队,如果没有空间则等待timeout时间,无元素则返回null
     */
    E poll(long timeout, TimeUnit unit) throws InterruptedException;
    /**
     * 返回该队列剩余的容量(如果没有限制则返回Integer.MAX_VALUE)
     */
    int remainingCapacity();
    /**
     * 如果元素o在队列中存在,则从队列中删除
     */
    boolean remove(Object o);
    /**
     * 判断队列中是否存在元素o
     */
    public boolean contains(Object o);
    /**
     * 将队列中的所有元素出队,并添加到给定的集合c中,返回出队的元素数量
     */
    int drainTo(Collection<? super E> c);
    /**
     * 将队列中的元素出队,限制数量maxElements个,并添加到给定的集合c中,返回出队的元素数量
     */
    int drainTo(Collection<? super E> c, int maxElements);
}

BlockingQueue主要提供了四类方法,如下表所示:

1686465653641.png

除了抛出异常和返回特定值方法与Queue接口定义相同外,BlockingQueue还提供了两类阻塞方法:一种是当队列没有空间/元素时一直阻塞,直到有空间/有元素;另一种是在特定的时间尝试入队/出队,等待时间可以自定义。


BlockingQueue是线程安全的队列,所以提供的方法也都是线程安全的;那么下面我们就继续看下BlockingQueue的实现类,以及如何实现线程安全和阻塞。


主要实现类

BlockingQueue接口主要由5个实现类,分别如下表所示。

1686465671254.png

其中在日常开发中用的比较多的是ArrayBlockingQueue和LinkedBlockingQueue

好文参考:深入理解Java系列 | BlockingQueue用法详解 - 掘金

在 Queue 中 poll()和 remove()有什么区别?

相同点:

都是返回第一个元素,并在队列中删除返回的对象。

不同点:

如果没有元素 poll()会返回 null,而 remove()会直接抛出NoSuchElementException 异常。

并发容器之ConcurrentLinkedQueue详解与源码分析

并发容器-ConcurrentLinkedQueue详解 - 简书

并发容器之ArrayBlockingQueue与LinkedBlockingQueue详解

ArrayBlockingQueue

什么是ArrayBlockingQueue队列

用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量(类比公平锁和非公平锁)


ArrayBlockingQueue的主要属性


/** The queued items */
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;

从源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建:

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();
}

put方法详解

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    //如果当前队列已满,将线程移入到notFull等待队列中
        while (count == items.length)
            notFull.await();
    //满足插入数据的要求,直接进行入队操作
        enqueue(e);
    } finally {
        lock.unlock();
    }
}


该方法的逻辑很简单,当队列已满时(count == items.length)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用enqueue(e)插入数据元素。

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();
}

enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。

take方法详解

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    //如果队列为空,没有数据,将消费者线程移入等待队列中
        while (count == 0)
            notEmpty.await();
    //获取数据
        return dequeue();
    } finally {
        lock.unlock();
    }
}


take方法也主要做了两步:

  • 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;
  • 若队列不为空则获取数据,即完成出队操作dequeue。


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;
}


dequeue方法也主要做了两件事情:


获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);

通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。

从以上分析,可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。


可阻塞式的插入和删除队列元素是啥意思?


阻塞队列最核心的功能是能够可阻塞式的插入和删除队列元素。也就是当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程),多线程中消息通知机制最常用的是lock的condition机制


LinkedBlockingQueue

基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。


作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。


LinkedBlockingQueue是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为Integer.MAX_VALUE。从它的构造方法可以看出:


public LinkedBlockingQueue() {

   this(Integer.MAX_VALUE);

}

LinkedBlockingQueue的主要属性


/** 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主要的区别是LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(takeLock和putLock)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为:

static class Node<E> {
    E item;
    /**
     * One of:
     * - the real successor Node
     * - this Node, meaning the successor is head.next
     * - null, meaning there is no successor (this is the last node)
     */
    Node<E> next;
    Node(E x) { item = x; }
}

put方法详解

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
    //如果队列已满,则阻塞当前线程,将其移入等待队列
        while (count.get() == capacity) {
            notFull.await();
        }
    //入队操作,插入数据
        enqueue(node);
        c = count.getAndIncrement();
    //若队列满足插入数据的条件,则通知被阻塞的生产者线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}


put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。

take方法详解

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
    //当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件
        while (count.get() == 0) {
            notEmpty.await();
        }
    //移除队头元素,获取数据
        x = dequeue();
        c = count.getAndDecrement();
        //如果当前满足移除元素的条件,则通知被阻塞的消费者线程
    if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

take方法的主要逻辑请见于注释,也很容易理解。

ArrayBlockingQueue与LinkedBlockingQueue的比较

相同点

LinkedBlockingQueue和ArrayBlockingQueue都是可阻塞的队列,当队列为空,消费者线程被阻塞;当队列装满,生产者线程被阻塞;

内部都是使用ReentrantLock和Condition来保证生产和消费的同步,也就是通过condition通知机制来实现可阻塞式插入和删除元素(都使用Condition的方法来同步和通信:await()和signal()),通过ReentrantLock满足线程安全的特性;


不同点

锁机制不同

LinkedBlockingQueue中的锁是分离的,生产者的锁PutLock,消费者的锁takeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。而ArrayBlockingQueue生产者和消费者使用的是同一把锁;


底层实现机制也不同


LinkedBlockingQueue内部维护的是一个链表结构。在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。而ArrayBlockingQueue内部维护了一个数组,在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。


构造时候的区别


LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小,ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值


统计元素的个数


LinkedBlockingQueue中使用了一个AtomicInteger对象来统计元素的个数,ArrayBlockingQueue则使用int类型来统计元素。


什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

什么是原子操作?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” ,原子操作是一个不受其他操作影响的操作任务单元,原子操作是在多线程环境下避免数据不一致必须的手段。

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

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference


原子数组 :AtomicIntegerArray,AtomicLongArray, AtomicReferenceArray


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


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


扩展

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。


int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。


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


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


说一下 atomic 的原理?

说一下 atomic ?

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

说一下 atomic 的原理?

atomic通过CAS(Compare And Wwap)来保证原子性(它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回true。否则,返回false),CAS是乐观锁和自旋锁的实现核心,JDK1.8通过降低锁粒度(多段锁))增加并发性能从而避免synchronized 的高开销,执行效率大为提升。


在 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 了,但是synchronized 和ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。


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

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


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

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

CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程 等待直到倒计时结束,再开始执行。

CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可 以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障 拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

目录
相关文章
|
4月前
|
Java 开发者
Java面试题:请解释内存泄漏的原因,并说明如何使用Thread类和ExecutorService实现多线程编程,请解释CountDownLatch和CyclicBarrier在并发编程中的用途和区别
Java面试题:请解释内存泄漏的原因,并说明如何使用Thread类和ExecutorService实现多线程编程,请解释CountDownLatch和CyclicBarrier在并发编程中的用途和区别
54 0
|
安全 算法 Java
去某东面试遇到并发编程问题:如何安全地中断一个正在运行的线程
一个位5年的小伙伴去某东面试被一道并发编程的面试题给Pass了,说”如何中断一个正在运行中的线程?,这个问题很多工作2年的都知道,实在是有些遗憾。 今天,我给大家来分享一下我的回答。
94 0
|
资源调度
JUC并发编程之同步器(Semaphore、CountDownLatch、CyclicBarrier、Exchanger、CompletableFuture)附带相关面试题
1.Semaphore(资源调度) 2.CountDownLatch(子线程优先) 3.CyclicBarrier(栅栏) 4.Exchanger(公共交换区) 5.CompletableFuture(异步编程)
160 0
|
5月前
|
算法 Java 调度
《面试专题-----经典高频面试题收集四》解锁 Java 面试的关键:深度解析并发编程进阶篇高频经典面试题(第四篇)
《面试专题-----经典高频面试题收集四》解锁 Java 面试的关键:深度解析并发编程进阶篇高频经典面试题(第四篇)
70 0
|
4月前
|
Java 测试技术 开发者
Java面试题:解释CountDownLatch, CyclicBarrier和Semaphore在并发编程中的使用
Java面试题:解释CountDownLatch, CyclicBarrier和Semaphore在并发编程中的使用
71 11
|
4月前
|
设计模式 缓存 安全
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
39 0
|
4月前
|
Java
Java面试题:Java内存模型与并发编程知识点,解释Java中“happens-before”的关系,分析Java中的内存一致性效应(Memory Consistency Effects)及其重要性
Java面试题:Java内存模型与并发编程知识点,解释Java中“happens-before”的关系,分析Java中的内存一致性效应(Memory Consistency Effects)及其重要性
28 0
|
5月前
|
存储 缓存 算法
【面试宝藏】Go并发编程面试题
探索Go语言并发编程,涉及Mutex、RWMutex、Cond、WaitGroup和原子操作。Mutex有正常和饥饿模式,允许可选自旋优化。RWMutex支持多个读取者并发,写入者独占。Cond提供goroutine间的同步,WaitGroup等待任务完成。原子操作保证多线程环境中的数据完整性,sync.Pool优化对象复用。了解这些,能提升并发性能。
80 2
|
5月前
|
安全 Java API
《面试专题-----经典高频面试题收集三》解锁 Java 面试的关键:深度解析并发编程基础篇高频经典面试题(第三篇)
《面试专题-----经典高频面试题收集三》解锁 Java 面试的关键:深度解析并发编程基础篇高频经典面试题(第三篇)
39 0
|
6月前
|
机器学习/深度学习 数据采集 自然语言处理
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程