In depth study of lock optimization for Java multithreaded programming

  • 2021-01-03 20:54:42
  • OfStack

The body of the

When programming in concurrent environment, we need to use lock mechanism to synchronize the operation between multiple threads to ensure exclusive access to shared resources. It seems to be a well-known fact that locking can cause performance damage. However, locking itself does not cost much in terms of performance, which is mainly in the process of getting the lock from the thread. If there is only one thread competing for the lock and there is no multithreading competition, THEN JVM will optimize and the performance cost of locking will be largely ignored. Therefore, standardizing the operation of locking, optimizing the use method of locking and avoiding unnecessary thread contention can not only improve the performance of the program, but also avoid the thread deadlock problem caused by non-standard locking and improve the robustness of the program. The following describes several lock optimization ideas.

1. Try not to lock up methods

When a lock is placed on a normal member function, the thread acquires the object lock of the object in which the method is located. The entire object will be locked. This also means that if multiple synchronized methods provided by this object are for different businesses, then because the entire object is locked, one business business must be processed by other unrelated business threads as well. The following example illustrates this situation:

The LockMethod class contains two synchronized methods that are called in two types of business processing:


public class LockMethod {
 public synchronized void busiA() {
  for (int i = 0; i < 10000; i++) {
   System.out.println(Thread.currentThread().getName() + "deal with bussiness A:"+i);
  }
 }
 public synchronized void busiB() {
  for (int i = 0; i < 10000; i++) {
   System.out.println(Thread.currentThread().getName() + "deal with bussiness B:"+i);
  }
 }
}

BUSSA is a threaded class that handles the A business and calls the busiA () method of LockMethod:


public class BUSSB extends Thread {
 LockMethod lockMethod;
 void deal(LockMethod lockMethod){
  this.lockMethod = lockMethod;
 }

 @Override
 public void run() {
  super.run();
  lockMethod.busiB();
 }
}

TestLockMethod class, using threads BUSSA and BUSSB for business processing:


public class TestLockMethod extends Thread {
 public static void main(String[] args) {
  LockMethod lockMethod = new LockMethod();
  BUSSA bussa = new BUSSA();
  BUSSB bussb = new BUSSB();
  bussa.deal(lockMethod);
  bussb.deal(lockMethod);
  bussa.start();
  bussb.start();
 }
}

Running the program, you can see that during the execution of thread bussa, bussb is not able to enter the function busiB() because the object lock of lockMethod is captured by thread bussa.

2. Shrink the synchronization code block and lock only the data

Sometimes, for programming purposes, some people use a large block of synchnoized code. If the operation in this block is not related to the shared resource, they should be placed outside the synchronized block to avoid holding the lock for a long time, causing other thread 1 to stay in a waiting state. In particular, some loop operations, synchronous I/O operations. Not only in the number of lines of code, synchronization blocks should also be reduced in execution logic, such as adding one more condition judgment, and then synchronization if the condition is satisfied, rather than condition judgment after synchronization, so as to minimize unnecessary logic into the synchronization block.

3. Try not to include locks in locks

This often happens, thread after got A lock, the synchronization method calls another object in synchronization method, won the second lock, that could lead to a call stack how lock requests, multi-threaded cases there may be very complex and difficult to analyze abnormal situation, lead to a deadlock. The following code shows this:


synchronized(A){
 synchronized(B){
  } 
}

Or a synchronized method is called in a synchronized block:


synchronized(A){
 B b = objArrayList.get(0);
 b.method(); // This is a 1 Three synchronization methods 
}

The solution is to jump out and lock, not include locking:


{
  B b = null;
 synchronized(A){
 b = objArrayList.get(0);
 }
 b.method();
}

4. Privatize locks and manage locks internally

It is safer to have the lock as a private object that cannot be accessed externally. An object may be locked directly by another thread, in which case the thread holds the object lock of the object, as in the following case:


class A {
 public void method1() {
 }
}
class B {
 public void method1() {
  A a = new A();
  synchronized (a) { // Just lock it a.method1();
  }
 }
}

In this way of use, the object lock of object a is held externally, making it more dangerous for the lock to be used in multiple external places and causing problems for the logical flow reading of the code. A better way is to manage the lock internally within the class, and provide the synchronous operation through the interface mode when the synchronization scheme is needed externally:


class A {
 private Object lock = new Object();
 public void method1() {
  synchronized (lock){
   
  }
 }
}
class B {
 public void method1() {
  A a = new A();
  a.method1();
 }
}

5. Perform appropriate lock decomposition

Consider the following program:


public class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
 if (player.getAccountBalance() > table.getLimit()) {
  synchronized (tables) {
  List<Player> tablePlayers = tables.get(table.getId());
  if (tablePlayers.size() < 9) {
   tablePlayers.add(player);
  }
  }
 }
 }
 public void leave(Player player, Table table) {/* omit */} 
 public void createTable() {/* omit */} 
 public void destroyTable(Table table) {/* omit */}
}

In this example, the join method uses only one synchronization lock to get List in tables < Player > Object, then determine if the number of players is less than 9, if so, add 1 player. When there are thousands and thousands of List < Player > In the presence of tables, competition for tables locks is fierce. Here, we can consider doing the lock decomposition: quickly pull out the data after the List < Player > Object is locked so that other threads can quickly compete for tables object locks:


public class GameServer {
    public Map < String,
    List < Player >> tables = new HashMap < String,
    List < Player >> ();
    public void join(Player player, Table table) {
        if (player.getAccountBalance() > table.getLimit()) {
            List < Player > tablePlayers = null;
            synchronized(tables) {
                tablePlayers = tables.get(table.getId());
            }
            synchronized(tablePlayers) {
                if (tablePlayers.size() < 9) {
                    tablePlayers.add(player);
                }
            }
        }
    }
    public void leave(Player player, Table table) {
        /* omit */
    }
    public void createTable() {
        /* omit */
    }
    public void destroyTable(Table table) {
        /* omit */
    }
}


Related articles: