转载自:https://blog.****.net/chenssy/article/details/78681423
ArrayBlockingQueue 中的一个构造函数使用了锁:
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
上面的代码中获取互斥锁,锁的目的不是为了互斥,而是为了保证可见性。
保证可见性?保证哪个可见性?我们知道 ArrayBlockingQueue 操作的其实就是一个 items 数组,这个数组是不具备线程安全的,所以保证可见性就是保证 items 的可见性。如果不加锁为什么就没法保证 items 的可见性呢?这其实是指令重排序的问题。
为什么说指令重排序会影响 items 的可见性呢?创建一个对象要分为三个步骤:
- 分配内存空间;
- 初始化对象;
- 将内存空间的地址赋值给对应的引用。
由于指令重排序的问题,步骤 2 和步骤 3 是可能发生重排序的。这个过程就会对上面产生影响。
假如我们两个线程:线程 A负责ArrayBlockingQueue 的实例化工作,线程 B负责入队、出队操作。线程 A 优先执行,当它执行第 2 行代码,也就是this(capacity, fair);
,如下:
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();
}
注意,这里几个属性都是final修饰的,不会被重排序影响,执行完这个构造函数后,实际上实例已经完成了初始化的工作。如果之后的代码发生了重排序,即内存空间的地址赋值给对应的引用,但是c中的内容给items还没完成。
那么对于线程B而言,ArrayBlockingQueue是已经完成初始化工作了,也就是可以使用了。其实线程A可能还正在执行构造函数中后面的代码。两个线程在不加锁的情况对一个不具备线程安全的数组同时操作,很有可能会引发线程安全问题。
还有一种解释:缓存一致性。为了解决CPU处理速度以及读写主存速度不一致的问题,引入了 CPU 高速缓存。虽然解决了速度问题,但是也带来了缓存一致性的问题。在不加锁的前提下,线程A在构造函数中 items 进行操作,线程 B 通过入队、出队的方式对 items 进行操作,这个过程对 items 的操作结果有可能只存在各自线程的缓存中,并没有写入主存,这样肯定会造成数据不一致的情况。