面试题答案
一键面试ArrayBlockingQueue 阻塞添加和阻塞获取操作原理
- 锁机制:
ArrayBlockingQueue
内部使用ReentrantLock
作为锁,在构造函数中初始化。例如:
public ArrayBlockingQueue(int capacity) { this(capacity, false); } 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
等方法)还是获取操作(take
等方法),都需要先获取这个锁。这保证了对队列内部数据结构(数组items
)的操作是线程安全的。
- 条件变量:
- 阻塞添加(
put
方法):put
方法用于向队列中添加元素,如果队列已满,调用线程会被阻塞。- 首先获取锁
lock.lock()
,然后检查队列是否已满if (count == items.length)
,如果满了,调用notFull.await()
,这会使当前线程释放锁并进入等待状态,直到其他线程调用notFull.signal()
唤醒它。 - 当被唤醒后,线程重新获取锁,将元素添加到队列中
items[putIndex] = e
,更新索引和队列元素个数等信息,最后调用notEmpty.signal()
通知等待在notEmpty
条件变量上的线程(即等待获取元素的线程)队列中有新元素了。 - 最后释放锁
lock.unlock()
。
- 阻塞获取(
take
方法):take
方法用于从队列中获取元素,如果队列为空,调用线程会被阻塞。- 同样先获取锁
lock.lock()
,检查队列是否为空if (count == 0)
,如果空,调用notEmpty.await()
,线程释放锁并进入等待状态,直到其他线程调用notEmpty.signal()
唤醒它。 - 被唤醒后重新获取锁,从队列中取出元素
Object x = items[takeIndex]
,更新索引和队列元素个数等信息,然后调用notFull.signal()
通知等待在notFull
条件变量上的线程(即等待添加元素的线程)队列有空间了。 - 最后释放锁
lock.unlock()
。
- 阻塞添加(
多线程环境下频繁操作可能遇到的性能问题及优化
- 性能问题:
- 锁竞争:由于
ArrayBlockingQueue
使用单个锁来保护所有操作,在多线程高并发场景下,大量线程可能会竞争这把锁,导致锁竞争激烈,线程上下文切换频繁,从而降低性能。 - 线程阻塞和唤醒开销:频繁的阻塞和唤醒线程会带来一定的系统开销,如线程状态切换的开销,影响整体性能。
- 锁竞争:由于
- 优化方法:
- 使用并发队列替代:例如
LinkedTransferQueue
或ConcurrentLinkedQueue
,它们采用无锁数据结构,避免了锁竞争问题,在高并发下性能更好。但需要注意它们与ArrayBlockingQueue
在特性上的差异,如ArrayBlockingQueue
有固定容量,而ConcurrentLinkedQueue
是无界的。 - 读写分离锁优化:如果读操作远多于写操作,可以考虑使用读写锁(如
ReentrantReadWriteLock
),读操作可以并发执行,写操作获取独占锁,减少锁竞争。不过ArrayBlockingQueue
自身设计不支持这种方式,需要对代码进行较大改造。 - 批量操作:尽量减少单个元素的操作,而是采用批量添加或批量获取的方式。例如,可以自定义方法一次添加或获取多个元素,减少锁的获取和释放次数,从而提高性能。
- 使用并发队列替代:例如