【悲观锁】Synchronized和ReentrantLock的区别
【线程方法】多线程下 wait、notify、park、unpark 和 await、signal 的区别
【同步工具】CyclicBarrier和CountDownLatch的区别
LinkedBlockingQueue
实现线程安全的关键在于使用了锁和条件变量来控制对队列的访问。下面是 LinkedBlockingQueue
实现线程安全的主要机制:
-
使用锁:
LinkedBlockingQueue
内部使用了一个锁来控制对队列的并发访问。具体来说,它使用了两个不同的锁,一个是putLock
,用于控制对队列尾部的添加操作的并发访问;另一个是takeLock
,用于控制对队列头部的移除操作的并发访问。这种分离锁的设计可以提高并发性能,减少竞争。 -
条件变量:
LinkedBlockingQueue
使用了两个条件变量notFull
和notEmpty
来实现阻塞式的添加和移除元素操作。当队列满时,添加操作会等待notFull
条件变量;当队列为空时,移除操作会等待notEmpty
条件变量。条件变量的使用使得线程可以在条件不满足时等待,在条件满足时被唤醒,从而实现了阻塞式的操作。 -
利用CAS操作和原子性操作:
LinkedBlockingQueue
使用CAS操作和原子性操作来保证对队列元素数量的更新是线程安全的。具体来说,它使用AtomicInteger
来记录队列中的元素数量,并通过CAS操作来更新数量,保证了更新操作的原子性。
put方法
public void put(E e) throws InterruptedException {
//1、首先,检查传入的元素是否为null,如果为null则抛出NullPointerException异常。
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;
//2、创建一个新的Node对象,其中包含要添加的元素。
LinkedBlockingQueue.Node<E> node = new LinkedBlockingQueue.Node<E>(e);
//3、获取putLock(一个ReentrantLock对象)的锁,以确保在多线程环境下对队列的修改操作是线程安全的。 count为队列中的元素数量
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//4、加锁设置为可打断锁
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.
*/
//5、使用一个while循环来检查队列是否已满。如果队列已满,则当前线程将会调用notFull.await()方法进入等待状态,直到队列有空间可用。在等待期间,当前线程会释放putLock锁,允许其他线程进行添加操作。
while (count.get() == capacity) {
notFull.await();
}
//6、当有空间可用时,调用enqueue(node)方法将新的Node添加到队列尾部。
enqueue(node);
//7、将count数量自增,注意此时c的值为count自增前的值
c = count.getAndIncrement();
//8、如果队列未满(即c + 1 < capacity),则调用notFull.signal()方法唤醒等待在notFull条件队列上的一个线程,通知其队列不再满了,可以继续添加元素。
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
//如果 c 的值为0,表示添加元素后队列中原来没有元素(即之前队列为空),则调用 signalNotEmpty() 方法来唤醒等待在 notEmpty 条件队列上的一个线程,通知其队列不再为空了
if (c == 0)
signalNotEmpty();
}
take方法
public E take() throws InterruptedException {
E x;
int c = -1;
//1、用于记录队列中的元素数量。
final AtomicInteger count = this.count;
//2、确保在多线程环境下对队列的修改操作是线程安全的。使用 lockInterruptibly() 方法获取锁,可响应中断。
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//3、while循环来检查队列是否为空。如果队列为空,则当前线程将会调用 notEmpty.await() 方法进入等待状态,直到队列不为空。在等待期间,当前线程会释放 takeLock 锁,允许其他线程进行添加操作。
while (count.get() == 0) {
notEmpty.await();
}
//4、当队列不为空时,调用 dequeue() 方法移除队列头部的元素,并将其赋值给局部变量 x。
x = dequeue();
//5、将队列元素数量执行自减,注意此时c的值为i--之前的值
c = count.getAndDecrement();
//6、若队列还有元素则唤醒等待在 notEmpty 条件队列上的一个线程,通知其队列不再为空。
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
//7、唤醒等待在 notFull 条件队列上的一个线程,通知其队列不再满了。
signalNotFull();
return x;
}
线程安全分析
LinkedBlockingQueue
有两把锁和dummy节点。
- ·当节点总数大于2时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是head 节点的线程安全。两把锁保证了入队和出队没有竞争。
- ·当节点总数等于2时(即一个dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争。
- 当节点总数等于1时(就一个dummy 节点)这时take 线程会被otEmpty 条件阻塞,有竞争,可能会阻塞。