ArrayBlockingQueue, by definition, blocks waiting for space to become available when a put() occurs and its fixed array is full.
The space that becomes available is the element returned from a take(). In other words, elements within the fixed array are getting reused over time. The put() must write its item to a specific location in the array.
The LinkedBlockingQueue, on the other hand, is a linked list. For this discussion, let's say you've created one that's bounded, just to make it more similar to the ArrayBlockingQueue. Attempt the same thing:
put() an element when the LinkedBlockingQueue is full. It will wait for an element to become available.
But in this case, when you perform a take() - it is just going to return the head value and nuke that item. The put() then sees that the LinkedBlockingQueue is below capacity. It links its item to the tail of the list. There's no overwriting of memory like the ArrayBlockingQueue, which must remain contiguous.
Edit: this is sort of a hypothetical exercise since the code isn't written this way. But anyhow, more details here, in particular the insert and extract methods:
http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/ArrayBlockingQueue.java
The potential problem IF 2 locks were used, AND the existing code stayed more or less the same:
ArrayBlockingQueue is full
Thread 1: calls take(), gets lock A, does its thing, decrements count to [capacity-1] -- but isn't done quite yet
Thread 2: calls put(), gets lock B while T1 is still running, increments count to [capacity], releases lock B
Thread 1: signals notFull()
Thread 3: put() starts execution, even though the array really is full, overwrites an element, since ArrayBlockingQueue uses a circular increment.
This situation can't happen in a LinkedBlockingQueue.