The Java thread-safe container consists of two main categories:
Vector
, Hashtable
as well as the wrapper class Collections.synchronizedList
and Collections.synchronizedMap
;
- Java 5.0 introduces a
java.util.concurrent
package that contains concurrent queues, concurrent HashMap, and copy-on-write containers.
According to the author, the early use of the synchronization container has two main problems: 1) by adding synchronized keyword to achieve synchronization, this coarse-grained locking operation in the Synchronized keyword itself is not fully optimized, low efficiency; 2) the synchronization container is thread-safe, However, in some external composite operations (example: if not added), the client still needs to lock to ensure data security. Therefore, since Java 5.0, concurrent programming favors the use of java.util.concurrent
container classes in the package (Doug Lea), this article will also focus on the container classes in the package, mainly including:
- blocking queues
- Concurrenthashmap
- Copy container when writing
One, blocking the queue
In the concurrency environment, the blocking queue is a common data structure, which ensures the efficient and safe transmission of data, which is very convenient for fast building high quality multi-threaded applications, such as MQ's principle is based on blocking queue. java.util.concurrent
contains a rich queue implementation, as shown in the relationship between them:
- Blockingqueue, Deque (bidirectional queue) inherits from the queue interface;
- Blockingdeque also inherits from the Blockingqueue, deque interface, providing blocking bidirectional queue properties;
- Linkedblockingqueue and Linkedblockingdeque respectively realized the Blockingqueue and Blockingdeque interface;
- Delayqueue realizes the Blockingqueue interface and provides task delay function.
- Transferqueue is introduced in Java 7 and is used instead of blockingqueue,linkedtransferqueue as its implementation class.
These queues are described in more detail below:
1.1 Blockingqueue and Blockingdeque
The blocking queue (Blockingqueue) is a queue that supports two additional operations. The two additional operations are:
- When the queue is empty, the thread that gets the element waits for the queue to become non-empty.
- When the queue is full, the thread that stores the element waits for the queue to be available.
Blocking queues are often used for producer and consumer scenarios, where the producer is the thread that adds elements to the queue, and the consumer is the thread that takes the elements from the queue. The blocking queue is the container where the producer stores the elements, and the consumer only takes the elements from the container.
The blocking queue provides four methods of handling:
method |
throw exception |
Returns the special value |
has been blocked |
timeout exit |
Insert method |
add (e) |
offer (e) |
put (e) |
offer (e,time,unit) |
Remove method |
remove () |
poll () |
take () |
poll (time,unit) |
Check method |
element () |
peek () |
Not available |
|
- Throw an exception: the IllegalStateException ("Queue full") exception is thrown when the blocking queue is filled and the element is inserted into the queue. When the queue is empty, an Nosuchelementexception exception is thrown when an element is fetched from the queue.
- Returns a special value that returns True if the Insert method returns success. Remove the method by taking an element from the queue and returning null if none
- Always blocked: When the blocking queue is full, if the producer thread puts elements into the queue, the queue blocks the producer thread until it gets the data, or the response interrupts the exit. When the queue is empty, the consumer thread tries to take the element from the queue, and the queue blocks the consumer thread until the queue is available.
- Timeout exit: When the blocking queue is full, the queue blocks the producer thread for a period of time, and if a certain amount of time is exceeded, the producer thread exits.
Blockingdeque on the basis of Blockingqueue, the properties that support bidirectional queues are added. As shown, the Insert and remove methods, compared to Blockingqueue, become XxxFirst
, XxxLast
methods, corresponding to each end of the queue, either added or removed from the head, or added or removed at the tail.
1.2 Linkedblockingqueue and Linkedblockingdeque
LinkedBlockingQueue
is a bounded blocking queue implemented with a linked list. The default and maximum length for this queue is to Integer.MAX_VALUE
sort the elements according to the FIFO principle.
First look at the LinkedBlockingQueue
domain in the core:
Static class Node<e> {E item; Node<e> Next; Node (E x) {item = x;}} Private final int Capacity;private final Atomicinteger count = new Atomicinteger (); transient node<e> head;private tr Ansient node<e> last;private final Reentrantlock takelock = new Reentrantlock ();p rivate final Condition notempty = t Akelock.newcondition ();p rivate final Reentrantlock putlock = new Reentrantlock ();p rivate final Condition notfull = Putloc K.newcondition ();
LinkedBlockingQueue
And LinkedList
Similarly, the storage of elements through static internal classes Node<E>
;
capacity
Represents the maximum capacity that the blocking queue can store, which can be manually specified at the time of creation and the default maximum capacity Integer.MAX_VALUE
;
count
Represents the number of elements in the current queue, LinkedBlockingQueue
the in and out queues use two different lock objects, so both in-queue and out-of-queue involve concurrent modifications to the number of elements, so an atomic action class is used to solve the thread-safety problem of concurrent modifications to the same variable.
head
And last
the head and tail of the linked list are respectively represented;
takeLock
Represents the lock that the thread acquires when the element is out of the queue, when it is executed, while the thread obtains it, and when the take
poll
notEmpty
queue is empty, the Condition
thread that gets the element is waiting;
putLock
Represents the lock that the thread acquires when the element is queued, gets it when it is executed, and so on, and when the put
offer
notFull
queue capacity is reached capacity
, the thread that Condition
joins the element waits.
Second, LinkedBlockingQueue
there are three construction methods, respectively, as follows:
Public Linkedblockingqueue () {this (integer.max_value);} public linkedblockingqueue (int capacity) {if (capacity <= 0) throw new IllegalArgumentException (); This.capacity = capacity; last = head = new node<e> (null);} Public Linkedblockingqueue (collection<. extends e> c) {this (integer.max_value); Final Reentrantlock putlock = This.putlock; Putlock.lock (); Never contended, but necessary for visibility try {int n = 0; For (e e:c) {if (E = = null) throw new NullPointerException (); if (n = = capacity) throw new IllegalStateException ("Queue full"); Enqueue (New node<e> (E)); ++n; } count.set (n); } finally {Putlock.unlock (); }}
The default constructor is called directly LinkedBlockingQueue(int capacity)
, LinkedBlockingQueue(int capacity)
initializing the first and the end nodes and placing null. LinkedBlockingQueue(Collection<? extends E> c)
joins all elements of a collection into the queue at the same time that the queue is initialized.
The final analysis of the next put
and take
the process, here is focused on: LinkedBlockingQueue
How to implement the addition/removal of parallel?
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty();}
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;}
The reason put
they are put take
together is because they are a pair of reciprocal processes:
-
put
gets the putlock
and the number of elements of the current queue first before inserting the element, and take
gets the takelock first before removing the element
and the number of elements in the current queue;
-
put
you need to determine if the current queue is full, when the front thread waits when it is full, and take
needs to determine if the queue is empty. Queue is empty when the front thread waits;
-
put
call enqueue
insert element at the end of the team and modify the tail pointer, take
call Dequeue
places the head
at the original first
position, and resets the data field of primary to NULL, enabling the deletion of the original first
pointer and generating a new head
and, at the same time, cut off references to the original head
node for easy garbage collection.
private void Enqueue (node<e> node) {last = Last.next = node;} Private E dequeue () {node<e> h = head; node<e> first = H.next;h.next = h; Help gchead = First;
E x = First.item;first.item = Null;return x;}
-
Finally, put
determines whether the queue is not full and the queue is empty based on Count
; take
based on Count
Determines whether the triggering queue is not empty and the queue is full.
Back to the question: LinkedBlockingQueue
How do I implement Add/remove parallelism?
LinkedBlockingQueue
Different locks are used when entering the queue and out of the queue, which means that there is no mutex between the operations. in the case of multiple CPUs, can be done at the same time both consumption, and production, to do parallel processing .
Likewise, on LinkedBlockingDeque
LinkedBlockingQueue
the basis of this, the properties of the bidirectional operation are added. Continue with put
and take
for example, LinkedBlockingDeque
add the putFirst
/ putLast
, takeFirst
/ takeLast
method, respectively for the queue header, the tail to be added and deleted. Unlike the other LinkedBlockingQueue
, LinkedBlockingDeque
the in queue and out queue no longer use different lock.
final ReentrantLock lock = new ReentrantLock();private final Condition notEmpty = lock.newCondition();private final Condition notFull = lock.newCondition();
Where lock represents the primary lock for read and write, Notempty and notfull still represent the corresponding control thread state condition. Take putFirst
and takeFirst
for example:
public void putFirst(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); Node<E> node = new Node<E>(e); final ReentrantLock lock = this.lock; lock.lock(); try { while (!linkFirst(node)) notFull.await(); } finally { lock.unlock(); }}
putFirst
Inserting a NULL element is not supported, a new object is created first Node
, then the calling ReentrantLock
lock
method acquires the lock, the insert operation is boolean linkFirst(Node<E> node)
implemented, and if the current queue header is full, the thread waits (the linkFirst
method releases the lock signal after the element is written successfully), and finally, Release the lock in the finally block ( ReentrantLock
use).
public E takeFirst() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lock(); try { E x; while ( (x = unlinkFirst()) == null) notEmpty.await(); return x; } finally { lock.unlock(); }}
putFirst
Similarly, takeFirst
the lock is first acquired, then a reference to the tail element object is lifted in the try, and if unlinkFirst
it is empty, the queue is empty and no element can be deleted, then the thread waits. Again, the lock is finally released in the finally block.
So the question is, LinkedBlockingDeque
why not use LinkedBlockingQueue
the read-write lock separation method? LinkedBlockingDeque
LinkedBlockingQueue
What is the difference between a usage scenario?
1.3 Delayqueue
DelayQueue
It is mainly used to implement delay tasks, such as: waiting for a period of time after closing the connection, cache object expiration Delete, task timeout processing, etc., the common feature of these tasks is to wait for a period of time after execution (similar to timertask). DelayQueue
implementation consists of three core features:
- Deferred task:
DelayQueue
the generic class needs to inherit from the Delayed
interface, and the Delayed
interface inherits from Comparable<Delayed>
, for the comparison of precedence in the queue;
- Priority queue:
DelayQueue
the implementation of the priority queue PriorityQueue
, that is, the shorter the delay time of the task the more first (recall the priority queue in the implementation of the binary heap).
- Blocking queue: Supports concurrent read and write,
ReentrantLock
the use of the lock operation to achieve read and write.
Therefore, DelayQueue
= Delayed
+ PriorityQueue
+ BlockingQueue
.
public interface Delayed extends Comparable<Delayed> { long getDelay(TimeUnit unit);}
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> { private final transient ReentrantLock lock = new ReentrantLock(); private final PriorityQueue<E> q = new PriorityQueue<E>(); private Thread leader = null; private final Condition available = lock.newCondition();}
DelayQueue
How does the read and write operation implement the delay task next?
public boolean offer(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { q.offer(e); if (q.peek() == e) { leader = null; available.signal(); } return true; } finally { lock.unlock(); }}
The lock operation is performed first, then element e is inserted into the priority queue, and the priority queue calls the generic E's CompareTo
method For comparison (for the operation of the binary heap, which is not described here, refer to the Data structure section of the correlation analysis), Adds a task with the shortest delay time to the team header. Finally check whether the next element is a team header, if it is a team header, set leader to empty, wake up all waiting queues, release the lock.
Public E take () throws Interruptedexception {final Reentrantlock lock = This.lock; Lock.lockinterruptibly (); try {for (;;) {E first = Q.peek (); if (first = = null) available.await (); else {Long delay = First.getdelay (nanoseconds); if (delay <= 0) return Q.poll (); first = null; Don ' t retain ref while waiting if (leader! = NULL) available.await (); else {Thread thisthread = Thread.CurrentThread (); leader = Thisthread; try {Available.awaitnanos (delay); } finally {if (leader = = Thisthread) leader = null; }}}}} finally {if (leader = = null && q.peek () = null) Available.signal(); Lock.unlock (); }}
- The lock operation is performed first, then the queue header is removed, and if the head is empty, the thread is blocked;
- Gets the delay time of the head element, if the delay time is less than or equal to 0, indicating that the element has reached the time that can be used, call the poll method to eject the element;
- When the delay time is greater than 0 o'clock, the first reference is released (avoiding memory leaks), and second, if the leader thread is not empty, then the thread is blocked (indicating that threads are waiting). Otherwise, assign the current thread to the leader element and then block the delay time, which is to wait for the queue header to arrive at the delay time, releasing the reference to the leader element in the finally block. After the loop, remove the enemy element and exit the For loop.
- Finally, if leader is empty and the priority queue is not empty (judging if there are any other subsequent nodes), call signal notifies the other thread and performs the unlock operation.
1.4 Transferqueue and LinkedTransferQueue
TransferQueue
is an inherited BlockingQueue
interface and adds a number of new methods. LinkedTransferQueue
is an TransferQueue
implementation class for an interface that is defined as an unbounded queue, with an FIFO-first-out feature.
TransferQueue
Interfaces mainly include the following methods:
public interface TransferQueue<E> extends BlockingQueue<E> { boolean tryTransfer(E e); void transfer(E e) throws InterruptedException; boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException; boolean hasWaitingConsumer(); int getWaitingConsumerCount();}
- Transfer (E): If a consumer thread is currently waiting to be acquired, it is immediately handed over; otherwise, the current element E is inserted into the tail of the queue and waits for the blocking state to take the element away from the consumer thread.
- Trytransfer (E): If there is currently a consumer thread waiting to be acquired (using the take () or poll () function), using this method will immediately transfer/Transfer the object element E, and if it does not, return false and do not enter the queue. This is a non-blocking operation.
- Trytransfer (e E, long timeout, timeunit unit): If there is currently a consumer thread waiting to be acquired, it will be transferred to it immediately, otherwise the element E will be inserted at the end of the queue and wait for the consumer thread to consume If the element e cannot be fetched by the consumer thread within the specified time, false is returned and the element is removed.
- Haswaitingconsumer (): Determines whether there is a consumer thread.
- Getwaitingconsumercount (): Gets the number of consumer threads waiting to get the element.
LinkedTransferQueue
Implementation of the above method, compared to LinkedBlockingQueue
when the queue is full, the queued operation will be blocked LinkedTransferQueue
, when the queue is dissatisfied can also block, as long as there is no consumer use elements. Here LinkedTransferQueue
's a look at the enqueue and the out-of-team operations: transfer
and take
methods.
public void transfer(E e) throws InterruptedException { if (xfer(e, true, SYNC, 0) != null) { Thread.interrupted(); // failure possible only due to interrupt throw new InterruptedException(); }}
public E take() throws InterruptedException { E e = xfer(null, false, SYNC, 0); if (e != null) return e; Thread.interrupted(); throw new InterruptedException();}
LinkedTransferQueue
A key approach is used by both the team and the teams:
private E xfer(E e, boolean haveData, int how, long nanos) {}
Wherein, E
represents the element to be manipulated, to represent the addition of haveData
true
data, to false
remove the data, how
there are four kinds of values: NOW
,, ASYNC
SYNC
or, respectively, the TIMED
timing of execution, nanos
expressed how
as TIMED
time limit.
( xfer
the method is more complex, and it is no longer unfolding.) In addition, the LinkedTransferQueue
CAs non-blocking synchronization mechanism is used, which is described later.
Java Concurrency (i)--thread-safe containers (top)