In general development, the author often sees that many students only use some basic methods in treating the Java concurrent development model. For example, Volatile, synchronized. Advanced concurrent packages like Lock and atomic are not often used by many people. I think most of the reasons are due to the lack of attributes to the principle. In the busy development work, who can accurately grasp and use the correct concurrency model?
So recently, based on this idea, I plan to organize the concurrency control mechanism into an article. It is not only a memory of your own knowledge, but also hopes that the content mentioned in this article can help most developers.
Parallel program development inevitably involves issues such as multi-threading and multi-task collaboration and data sharing. In JDK, multiple ways are provided to implement concurrent control between multiple threads. For example, commonly used: internal lock, reentry lock, read-write lock and semaphore.
Java memory model
In Java, each thread has a working memory area, which stores a copy of the value of the variable in the main memory shared by all threads. When a thread executes, it operates these variables in its own working memory.
In order to access a shared variable, a thread usually acquires the lock and clears its working memory area, which ensures that the shared variable is correctly loaded from the shared memory area of all threads into the thread's working memory area. When the thread unlocks, the value of the variable in the working memory area is guaranteed to be associated with the shared memory.
When a thread uses a certain variable, regardless of whether the program uses thread synchronization operations correctly, 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 into the same shared variable, then the value of the variable is either from this thread or from that thread, and the value of the shared variable will not be composed of the reference values of the two threads.
An address that Java programs can access when a variable is used. It not only includes basic type variables and 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, so developers do not have to worry about thread safety issues of local variables.
Volatile variables can be seen between multiple threads
Since each thread has its own working memory area, it may be invisible to other threads when one thread changes its own working memory data. To do this, you can use the volatile keyword to break all threads to read and write variables in memory, so that the volatile variables are visible among multiple threads.
Variables declared as volatile can be guaranteed as follows:
1. The modifications of variables by other threads can be promptly reflected in the current thread;
2. Ensure that the current thread's modification of the volatile variable can be written back to the shared memory in time and seen by other threads;
3. Use variables declared by volatile, and the compiler will ensure their orderliness.
Synchronized keywords
Synchronized keyword synchronized is one of the most commonly used synchronization methods in Java language. In early JDK versions, synchronized's performance was not very good, and the value was suitable for occasions where lock competition was not particularly fierce. In JDK6, the gap between synchronized and unfair locks has narrowed. More importantly, synchronized is more concise and clear, and the code is readable and maintained.
Methods 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. After the violation is over, the object lock will be released. The above method is equivalent to the following writing method:
public void method(){synchronized(this){// do something …}} Secondly, synchronized can also be used to construct synchronization blocks. Compared with synchronization methods, synchronization blocks can control the synchronization code range more accurately. A small synchronization code is very fast in and out of locks, thus giving the system a higher throughput.
public void method(Object o){// beforesynchronized(o){// do something ...}// after} Synchronized can also be used for static functions:
public synchronized static void method(){}
It is important to note in this place that the synchronized lock is added to the current Class object, so all calls to this method must obtain the lock of the Class object.
Although synchronized can ensure thread safety of objects or code segments, using synchronized alone is still not enough to control thread interactions with complex logic. In order to achieve interaction between multiple threads, the wait() and notify() methods of the Object object are also required.
Typical usage:
synchronized(obj){ while(<?>){ obj.wait(); // Continue to execute after receiving the notification. }} Before using the wait() method, you need to obtain the object lock. When the wait() method is executed, the current thread may release the exclusive lock of obj for use by other threads.
When waiting for the thread on obj to receive obj.notify(), it can regain obj's exclusive lock and continue running. Note that the notify() method is to randomly evoke a thread waiting on the current object.
Here is an implementation of a blocking queue:
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.notify(); }} Synchronized and wait() and notify() should be a basic skill that Java developers must master.
Reentrantlock reentrantlock lock
Reentrantlock is called a reentrantlock. It has more powerful features than synchronized, it can interrupt and time. In the case of high concurrency, it has obvious performance advantages over synchronized.
Reentrantlock provides both fair and unfair locks. A fair lock is the first-in-first-out of the lock, and not a fair lock can be cut in line. Of course, from a performance perspective, the performance of unfair locks is much better. Therefore, in the absence of special needs, unfair locks should be preferred, but synchronized provides locking industry is not absolutely fair. Reentrantlock can specify whether the lock is fair when constructing.
When using a reentry lock, be sure to release the lock at the end of the program. Generally, the code for releasing the lock must be written in finally. Otherwise, if the program exception occurs, Loack will never be released. The synchronized lock is automatically released by the JVM at the end.
Classic usage is as follows:
try { if (lock.tryLock(5, TimeUnit.SECONDS)) { //If it has been locked, try waiting for 5s to see if the lock can be obtained. If the lock cannot be obtained after 5s, return false to continue execution // lock.lockInterruptibly(); can respond to interrupt event try { //Operation} finally { lock.unlock(); } }} catch (InterruptedException e) { e.printStackTrace(); // When the current thread is interrupted (interrupt), an InterruptedException will be thrown }Reentrantlock provides a rich variety of lock control functions, and flexibly applies these control methods to improve the performance of the application. However, it is not highly recommended to use Reentrantlock here. Reentry lock is an advanced development tool provided in JDK.
ReadWriteLock read and write lock
Reading and writing separation is a very common data processing idea. It should be considered a necessary technology in SQL. ReadWriteLock is a read-write separation lock provided in JDK5. Read and write separation locks can effectively help reduce lock competition to improve system performance. The use scenarios for separation of read and write are mainly if in the system, the number of read operations is much larger than the write operations. How to use it 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 between multiple threads. Mainly associated with locks. A Condition instance bound to Lock can be generated through the newCondition() method in the Lock interface. The relationship between a Condition object and a lock is like using the two functions Object.wait(), Object.notify() and the synchronized keywords.
Here you can extract the source code of ArrayBlockingQueue:
public class ArrayBlockingQueue extends AbstractQueue implements BlockingQueue, java.io.Serializable {/** Main lock guarding all access */final ReentrantLock lock;/** Condition for waiting take */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 Condition 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(); // Then the consumer queue has to wait 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(); // Notify put() that the thread queue has free space return x;}// other code} Semaphore Semaphore <br />Semaphore provides a more powerful control method for multi-thread collaboration. Semaphore is an extension to the lock. Whether it is the internal lock synchronized or the reentrantLock, one thread allows access to a resource at a time, while the semaphore can specify that multiple threads access a resource at the same time. From the constructor, we can see:
public Semaphore(int permits) {}
public Semaphore(int permits, boolean fair){} // Can specify whether it is fair
Permits specifies the access book for the semaphore, which means how many licenses can be applied for at the same time. When each thread only applies for one license at a time, this is equivalent to specifying how many threads can access a certain resource at the same time. Here are the main methods to use:
public void acquire() throws InterruptedException {} //Try to obtain an access permission. If it is not available, the thread will wait, knowing that a thread releases a permission or the current thread is interrupted.
public void acquireUninterruptibly(){} // Similar to acquire(), but does not respond to interrupts.
public boolean tryAcquire(){} // Try to get it, true if successful, otherwise false. This method will not wait and will return immediately.
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {} // How long does it take to wait
public void release() // is used to release a license after the on-site access resource is completed so that other threads waiting for permission can access the resource.
Let’s take a look at the examples of using semaphores provided in the JDK document. This example explains how to control resource access through semaphores.
public class Pool {private static final int MAX_AVAILABLE = 100;private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);public Object getItem() throws InterruptedException { available.acquire(); // Apply for a license// Only 100 threads can enter to obtain available items at the same time, // If more than 100, you need to wait for return getNextAvailableItem();}public void putItem(Object x) { // Put the given item back into the pool and mark it as not used if (markAsUnused(x)) { available.release(); // Added an available item, release a license, and the thread requesting the resource is activated }}// For example reference only, non-real data protected Object[] items = new Object[MAX_AVAILABLE]; // Used for object pool multiplexing objects protected boolean[] used = new boolean[MAX_AVAILABLE]; // Markup 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 simply implements an object pool with a maximum capacity of 100. Therefore, when there are 100 object requests at the same time, the object pool will have resource shortage, and threads that fail to obtain resources need to wait. When a thread finishes using an object, it needs to return the object to the object pool. At this time, since the available resources increase, a thread waiting for the resource can be activated.
ThreadLocal thread local variables <br />After just starting to contact ThreadLocal, it is difficult for me to understand the use scenarios of this thread local variable. When looking back now, ThreadLocal is a solution for concurrent access to variables between multiple threads. Unlike synchronized and other locking methods, ThreadLocal does not provide locks at all, but uses the method of exchanging space for time to provide each thread with independent copies of variables to ensure thread safety. Therefore, it is not a solution for data sharing.
ThreadLocal is a good idea to solve thread safety problems. There is a Map in the ThreadLocal class that stores a copy of variables for each thread. The key of the element in the map is a thread object, and the value corresponds to the copy of the variables for the thread. Since the Key value cannot be repeated, each "thread object" corresponds to the "copy of variables" of the thread, and it reaches thread safety.
It is particularly noteworthy. In terms of performance, ThreadLocal does not have absolute performance. When the concurrency volume is not very high, the performance of locking will be better. However, as a set of thread-safe solutions that are completely unrelated to locks, using ThreadLocal can reduce lock competition to a certain extent in high concurrency or fierce competition.
Here is a simple use of ThreadLocal:
public class TestNum { // Overwrite ThreadLocal's initialValue() method through anonymous inner class, specify the initial value private static ThreadLocal seqNum = new ThreadLocal() { public Integer initialValue() { return 0; } }; // Get 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(); //3 threads share sn, each generating a sequence 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++) { // Each thread produces 3 sequence values 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 can be found that although the sequence numbers generated by each thread share the same TestNum instance, they do not interfere with each other, but each generates independent sequence numbers. This is because ThreadLocal provides a separate copy for each thread.
Lock performance and optimization of "locks" are one of the most commonly used synchronization methods. In normal development, you can often see many students directly adding a large piece of code to the lock. Some students can only use one lock method to solve all sharing problems. Obviously, such encoding is unacceptable. Especially in high concurrency environments, fierce lock competition will lead to more obvious performance degradation of the program. Therefore, the rational use of locks is directly related to the performance of the program.
1. Thread overhead <br />In the case of multi-core, using multi-threading can significantly improve the performance of the system. However, in actual situations, using multi-threading will add additional system overhead. In addition to the resource consumption of single-core system tasks themselves, multi-threaded applications also need to maintain additional multi-threaded unique information. For example, the metadata of the thread itself, thread scheduling, thread context switching, etc.
2. Reduce lock holding time
In programs that use locks for concurrent control, when locks compete, the lock holding time of a single thread has a direct relationship with system performance. If the thread holds the lock for a long time, the competition for the lock will be more intense. Therefore, during the process of program development, the time to occupy a certain lock should be minimized to reduce the possibility of mutual exclusion between threads. For example, the following code:
public synchronized void syncMehod(){beforeMethod();mutexMethod();afterMethod();} If only the mutexMethod() method in this instance is synchronous, but in beforeMethod() and afterMethod() do not require synchronization control. If beforeMethod() and afterMethod() are heavyweight methods, it will take a long time for CPU. At this time, if the concurrency is large, using this synchronization scheme will lead to a large increase in waiting threads. Because the currently executing thread will release the lock only after all tasks have been executed.
The following is an optimized solution, which only synchronizes when necessary, so that the time for threads to hold locks can be significantly reduced and the system's throughput can be improved. The code is as follows:
public void syncMehod(){beforeMethod();synchronized(this){mutexMethod();}afterMethod();} 3. Reduce the locking particle size
Reducing the lock granularity is also an effective means to weaken the competition for multi-threaded locks. The typical use scenario of this technology is the ConcurrentHashMap class. In ordinary HashMap, whenever an add() operation or get() operation is performed on a collection, the lock of the collection object is always obtained. This operation is completely a synchronous behavior because the lock is on the entire collection object. Therefore, in high concurrency, fierce lock competition will affect the system's throughput.
If you have read the source code, you should know that HashMap is implemented in an array + linked list. ConcurrentHashMap divides the entire HashMap into several segments (Segments), and each segment is a sub-HashMap. If you need to add a new table entry, you do not lock the HashMap. The twenty search line will obtain the section in which the table entry should be stored according to the hashcode, and then lock the section and complete the put() operation. In this way, in a multi-threaded environment, if multiple threads perform write operations at the same time, as long as the item being written does not exist in the same segment, true parallelism can be achieved between threads. For specific implementation, I hope readers will take some time to read the source code of the ConcurrentHashMap class, so I will not describe it too much here.
4. Lock separation <br />A readWriteLock read and write lock mentioned earlier, then the extension of read and write separation is the separation of lock. The source code of lock separation can also be found in the JDK.
public class LinkedBlockingQueue extends AbstractQueue implements BlockingQueue, java.io.Serializable {/* Lock hold by take, poll, etc /private final ReentrantLock takeLock = new ReentrantLock();/** Wait queue for waiting take */private final Condition notEmpty = takeLock.newCondition();/** Lock hold 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 { Ex; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); // There cannot be two threads to read data at the same time try { while (count.get() == 0) { // If there is no data available, wait for the notification of put() notEmpty.await(); } x = dequeue(); // Remove an item c = count.getAndDecrement(); // Size minus 1 if (c > 1) notEmpty.signal(); // Notify other take() operations} finally { takeLock.unlock(); // Release lock} if (c == capacity) signalNotFull(); // Notify put() operation, there is already 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(); // There cannot be two threads put data at the same time try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signed if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ while (count.get() == capacity) { // If the queue is full, wait notFull.await(); } enqueue(node); // Join the queue c = count.getAndIncrement();// size plus 1 if (c + 1 < capacity) notFull.signal(); // Notify other threads if there is enough space} finally { putLock.unlock();// Release the lock} if (c == 0) signalNotEmpty();// After the insertion is successful, notify take() operation to read data}// other code }What needs to be explained here is that take() and put() functions are independent of each other, and there is no lock competition relationship between them. You only need to compete for takeLock and putLock within the respective methods of take() and put(). Thus, the possibility of lock competition is weakened.
5. Lock coarseness <br />The above mentioned reduction of lock time and granularity is done to meet the shortest time for each thread to hold the lock. However, one degree should be grasped in granularity. If a lock is constantly requested, synchronized and released, it will consume valuable resources of the system and increase system overhead.
What we need to know is that when a virtual machine encounters a series of continuous requests and releases of the same lock, it will integrate all lock operations into one request to the lock, thereby reducing the number of requests for the lock. This operation is called lock coarseness. Here is a demonstration of the integration example:
public void syncMehod(){synchronized(lock){method1();}synchronized(lock){method2();}} The form after JVM integration: public void syncMehod(){synchronized(lock){method1();method2();}}Therefore, such integration gives our developers a good demonstration effect on the grasp of lock granularity.
Lockless parallel computing <br />The above has spent a lot of time talking about locking, and it is also mentioned that locking will bring additional resource overhead for certain context switching. In high concurrency, the fierce competition for "locking" may become a system bottleneck. Therefore, a non-blocking synchronization method can be used here. This lock-free method can still ensure that data and programs maintain consistency between multiple threads in a high concurrency environment.
1. Non-blocking synchronization/lockless
The non-blocking synchronization method is actually reflected in the previous ThreadLocal. Each thread has its own independent copy of variables, so there is no need to wait for each other when computing in parallel. Here, the author mainly recommends a more important lock-free concurrency control method based on Compare And Swap CAS algorithm.
The process of CAS algorithm: It contains 3 parameters CAS (V,E,N). V represents the variable to be updated, E represents the expected value, and N represents the new value. The value of V will be set to N only when the V value is equal to the E value. If the V value is different from the E value, it means that other threads have done updates, and the current thread does nothing. Finally, CAS returns the true value of the current V. When operating CAS, it is carried out with an optimistic attitude, and it always believes that it can successfully complete the operation. When multiple threads use CAS to operate a variable at the same time, only one will win and be updated successfully, while the rest of Junhui fails. The failed thread will not be suspended, it is only told that the failure is allowed, and it is allowed to try again, and of course the failed thread will also allow the operation to be abandoned. Based on this principle, CAS operation is timely without locks, and other threads can also detect interference to the current thread and handle it appropriately.
2. Atomic weight operation
JDK's java.util.concurrent.atomic package provides atomic operation classes implemented using lock-free algorithms, and the code mainly uses the underlying native code implementation. Interested students can continue to track the native level code. I won't post the surface code implementation here.
The following mainly uses an example to show the performance gap between ordinary synchronization methods and lock-free synchronization:
public class TestAtomic {private static final int MAX_THREADS = 3;private static final int TASK_COUNT = 3;private static final int TARGET_COUNT = 100 * 10000;private AtomicInteger account = new AtomicInteger(0);private int count = 0;synchronized int inc() { return ++count;}synchronized int getCount() { return count;}public class SyncThread implements Runnable { String name; long startTime; TestAtomic out; public SyncThread(TestAtomic o, long startTime) { this.out = o; this.startTime = startTime; } @Override public void run() { int v = out.inc(); while (v < TARGET_COUNT) { v = out.inc(); } long endTime = System.currentTimeMillis(); System.out.println("SyncThread spend:" + (endTime - startTime) + "ms" + ", v=" + v); }}public class AtomicThread implements Runnable { String name; long startTime; public AtomicThread(long startTime) { this.startTime = startTime; } @Override public void run() { int v = account.incrementAndGet(); while (v < TARGET_COUNT) { v = account.incrementAndGet(); } long endTime = System.currentTimeMillis(); System.out.println("AtomicThread spend:" + (endTime - startTime) + "ms" + ", v=" + v); }}@Testpublic void testSync() throws InterruptedException { ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS); long startTime = System.currentTimeMillis(); SyncThread sync = new SyncThread(this, startTime); for (int i = 0; i < TASK_COUNT; i++) { exe.submit(sync); } Thread.sleep(10000);}@Testpublic void testAtomic() throws InterruptedException { ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS); long startTime = System.currentTimeMillis(); AtomicThread atomic = new AtomicThread(startTime); for (int i = 0; i < TASK_COUNT; i++) { exe.submit(atomic); } Thread.sleep(10000);}} The test results are as follows:
testSync():
SyncThread spend:201ms, v=1000002
SyncThread spend:201ms, v=1000000
SyncThread spend:201ms, v=1000001
testAtomic():
AtomicThread spend:43ms, v=1000000
AtomicThread spend:44ms, v=1000001
AtomicThread spend:46ms, v=1000002
I believe that such test results will clearly reflect the performance differences between internal locking and non-blocking synchronization algorithms. Therefore, the author recommends directly deeming this atomic class under atomic.
Conclusion
Finally, I have sorted out the things I want to express. In fact, there are still some classes like CountDownLatch that have not been mentioned. However, what is mentioned above is definitely the core of concurrent programming. Perhaps some readers can see a lot of such knowledge points on the Internet, but I still think that only by comparison can the knowledge be found in a suitable use scenario. Therefore, this is also the reason why the editor has compiled this article, and I hope this article can help more students.
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.