Java concurrency control mechanism learning Notes

Source: Internet
Author: User
Tags semaphore static class volatile


In general development, I often see that many students only use some basic methods in the java concurrent development model. For example, volatile and synchronized. Advanced release packages such as Lock and atomic are not frequently used. I think most of the reason is due to the non-attribute of the principle. Who can accurately grasp and use the correct concurrency model during busy development?
So recently, based on this idea, I plan to organize this part of the concurrency control mechanism into an article. It is not only a memory of your knowledge, but also a hope that the class content mentioned in this article can help most developers.
Parallel Program development inevitably involves multiple threads, multi-task collaboration, and data sharing. In JDK, multiple methods are provided to control concurrency among multiple threads. For example, internal locks, reentrant locks, read/write locks, and semaphores are commonly used.

Java memory model

In java, each thread has a working memory zone, which stores copies of the variable values in the main memory shared by all threads. When the thread executes, it operates these variables in its own working memory.
To access a shared variable, a thread usually gets the lock first and clears its working memory zone, this ensures that the shared variable is correctly loaded into the working memory of the thread from the shared memory area of all threads, when the thread is unlocked, ensure that the values of the variables in the working memory area are merged into the shared memory.
When a thread uses a variable, whether or not the program correctly uses the thread synchronization operation, the value it obtains must be the value stored in the variable by itself or other threads. For example, if two threads store different values or object references in the same shared variable, the value of the variable is either in this thread or in that thread, the value of the shared variable is not composed of the reference values of two threads.
An address that the Java program can access when a variable is used. It includes not only basic type variables, reference type variables, but also array type variables. Variables stored in the main memory area can be shared by all threads, but it is impossible for one thread to access the parameters or local variables of another thread. Therefore, developers do not have to worry about thread security issues of local variables. Pay attention to the interaction between the thread working memory and the main memory in the memory model.
Volatile variable-visible among multiple threads

Since each thread has its own working memory zone, when a thread changes its data in the working memory, it may be invisible to other threads. To this end, you can use the volatile keyword to read and write the variables in the memory, so that the volatile variables are visible among multiple threads.
Variables declared as volatile can be guaranteed as follows:
1. Modifications made to variables by other threads can be promptly reflected in the current thread;
2. Make sure that the current thread modifies the volatile variable and writes it back to the shared memory in time, which is visible to other threads;
3. The compiler ensures the orderliness of variables declared by volatile.
Synchronization keyword synchronized
Synchronized is one of the most common synchronization methods in Java. In earlier JDK versions, synchronized has poor performance and is suitable for lock competition. Synchronized and non-fairness in JDK 6
The lock gap has been narrowed. More importantly, synchronized is more concise and clear, and the code is easier to read and maintain.
How to lock an object:

Public synchronized void method (){}
'''

When the method () method is called, the calling thread must first obtain the current object. If the current object lock is held by other threads, the calling thread will wait, the object lock will be released. The above method is equivalent to the following statement:
Public void method (){
Synchronized (this ){
// Do something...
}
}

Second, synchronized can also be used to construct synchronization blocks. Compared with the synchronization method, synchronization blocks can more accurately control the scope of synchronization code. A small synchronization code is very fast-forward and fast-out with locks, so that the system has a higher throughput.
Public void method (Object o ){
// Before
Synchronized (o ){
// Do something...
}
// After
}

Synchronized can also be used for static functions:
Public synchronized static void method (){}


Note that the synchronized lock is added to the ** Current Class object **. Therefore, all calls to this method must obtain the lock of the Class object.

Although synchronized can guarantee the thread security of objects or code segments, it is not enough to use synchronized alone to control thread interaction with complex logic. To achieve interaction between multiple threads, you also need to use the wait () and Y () methods of the Object.
Typical usage:
Synchronized (obj ){
While (<?>) {
Obj. wait ();
// Continue execution after receiving the notification.
    }
}
You must obtain the object lock before using the wait () method. When the wait () method is executed, the current thread or the exclusive lock of obj is released for other threads.
When the thread on obj receives obj. Y (), it can obtain the exclusive lock of obj again and continue running. Note that the notify () method is ** randomly aroused ** waiting in a thread of the current object.
The following is an implementation of blocking queues:
Public class BlockQueue {
Private List list = new ArrayList ();

Public synchronized Object pop () throws InterruptedException {
While (list. size () = 0 ){
This. wait ();
        }
If (list. size ()> 0 ){
Return list. remove (0 );
} Else {
Return null;
        }
    }

Public synchronized Object put (Object obj ){
List. add (obj );
This. Policy ();
    }

}

** Synchronized should be used with wait () and Y () ** as basic skills for Java developers.

# Reentrantlock
Reentrantlock is called a re-entry lock. It has more powerful functions than synchronized, and can be interrupted and timed. In the case of high concurrency, it has obvious performance advantages over synchronized.
Reentrantlock provides two locks: fair and unfair. A fair lock is a first-in-first-out lock, but a non-fair lock can be inserted into the queue. Of course, from the performance analysis, the performance of unfair locks is much better. Therefore, unfair locks should be preferred without special needs, but synchronized is not absolutely fair in providing locks. When constructing Reentrantlock, you can specify whether the lock is fair.
When using the re-import lock, you must release the lock at the end of the program. Generally, the lock release code should be written in finally. Otherwise, if the program encounters an exception, Loack will never be released. The synchronized lock is automatically released by JVM.
The classic mode is as follows:
Try {
If (lock. tryLock (5, TimeUnit. (SECONDS) {// if the lock has been applied, wait for 5 s to see if the lock can be obtained. If the lock still cannot be obtained after 5s, false is returned to continue execution.
// Lock. lockInterruptibly (); can respond to interrupt events
Try {
// Operation
} Finally {
Lock. unlock ();
            }
    }
} Catch (InterruptedException e ){
E. printStackTrace (); // when the current thread is interrupted (interrupt), InterruptedException will be thrown
}
Reentrantlock provides a rich set of lock control functions. You can flexibly apply these control methods to improve application performance. However, Reentrantlock is not strongly recommended here. The re-entry lock is an advanced development tool provided by JDK. Here is an article dedicated to ** [comparison of ReentrantLock and synchronized locking mechanisms] (http://blog.csdn.net/fw0124/article/details/6672522 )**.

# ReadWriteLock read/write lock
Read/write splitting is a very common data processing idea. It should be a necessary technology in SQL. ReadWriteLock is the read/write splitting lock provided in jdk5. Read/write splitting locks can effectively help reduce lock competition to improve system performance. Read/write splitting is mainly used in scenarios where the number of read operations in the system is far greater than that in write operations. The usage is as follows:
Private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock ();
Private Lock readLock = readWriteLock. readLock ();
Private Lock writeLock = readWriteLock. writeLock ();
Public Object handleRead () throws InterruptedException {
Try {
ReadLock. lock ();
Thread. sleep (1000 );
Return value;
} Finally {
ReadLock. unlock ();
    }
}
Public Object handleRead () throws InterruptedException {
Try {
WriteLock. lock ();
Thread. sleep (1000 );
Return value;
} Finally {
WriteLock. unlock ();
    }
}

# Condition object

The Conditiond object is used to coordinate complex collaboration among multiple threads. It is mainly associated with the phase lock. You can use the newCondition () method in the Lock interface to generate a Condition instance bound to the Lock. The relationship between the Condition Object and the lock is the same as that between the Object. wait () and Object. Policy () functions and the synchronized keyword.
Here we can take a look at the source code of ArrayBlockingQueue:
Public class ArrayBlockingQueue extends actqueue
Implements BlockingQueue, java. io. Serializable {
/** Main lock guarding all access */
Final ReentrantLock lock;
/** Condition for waiting takes */
Private final Condition notEmpty;
/** Condition for waiting puts */
Private final Condition notFull;

Public ArrayBlockingQueue (int capacity, boolean fair ){
If (capacity <= 0)
Throw new IllegalArgumentException ();
This. items = new Object [capacity];
Lock = new ReentrantLock (fair );
NotEmpty = lock. newCondition (); // Generate a Condition bound to the Lock
NotFull = lock. newCondition ();
}

Public void put (E e) throws InterruptedException {
CheckNotNull (e );
Final ReentrantLock lock = this. lock;
Lock. lockInterruptibly ();
Try {
While (count = items. length)
NotFull. await ();
Insert (e );
} Finally {
Lock. unlock ();
    }
}

Private void insert (E x ){
Items [putIndex] = x;
PutIndex = inc (putIndex );
++ Count;
NotEmpty. signal (); // notification
}

Public E take () throws InterruptedException {
Final ReentrantLock lock = this. lock;
Lock. lockInterruptibly ();
Try {
While (count = 0) // if the queue is empty
NotEmpty. await (); // The consumer queue waits for a non-empty signal.
Return extract ();
} Finally {
Lock. unlock ();
    }
}

Private E extract (){
Final Object [] items = this. items;
E x = this. <E> cast (items [takeIndex]);
Items [takeIndex] = null;
TakeIndex = inc (takeIndex );
-- Count;
NotFull. signal (); // notifies the put () thread queue that there is free space
Return x;
}

// Other code
}


# Semaphore
Semaphores provide more powerful control methods for multi-threaded collaboration. Semaphores are an extension of the lock. Both the internal lock synchronized and the ReentrantLock allow one thread to access one resource at a time, while the Semaphore allows multiple threads to access one resource at a time. The constructor shows that:

'''
Public Semaphore (int permits ){}
Public Semaphore (int permits, boolean fair) {}// you can specify whether it is fair.
'''

Permits specifies the Semaphore access book, that is, the number of licenses that can be applied for at the same time. When each thread applies for only one license each time, this is equivalent to specifying how many threads can access a certain resource at the same time. The usage of the main methods is as follows:

> * Public void acquire () throws InterruptedException {} // try to obtain a permission for access. If it cannot be obtained, the thread will wait and know that a thread is allowed to release or the current thread is interrupted.
* Public void acquireUninterruptibly () {}// similar to acquire (), but does not respond to interruptions.
* Public boolean tryAcquire () {}// try to get it. If it succeeds, it is true; otherwise, it is false. This method does not wait and returns immediately.
* Public boolean tryAcquire (long timeout, TimeUnit unit) throws InterruptedException {}// how long does the attempt wait?
* Public void release () // used to release a license after the on-site access to the resource ends, so that other threads waiting for the permission can access the resource.

Next, let's take a look at the example that uses semaphores in the JDK Documentation. This instance explains how to control resource access through semaphores.
Public class Pool {
Private static finals int MAX_AVAILABLE = 100;
Private final Semaphore available = new Semaphore (MAX_AVAILABLE, true );
Public Object getItem () throws InterruptedException {
Available. acquire ();
// Apply for a license
// At the same time, only 100 threads can enter to obtain available items,
// Wait for more than 100
Return getNextAvailableItem ();
}

Public void putItem (Object x ){
// Put the given item back into the pool and mark it as unused
If (markAsUnused (x )){
Available. release ();
// A new available item is added, releasing a license, and the thread requesting the resource is activated
    }
}

// For example only, non-real data
Protected Object [] items = new Object [MAX_AVAILABLE]; // used for reusing objects in the Object pool
Protected boolean [] used = new boolean [MAX_AVAILABLE]; // tag function

Protected synchronized Object getNextAvailableItem (){
For (int I = 0; I <MAX_AVAILABLE; ++ I ){
If (! Used [I]) {
Used [I] = true;
Return items [I];
        }
    }
Return null;
}

Protected synchronized boolean markAsUnused (Object item ){
For (int I = 0; I <MAX_AVAILABLE; ++ I ){
If (item = items [I]) {
If (used [I]) {
Used [I] = false;
Return true;
} Else {
Return false;
            }
        }
    }
Return false;
}
}

This instance implements an object pool with a maximum capacity of 100. Therefore, when there are 100 object requests at the same time, there will be a resource shortage in the object pool, and the threads that fail to obtain the resources need to wait. After a thread uses an object, it needs to return the object to the object pool. In this case, a thread waiting for the resource can be activated because the available resources are increased.

# ThreadLocal thread local variables
At the beginning, it was difficult for me to understand the use cases of ThreadLocal variables. Looking back, ThreadLocal is a solution for concurrent variable access among multiple threads. Unlike synchronized locking, ThreadLocal does not provide a lock at all. Instead, it uses space-for-time means to provide independent copies of variables for each thread to ensure thread security, therefore, it is not a data sharing solution.
ThreadLocal is a good idea to solve the thread security problem. The ThreadLocal class has a Map used to store copies of variables of each thread. The Keys of elements in the Map are thread objects, the value corresponds to the variable copy of the thread. Because the Key value cannot be repeated, every "thread object" corresponds to the "variable copy" of the thread, thus achieving thread safety.
In terms of performance, ThreadLocal is not absolutely the same. When the concurrency is not very high, the performance of locking is better. However, as a thread security solution that has nothing to do with locks, ThreadLocal can be used to reduce lock competition to some extent in high concurrency or highly competitive scenarios.
The following is a simple use of ThreadLocal:
Public class TestNum {
// Override the initialValue () method of ThreadLocal through an anonymous internal class and specify the initial value
Private static ThreadLocal seqNum = new ThreadLocal (){
Public Integer initialValue (){
Return 0;
}
};
// Obtain the next sequence value
Public int getNextNum (){
SeqNum. set (seqNum. get () + 1 );
Return seqNum. get ();
}

Public static void main (String [] args ){
TestNum sn = new TestNum ();
// Three threads share the sn, each generating a serial number
TestClient t1 = new TestClient (sn );
TestClient t2 = new TestClient (sn );
TestClient t3 = new TestClient (sn );
T1.start ();
T2.start ();
T3.start ();
}

Private static class TestClient extends Thread {
Private TestNum sn;

Public TestClient (TestNum sn ){
This. sn = sn;
    }

Public void run (){
For (int I = 0; I <3; I ++ ){
// Three sequence values are output for each thread.
System. out. println ("thread [" + Thread. currentThread (). getName () + "] --> sn ["
+ Sn. getNextNum () + "]");
        }
    }
}
}

Output result:

Thread [Thread-0]-> sn [1]
Thread [Thread-1]-> sn [1]
Thread [Thread-2]-> sn [1]
Thread [Thread-1]-> sn [2]
Thread [Thread-0]-> sn [2]
Thread [Thread-1]-> sn [3]
Thread [Thread-2]-> sn [2]
Thread [Thread-0]-> sn [3]
Thread [Thread-2]-> sn [3]

The output result information shows that although the serial numbers generated by each thread share the same TestNum instance, they do not interfere with each other, but they each generate independent serial numbers, this is because ThreadLocal provides a separate copy for each thread.

# Lock performance and optimization

"Lock" is one of the most common synchronization methods. During normal development, it is often seen that many people directly add a lot of code to the lock. Other users only use one lock method to solve all sharing problems. Obviously, such encoding is unacceptable. Especially in a highly concurrent environment, fierce lock competition will lead to a more obvious reduction in program performance. Therefore, the rational use of the lock is directly related to the program performance.
** 1. Thread overhead **
In multi-core scenarios, multithreading can significantly improve the system performance. However, in actual situations, multithreading will increase the overhead of the system. Compared with the resource consumption of single-core system tasks, multi-threaded applications also need to maintain the unique information of extra multi-threading. For example, the metadata of the thread itself, thread scheduling, and thread context switching.
** 2. Reduce lock hold time **
In the program that uses the lock for concurrency control, when the lock is competitive, the lock hold time of a single thread has a direct relationship with the system performance. If the thread holds the lock for a long time, the competition for the lock will be more intense. Therefore, in the process of program development, we should minimize the time occupied by a lock to reduce the possibility of mutual exclusion between threads. For example, the following code:

Public synchronized void syncMehod (){
BeforeMethod ();
MutexMethod ();
AfterMethod ();
}


In this example, only the mutexMethod () method is required for synchronization, while in beforeMethod (), and afterMethod (), synchronization control is not required. If beforeMethod () and afterMethod () are both heavyweight methods, it takes a long CPU time. At this time, if the concurrency is large, using this synchronization scheme will lead to a large increase of waiting threads. Because the lock is released only after all tasks are executed by the currently executed thread.
The following is an optimized solution, which only synchronizes data when necessary. This can significantly reduce the lock hold time of the thread and improve the throughput of the system. The code is as follows:
Public void syncMehod (){
BeforeMethod ();
Synchronized (this ){
MutexMethod ();
}
AfterMethod ();
}

 

** 3. Reduce lock granularity **
Reducing the lock granularity is also an effective way to weaken the competition for multi-thread locks. The typical use scenario of this technology is ConcurrentHashMap. In a normal HashMap, whenever the add () operation or get () operation is performed on the set, the lock of the set object is always obtained. This kind of operation is completely a synchronization behavior, because the lock is on the entire set object, so in high concurrency, fierce lock competition will affect the throughput of the system.
If you have read the source code, you should know that HashMap is implemented through arrays and linked lists. On the basis of HashMap, ConcurrentHashMap divides the entire HashMap into several segments, each of which is a subhashmap. If you need to add a new table item, instead of locking the HashMap, you can find out which segment the table item should be stored based on the hashcode and then lock it, and complete the put () operation. In this way, in a multi-threaded environment, if multiple threads perform the write operation at the same time, as long as the written item does not exist in the same segment, the real parallel operation can be achieved between threads. The specific implementation requires the reader to spend some time reading the source code of the ConcurrentHashMap class, so we will not describe it too much here.
** 4. Lock separation **
Read/Write locks have been mentioned before, so the extension of read/write splitting is the separation of locks. You can also find the lock separation source code javasblockingqueue in JDK.
Public class extends blockingqueue extends actqueue
Implements BlockingQueue, java. io. Serializable {
/* Lock held by take, poll, etc/
Private final ReentrantLock takeLock = new ReentrantLock ();
/** Wait queue for waiting takes */
Private final Condition notEmpty = takeLock. newCondition ();

/** Lock held by put, offer, etc */
Private final ReentrantLock putLock = new ReentrantLock ();

/** Wait queue for waiting puts */
Private final Condition notFull = putLock. newCondition ();


Public E take () throws InterruptedException {
E x;
Int c =-1;
Final AtomicInteger count = this. count;
Final ReentrantLock takeLock = this. takeLock;
TakeLock. lockInterruptibly (); // two threads cannot read data simultaneously.
Try {
While (count. get () = 0) {// if no data is available currently, wait until the notification of put ()
NotEmpty. await ();
        }
X = dequeue (); // remove an item from the header
C = count. getAndDecrement (); // The size is reduced by 1.
If (c> 1)
NotEmpty. signal (); // notify other take () operations
} Finally {
TakeLock. unlock (); // release the lock
    }
If (c = capacity)
SignalNotFull (); // notifies you of the put () operation, which has free space.
Return x;
}

Public void put (E e) throws InterruptedException {
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;
Node <E> node = new Node (e );
Final ReentrantLock putLock = this. putLock;
Final AtomicInteger count = this. count;
PutLock. lockInterruptibly (); // two threads cannot put data at the same time.
Try {
/*
* Note that count is used in wait guard

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.