JAVA high concurrency lock optimization and source code interpretation

In the era of multi-core modern systems, the use of multi-threading has significantly improved the performance of the system, but in a high-concurrency environment, fierce lock competition has a serious impact on the performance of the system, because for multi-threading, it It is not only necessary to maintain the metadata of each thread itself, but also responsible for switching between threads, constantly suspending and waking up, which wastes a lot of time. Therefore, it is necessary to discuss how to optimize the lock in multi-threading to the extreme. , to bring greater benefits to the system.

Optimization of "lock" performance
This article will focus on "lock" optimization, which will involve some JDK source code interpretation. I hope to use some internal JDK examples to illustrate the benefits of lock optimization.

The optimization of locks at the application level mainly includes the following:

  1. Reduce lock holding time
  2. Reduce the granularity of locks
  3. Read-write lock separation to replace exclusive locks
  4. lock separation
  5. Coarsening of the lock

(Maybe online readers may see different statements, but the principle is similar, this article is based on JDK1.8).

1. Reduce the holding time of the lock

For applications that use locks, in multi-threading, as long as one thread occupies the lock, other locks will wait for the current thread to release the lock. If each thread holds the lock for a very long time, then the entire system Performance will be greatly reduced. Take the following piece of code as an example:

    private synchronized void sync() {
        method1();
        mutexMethod();
        method2();
    }

Obviously, in a concurrent environment, only the mutexMethod() method is needed to achieve synchronization, and you lock the entire method, and this method needs to call three methods. If the method1 method and the method2 method are heavyweight methods, won't it waste a lot of time? ? Therefore, it is necessary for us to change the above code to the following code to reduce the lock holding time to optimize the system:

    private  void sync() {
        method1();
        synchronized (mutex) {
            mutextMethod();
        }
        method2();
    }

In fact, this method is also widely used in JDK to optimize locks. For example, the internal matcher method of the Pattern class that processes regular expressions will only lock locally when the expression is not compiled, which greatly improves the performance of the matcher method. effectiveness.
write picture description here

2. Reduce the granularity of the lock

Reducing the granularity of locks is also a solution for optimizing locks. The most typical example is the implementation principle of ConcurrentHashMap inside JDK. We all know that the difference between it and HashMap is that it is thread-safe, so have you ever thought about it? What about implementing thread safety? Inside HashMap, there are two important methods, put and get. Most people may think of adding locks to these two methods, but the internal implementation of these two methods is very complicated. If you add a lock to these two methods The method lock will cause the lock granularity to be too large, so this method is definitely not acceptable. Let's take a look at how ConcurrentHashMap achieves thread safety. I think it will not add heavy and clumsy locks like the above.
write picture description here
write picture description here
Let's take a brief look at the source code of ConcurrentHashMap. Between JDK1.7 and JDK1.8, ConcurrentHashMap has made great changes. In JDK1.7, there is a concept of segment (Segment), that is to say, HashMap is used inside ConcurrentHashMap. It is subdivided into several HashMaps, which are called segments. By default, ConcurrentHashMap is divided into 16 segments. When a table entry needs to be put, ConcurrentHashMap does not lock the entire HashMap. It first obtains which segment the table entry is placed in through hashcode, and then locks the segment. Therefore, if there are multiple threads to put at the same time operations, they are not necessarily put into a segment, so that it is possible to achieve true parallelism by locking different segments. Let's take a look at the source code of local variables and put methods under JDK1.7:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {

    // 将整个hashmap分成几个小的map,每个segment都是一个锁;与hashtable相比,这么设计的目的是对于put, remove等操作,可以减少并发冲突,对
    // 不属于同一个片段的节点可以并发操作,大大提高了性能
    final Segment<K,V>[] segments;

    // 本质上Segment类就是一个小的hashmap,里面table数组存储了各个节点的数据,继承了ReentrantLock, 可以作为互拆锁使用
    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        transient volatile HashEntry<K,V>[] table;
        transient int count;
    }
    // 基本节点,存储Key, Value值
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
    }
}

public V put(K key,V value){
   Segment<K,V> s;
   if(value==null){
       throw new NullPointException();
   int hash=hash(key);
   int j=(hash>>>segmentShift)&segmentMask;
   if((s=(Segment<K,V>)UNSAFE.getObject
       (segments,(j<<SSHIFT)+SBASE))==NULL)
       s=ensureSegment(j);
    return s.put(key,hash,value,false);
)

It can be seen that it will first find the hash value according to the key, and then locate the segment for operation. By the way, in the size() method of this class in JDK1.7, if you want to get its size, you will first try to sum it in a lock-free way. If it fails, you will first add a lock in each segment. , then sum each segment, then aggregate, and finally release the lock, so it can be seen that the performance of using the size method is not very high, but in most cases, we rarely use the size method using ConcurrentHashMap, so it is worthwhile.

In JDK1.8, the concept of segment is cancelled, table is used to save data, and each row of data is locked, which reduces the granularity of lock. The following is part of the source code of the put method in JDK1.8:
write picture description here
Since the topic discussed in this article is not ConcurrentHashMap, we will discuss its source code and the differences between different JDK versions. Readers can refer to this blog if they want to go deeper . .csdn.net/mawming/article/details/52302448 )

3. Read-write separation locks to replace exclusive locks

In the case of reading more and writing less, we can use the read-write lock ReadWriteLock, which is an excuse to use different locks for read and write operations.
write picture description here
Therefore, the read operations of data do not need to wait for each other. You read first and it reads first, and you don't want to generate dirty data like write operations. Therefore, we can conclude that the access constraint table for read-write locks is as follows:

read Write
read non-blocking block
Write block block

You can look at a simple example to see the performance of read-write locks.

package cn.just.thread.concurrent;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * 测试读写锁
 * 使用读写锁时:读读操作是并行的,所以耗费时间短
 * 使用普通锁时:读读操作是串行的,所以要耗费很多时间
 * @author Shinelon
 *
 */
public class ReaddWriteLockDemo {
    private static Lock lock=new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();   //读写锁
    private static Lock readLock=readWriteLock.readLock();      //读锁
    private static Lock writeLock=readWriteLock.writeLock();    //写锁
    private int value;
    /**
     * 读操作
     * @param lock
     * @return
     * @throws InterruptedException
     */
    public Object handRead(Lock lock) throws InterruptedException{
        try{
            lock.lock();
            Thread.sleep(1000);
            return value;
        }finally{
            lock.unlock();
        }
    }
    /**
     * 写操作
     * @param lock
     * @param index
     * @throws InterruptedException
     */
    public void handWrite(Lock lock,int index) throws InterruptedException{
        try{
            lock.lock();
            Thread.sleep(1000);
            value=index;
            System.out.println(value);
        }finally{
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final ReaddWriteLockDemo demo=new ReaddWriteLockDemo();
        Runnable readRunnable=new Runnable() {
            @Override
            public void run() {
                try{
//                  demo.handRead(readLock);    //使用读锁
                    demo.handRead(lock);        //使用普通重入锁
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Runnable writeRunnable=new Runnable() {

            @Override
            public void run() {
                try{
//                  demo.handWrite(writeLock, new Random().nextInt());
                    demo.handWrite(lock, new Random().nextInt());
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        /**
         * 启动20个读线程
         */
        for(int i=0;i<20;i++){
            new Thread(readRunnable).start();
        }
        /**
         * 启动2个写线程
         */
        for(int i=18;i<20;i++){
            new Thread(writeRunnable).start();
        }
    }
}

The above code uses ordinary reentrant locks and read-write locks to open 18 reading threads and 2 writing threads for testing. When ordinary reentrant locks are used, read operations also need to wait for each other, so the entire program is completed. It takes about 20 seconds, a long period of time, and using the read-write lock, there is no need to wait between reading operations, so the real parallel between reading and reading, only two writing threads need to wait, therefore, It takes very little time to complete, about 2 seconds. Therefore, the use of read-write locks in the case of more reads and fewer writes will greatly improve the performance of the system.

Four, lock separation

Some people may think that lock separation is similar to read-write locks. In fact, read-write locks are divided into different types according to different operations, while lock separation is an extension of read-write locks. It adopts the idea of ​​separation according to the characteristics of application functions. This may be a bit confusing. We can better understand the exclusive lock by looking at the source code of the two implementation classes of the BlockingQueue interface. (The previous article briefly explored the source code of BlockingQueue, the producer-consumer model case and the source code analysis of the data sharing queue [BlockingQueue] )

Lock its take() and put() methods in the source code of LinkedBlockingQueue (an implementation class of BlockingQueue, the data structure of the linked list), because these two operations start from the head and tail of the linked list, respectively. Therefore, they do not affect each other, so two different locks can be used to improve concurrency.
write picture description here
write picture description here
write picture description here

Fifth, the coarsening of the lock

Some readers may wonder, isn't the above about reducing the granularity of the lock? Why is it necessary to coarsen the lock here? Indeed, in some cases, the size of the lock needs to be coarsened to avoid unnecessary loss and improve performance. If you need to lock for synchronization in a series of operations, but you lock each operation, in this way, frequent locking and releasing locks will seriously deplete the performance of the system, it is better to add a large locks to avoid constant requests for locks. Take the following code as an example to see how to coarsen the lock:

public void test1(){
  synchronized(lock){
       //do something
       }
    //中间是耗时很小的操作
   synchronized(lock){
      //do something
      }

For the above case we can use the following ways to optimize the lock:

public void test1(){
   synchronized(lock){
       //do something
     }

There is also a for loop locking:

for(int i=0;i<size;i++){
   synchronized(lock){
   //.....
   }

Should be optimized to the following code:

synchronized(lock){
for(int i=0;i<size;i++){
   //.....
   }
  }

The above are several different ways of optimizing the lock. For different occasions, we can use different optimization solutions. Of course, you can also have your own unique optimization methods. In short, we must consider the system in the actual development. performance.


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325517195&siteId=291194637