in Java multithreaded applications, queue usage is high, and the preferred data structure for most production consumption models is the queue. Java provides thread-safe queues that can be divided into
blocking queues and non-blocking queues, where the typical example of a blocking queue is blockingqueue, a typical example of a non-blocking queue is concurrentlinkedqueue, which, in practice, chooses a blocking queue or a non-blocking queue based on actual needs.
Note: What is thread safety ? This must be clear first. Thread-safe classes, which refer to the access of shared global variables within a class, must be guaranteed to be unaffected by the form of multithreading. This class is not thread-safe if the structure of these variables is corrupted due to multi-threaded access (such as modification, traversal, viewing) or if the atomicity of these variable operations is compromised.
Talk about these two types of queue today, this article is divided into the following two sections, separated by dividing lines:
- Blockingqueue Blocking algorithm
- Concurrentlinkedqueue, non-blocking algorithm
First look at blockingqueue:
There is no need to say more about what a queue is, a word: Queues are FIFO. In contrast, the stack is LIFO. If you are unfamiliar, look for the basic data structure of the book first.
Blockingqueue, as the name implies, "Blocking queue": A queue that can provide blocking functionality.
First, look at the common methods that Blockingqueue provides:
|
May report an exception |
Returns a Boolean value |
May block |
Setting the wait Time |
Team |
Add (E) |
Offer (e) |
Put (e) |
Offer (E, timeout, unit) |
Out Team |
Remove () |
Poll () |
Take () |
Poll (timeout, unit) |
View |
Element () |
Peek () |
No |
No |
It is clear from the table above that the function of each method can be clearly seen. What I want to say is:
- The Add (e) Remove () Element () method does not block the thread. The illegalstateexception exception is thrown when the constraint is not met. For example: When the queue is filled with elements, then add (e) is called, and an exception is thrown.
- Offer (e) the poll () Peek () method does not block the thread and does not throw an exception. For example: When the queue is filled with an element and then call offer (e), the element is not inserted and the function returns FALSE.
- To implement blocking, you need to call the put (e) Take () method. When a constraint is not met, the thread is blocked.
OK, the source code on the point you will understand more. Take the Arrayblockingqueue class as an example:
For the first class of methods, it is obvious that an exception is thrown if the operation is unsuccessful. And you can see that the second kind of method is actually called, why? Because the second class of methods returns a Boolean.
Java code
- Public boolean Add (E e) {
- if (Offer (e))
- return true;
- Else
- throw new IllegalStateException ("queue full");//queue filled, throw exception
- }
- Public E Remove () {
- E x = poll ();
- if (x! = null)
- return x;
- Else
- throw new Nosuchelementexception ();//queue is empty, throw exception
- }
For the second class of methods, a very standard way to use Reentrantlock (unfamiliar friends to see my last Post http://hellosure.iteye.com/blog/1121157), In addition, there is nothing to say about the implementation of insert and extract.
Note: without looking at blocking or not, the use of this reentrantlock means that the class is thread-safe .
Java code
- Public boolean offer (e-e) {
- if (e == null) throw new NullPointerException ();
- final reentrantlock lock = this.lock;
- Lock.lock ();
- try {
- if (count = = items.length)//Queue full, return false
- Return false;
- else {
- Insert (e); Notempty.signal () is emitted in the//insert method;
- return true;
- }
- } finally {
- lock.unlock ();
- }
- }
-
- public e poll () {
- final reentrantlock lock = this.lock;
- Lock.lock ();
- try {
- if (Count == 0)//queue is empty, return false
- return null;
- E x = Extract (); The//extract method emits a notfull.signal ();
- return x;
- } finally {
- lock.unlock ();
- }
- }
For the third class of methods, this involves the condition class, briefly mentioning,
The await method means that the current thread waits until it receives a signal or is interrupted.
Signal method means: Wakes up a waiting thread.
Java code
- public void put (e e) throws interruptedexception {
- if (E = = null) throw new NullPointerException ();
- Final e[] items = this.items;
- Final Reentrantlock lock = This.lock;
- Lock.lockinterruptibly ();
- try {
- try {
- while (count = = items.length)//If the queue is full, wait for notfull this condition, when the current thread is blocked
- Notfull.await ();
- } catch (Interruptedexception IE) {
- Notfull.signal (); Wakes the current thread that is blocked by notfull
- throw ie;
- }
- Insert (e);
- } finally {
- Lock.unlock ();
- }
- }
- Public E take () throws Interruptedexception {
- Final Reentrantlock lock = This.lock;
- Lock.lockinterruptibly ();
- try {
- try {
- while (count = = 0)//If the queue is empty, wait for the notempty condition, when the current thread is blocked
- Notempty.await ();
- } catch (Interruptedexception IE) {
- Notempty.signal ();//Wake up current thread blocked by Notempty
- throw ie;
- }
- E x = extract ();
- return x;
- } finally {
- Lock.unlock ();
- }
- }
The fourth type of method is to wait for a specified time if necessary, not to mention it in detail.
Let's take a look at the specific implementation class of the Blockingqueue interface:
- Arrayblockingqueue, its constructor must take an int parameter to indicate its size
- Linkedblockingqueue, if its constructor takes a specified size parameter, the generated blockingqueue has a size limit, without size parameter, the size of the resulting blockingqueue is determined by Integer.max_value.
- Priorityblockingqueue, the sort of objects it contains is not FIFO, but is based on the order of the object's natural sort or the comparator of the constructor.
Above is the example with Arrayblockingqueue, see below linkedblockingqueue:
First, since it is a linked list, there should be node nodes, which is an internal static class:
Java code
- Static Class Node<e> {
- /** the item, volatile to ensure barrier separating write and read */
- Volatile E item;
- Node<e> Next;
- Node (E x) {item = x;}
- }
Then, for a linked list, there must be two variables to indicate the head and tail:
Java code
- /** Head pointer */
- Private transient node<e> head;//head.next is the head element of the queue
- /** Tail Hands */
- Private transient node<e> last;//last.next is null
Well, it's natural to understand the queue and the team:
Java code
- private void Enqueue (E x) {
- last = Last.next = new node<e> (x);//The queue is for last to find another
- }
- Private E dequeue () {
- node<e> first = Head.next; The team pulls the head.next out and moves the head back one
- head = First;
- E x = First.item;
- First.item = null;
- return x;
- }
In addition, Linkedblockingqueue differs from arrayblockingqueue in that there are two reentrantlock, and the size of the queue's existing elements is indicated by a Atomicinteger object.
Note: The Atomicinteger class is an atomic way of manipulating an integer variable.
Java code
- Private final Atomicinteger Count =new atomicinteger (0);
- /** exclusive lock for reading */
- Private final Reentrantlock Takelock =new reentrantlock ();
- /** queue is empty condition */
- Private final Condition Notempty = Takelock.newcondition ();
- /** exclusive lock for Write */
- Private final Reentrantlock Putlock =new reentrantlock ();
- /** whether the queue is full */
- Private final Condition notfull = Putlock.newcondition ();
There are two condition that are well understood and are also done in Arrayblockingqueue. But why do you need two reentrantlock? The following will slowly come to the road.
Let's take a look at the code for the Offer and poll method:
Java code
- Public Boolean offer (E e) {
- if (E = = null) throw new NullPointerException ();
- Final Atomicinteger count = This.count;
- if (count.get () = = capacity)
- return false;
- int c =-1;
- Final Reentrantlock Putlock =this.putlock;//of course with Putlock
- Putlock.lock ();
- try {
- if (Count.get () < capacity) {
- Enqueue (e); Team
- c = count.getandincrement (); Captain Degree +1
- if (c + 1 < capacity)
- Notfull.signal (); The queue is not full, of course it can be unlocked.
- }
- } finally {
- Putlock.unlock ();
- }
- if (c = = 0)
- Signalnotempty ();//The method emits a notempty.signal ();
- return c >= 0;
- }
- Public E poll () {
- Final Atomicinteger count = This.count;
- if (count.get () = = 0)
- return null;
- E x = null;
- int c =-1;
- Final Reentrantlock Takelock =this.takelock; The team, of course, uses Takelock.
- Takelock.lock ();
- try {
- if (Count.get () > 0) {
- x = Dequeue ();//Team
- c = count.getanddecrement ();//Team Length-1
- if (C > 1)
- Notempty.signal ();//queue not available, unlock
- }
- } finally {
- Takelock.unlock ();
- }
- if (c = = capacity)
- Signalnotfull ();//The method emits a notfull.signal ();
- return x;
- }
Look at the source code discovery and the above arrayblockingqueue very similar, the key question is: Why use two Reentrantlock putlock and Takelock?
We think about it, the team operation is actually only the end of the queue reference last, and does not involve the head. And the team operation is actually only for head, and last does not matter. Then that is, the queue and the operation of the team do not need a public lock, so the design of two locks, so that the implementation of a number of different tasks of the queue can be queued while the operation of the team, on the other hand, because two operations common use of the count is Atomicinteger type, So there is no need to consider the problem of decreasing the counter increment completely.
In addition, there is one more point to note: await () and Singal () both methods execute to check whether the current thread is the current thread of an exclusive lock, and if not, throw a java.lang.IllegalMonitorStateException exception. So you can see that both methods in the source code appear in the lock's protection block.
-------------------------------I'm a split line--------------------------------------
and then say Concurrentlinkedqueue , which is a lock-free, concurrent thread-safe queue.
The following sections refer to this post http://yanxuxin.iteye.com/blog/586943
The implementation of the lock mechanism, the difficulty of using the lock-free mechanism is to fully consider the coordination between the threads. In short, when multiple threads are accessing an internal data structure, other threads can detect and help complete the rest of the operation if one of the threads fails halfway through for some reason. This requires that the operation of the data structure is finely divided into multiple states or stages, taking into account the situation of multi-threaded access to each stage or state. The
Concurrentlinkedqueue has two volatile thread-sharing variables: Head,tail. To ensure the thread safety of this queue is to ensure the atomicity and visibility of the access (update, view) of the two node references, the atomicity of the modification is guaranteed because volatile itself guarantees visibility.
Following the implementation of the Offer method to see how to ensure atomicity in the absence of a lock:
Java code
- Public Boolean offer (E e) {
- if (E = = null) throw new NullPointerException ();
- node<e> n = new node<e> (E, NULL);
- for (;;) {
- node<e> t = tail;
- node<e> s = t.getnext ();
- if (t = = tail) {//------------------------------a
- if (s = = null) {//---------------------------B
- if (T.casnext (S, N)) {//-------------------C
- Castail (t, N); ------------------------D
- return true;
- }
- } else {
- Castail (t, s); ----------------------------E
- }
- }
- }
- }
The loop of this method first obtains the tail pointer and its next object, since tail and node's next are volatile, guaranteeing that the difference is the most recent value.
Code a: T==tail is the top level of coordination, and if other threads change the tail reference, then it is now time to get a tail pointer that is not up-to-date and needs to be re-cycled to get the latest value.
Code B: S==null's judgment. At rest, tail next must be pointing to null, but another state under multithreading is the middle: tail's direction has not changed, but its next point is pointing to a new node, that is, the state before the tail reference change is complete, s!=null. Here is the typical application of coordination, directly intoCode eTo coordinate the thread participating in the middle State to complete the final update, and then cycle through the new tail to start your own new queue attempt. It is also noteworthy that between A and B, other threads may change the direction of the tail so that the coordinated operation fails. From this step, you can see the complexity of the lock-free implementation.
Code C: T.casnext (S, N) is the first step in the queue because it takes two steps: Update node's next and change the direction of tail. Before code C may occur tail reference to the change or into the updated intermediate state, both of which will make T point to the element's next property is changed by the atom, no longer point to null. The code C operation fails and re-enters the loop.
Code D: This is the last step to complete the update, is to update the point of tail, the most interesting coordination here is also reflected. Looking at Castail (t, n) from the code, whether or not it succeeds will then return true to mark the success of the update. First, if success indicates that this thread has completed a two-step update, it is natural to return true; if Castail (t, N) is unsuccessful? It is clear that the completion code C means that the update is in the middle State, the code D is unsuccessful, and the point of tail is changed by other threads. means for other threads: they get an update of the middle State, s!=null, intoCode eHelp this thread perform the last step and succeed before this thread. This thread, although the code D failed, but because the assistance of other threads was completed first, so it is natural to return true.
By analyzing this queue operation, we can clearly see the coordination and work of multi-threading in every step and state without lock implementation.
Note: Above this large paragraph of text looks very tired, first can understand how much to understand how much, now do not understand first not anxious, the following will also mention this algorithm, and with the instructions, easy to understand a lot.
When using Concurrentlinkedqueue, be aware that if you directly use the functions it provides, such as the Add or poll methods, we do not need to do any synchronization ourselves.
But if non-atomic operations, such as:
Java code
- if (!queue.isempty ()) {
- Queue.poll (obj);
- }
It is difficult to guarantee that the queue was not modified by another thread until IsEmpty () was called, poll (). So for this situation, we still need to synchronize ourselves:
Java code
- Synchronized (queue) {
- if (!queue.isempty ()) {
- Queue.poll (obj);
- }
- }
Note: This needs to be synchronized to the situation, depending on the situation, not in any case need to do so.
Also say, Concurrentlinkedqueue's size () is to iterate over the collection, so try to avoid using size instead of IsEmpty (), to avoid slow performance.
OK, finally want to say something, blocking algorithm is actually very good understanding, simple understanding is to add lock, for example, see in Blockingqueue, then push point forward, that is synchronized. In contrast, the design and implementation of non-blocking algorithms are difficult to support concurrency through low-level atomicity. Here is a brief introduction to the non-blocking algorithm , the following section of the content refers to a very classic article http://www.ibm.com/developerworks/cn/java/j-jtp04186/
Note: I think it can be understood that blocking corresponds to synchronization, non-blocking corresponds to concurrency. Can also be said: Synchronization is blocking mode, asynchronous non-blocking mode
Give an example of what a non-blocking algorithm is: a nonblocking counter
First, use the synchronized thread-safe counter code as follows
Java code
- Public Finalclass Counter {
- Private Long value = 0;
- Public Synchronizedlong GetValue () {
- return value;
- }
- Public Synchronizedlong increment () {
- return ++value;
- }
- }
The following code shows one of the simplest non-blocking algorithms: Counters using Atomicinteger's Compareandset () (CAs method ). The Compareandset () method Specifies "update this variable to a new value, but if the value has been modified by another thread since I last saw it, the update fails"
Java code
- public class Nonblockingcounter {
- Private Atomicinteger value;//mentioned earlier, the Atomicinteger class is an atomic way of manipulating integer variables.
- public int GetValue () {
- return Value.get ();
- }
- public int increment () {
- int V;
- do {
- v = value.get ();
- while (!value.compareandset (V, v + 1));
- return v + 1;
- }
- }
The non-blocking version has several performance advantages over lock-based versions. First, it replaces the JVM's locking code path with the native form of the hardware to synchronize at a finer level of granularity (independent memory location), and the failed thread can retry immediately without being suspended and re-dispatched. Finer granularity reduces the chance of contention, and the ability to retry without rescheduling reduces contention costs. Even with a small number of failed CAS operations, this approach is still much faster than the rescheduling caused by lock contention.
Nonblockingcounter This example may be simpler, but it demonstrates a basic feature of all non-blocking algorithms-some of the algorithm steps are risky, knowing that if the CAS are unsuccessful, they may have to be re-made. Non-blocking algorithms are often called optimistic algorithms because they continue to operate with the assumption that there will be no interference. If interference is found, it will be rolled back and tried again. In the example of a counter, the risk step is incrementing-it retrieves the old value and adds one to the old value, hoping that the value will not change during the calculation of the update. If its hope fails, it retrieves the value again and increments the calculation.
Another example, Michael-scott non-blocking queue algorithm insert operation, Concurrentlinkedqueue is implemented with this algorithm, now to combine analysis, it is clear:
Java code
- public class Linkedqueue <E> {
- Private Staticclass Node <E> {
- Final E item;
- Final atomicreference<node<e>> next;
- Node (E item, node<e> next) {
- This.item = Item;
- This.next = new Atomicreference<node<e>> (next);
- }
- }
- Private atomicreference<node<e>> Head
- = new Atomicreference<node<e>> (new Node<e> (Null,null));
- Private atomicreference<node<e>> tail = head;
- Public Boolean put (E item) {
- node<e> NewNode = new node<e> (item,null);
- while (true) {
- Node<e> curtail = Tail.get ();
- node<e> residue = CurTail.next.get ();
- if (curtail = = Tail.get ()) {
- if (residue = = null)/* A */{
- if (CurTail.next.compareAndSet (null, NewNode))/*/C */{
- Tail.compareandset (Curtail, newNode)/* D */;
- return true;
- }
- } else {
- Tail.compareandset (curtail, residue)/* B */;
- }
- }
- }
- }
- }
Look at this code is completely concurrentlinkedqueue source AH.
Inserting an element involves the head pointer and the tail pointer two pointer updates, both of which are made through the CAS: link to the new node from the current last node (C) of the queue, and move the tail pointer to the new last node (D). If the first step fails, the status of the queue remains unchanged, and the insert thread continues to retry until it succeeds. Once the operation succeeds, the insert is considered as valid and the other threads can see the modification. You also need to move the tail pointer to the location of the new node, but this work can be seen as "clean up", because any thread in this situation can determine whether this cleanup is needed and how to clean it up.
A queue is always in one of two states: normal (or stationary, figure 1 and Figure 3), or intermediate states (Figure 2). After the insert operation and the second CAS (D) succeeds, the queue is in a stationary state, and after the first CAS (C) succeeds, the queue is in the middle state. At rest, the next field of the linked node pointed to by the tail pointer is always NULL, while in the middle State, the field is non-null. Any thread can determine the state of the queue by comparing whether Tail.next is null, which is critical for the thread to help other threads "complete" the operation.
shown: There are two elements in a stationary queue
The insert operation checks whether the queue is in an intermediate state before inserting a new element (A). If it is in the middle state, then there must be other threads that are already halfway through the element insertion, between steps (C) and (D). Instead of waiting for other threads to finish, the current thread can "help" it complete the operation and move the tail pointer forward (B). If necessary, it will also continue to check the tail pointer and move the pointer forward until the queue is stationary, and it can begin its own insertion.
The first CAS (C) may fail because two threads compete to access the current last element of the queue, in which case no modification occurs, and the thread that loses the CAS is re-loaded into the tail pointer and tried again. If the second CAS (D) fails, the insert thread does not need to retry-because the other thread has done this for it in step (B)!
Display: A queue inserted in an intermediate state, after the new element is inserted, before the tail pointer is updated
Display: Queue is back at rest after tail pointer update
Thread-safe queue for Java Multithreading summary