To blog |

Improving lock performance in Java

January 14, 2015 by Vladimir Šor Filed under: Locked Threads

solving jave threadlockAfter we introduced locked thread detection to Plumbr couple of months ago, we have started to receive queries similar to “hey, great, now I understand what is causing my performance issues, but what I am supposed to do now?”

We are working hard to build the solution instructions into our own product, but in this post I am going to share several common techniques you can apply independent of the tool used for detecting the lock. The methods include lock splitting, concurrent data structures, protecting the data instead of the code and lock scope reduction.

Locking is not evil, lock contention is

Whenever you face a performance  problem with the threaded code there is a chance that you will start blaming locks. After all, common “knowledge” is that locks are slow and limit scalability. So if you are equipped with this “knowledge” and start to optimize the code and getting rid of locks there is a chance that you end up introducing nasty concurrency bugs that will surface later on.

So it is important to understand the difference between contended and uncontended locks. Lock contention occurs when a thread is trying to enter the synchronized block/method currently executed by another thread. This second thread is now forced to wait until the first thread has completed executing the synchronized block and releases the monitor. When only one thread at a time is trying to execute the synchronized code, the lock stays uncontended.

As a matter of fact, synchronization in JVM is optimized for the uncontended case and for the vast majority of the applications, uncontended locks pose next to no overhead during execution. So, it is not locks you should blame for performance, but contended locks. Equipped with this knowledge, lets see what we can do to reduce either the likelihood of contention or the length of the contention.

Did you know that 16% of Java applications face degraded user experience due to lock contention? Don’t blame the locks – detect them with Plumbr instead.

Protect the data not the code

A quick way to achieve thread-safety is to lock access to the whole method. For example, take look at the following example, illustrating a naive attempt to build an online poker server:

class GameServer {
  public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();

  public synchronized void join(Player player, Table table) {
    if (player.getAccountBalance() > table.getLimit()) {
      List<Player> tablePlayers = tables.get(table.getId());
      if (tablePlayers.size() < 9) {
        tablePlayers.add(player);
      }
    }
  }
  public synchronized void leave(Player player, Table table) {/*body skipped for brevity*/}
  public synchronized void createTable() {/*body skipped for brevity*/}
  public synchronized void destroyTable(Table table) {/*body skipped for brevity*/}
}

The intentions of the author have been good – when new players join() the table, there must be a guarantee that the number of players seated at the table would not exceed the table capacity of nine.

But whenever such a solution would actually be responsible for seating players to tables – even on a poker site with moderate traffic, the system would be doomed to constantly trigger contention events by threads waiting for the lock to be released. Locked block contains account balance and table limit checks which potentially can involve expensive operations both increasing the likelihood and length of the contention.

First step towards solution would be making sure we are protecting the data, not the code by moving the synchronization from the method declaration to the method body. In the minimalistic example above, it might not change much at the first place. But lets consider the whole GameServer interface, not just the single join() method:

class GameServer {
  public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

  public void join(Player player, Table table) {
    synchronized (tables) {
      if (player.getAccountBalance() > table.getLimit()) {
        List<Player> tablePlayers = tables.get(table.getId());
        if (tablePlayers.size() < 9) {
          tablePlayers.add(player);
        }
      }
    }
  }
  public void leave(Player player, Table table) {/* body skipped for brevity */}
  public void createTable() {/* body skipped for brevity */}
  public void destroyTable(Table table) {/* body skipped for brevity */}
}

What originally seemed to be a minor change, now affects the behaviour of the whole class. Whenever players were joining tables, the previously synchronized methods locked on the GameServer instance (this) and introduced contention events to players trying to simultaneously leave() tables. Moving the lock from the method signature to the method body postpones the locking and reduces the contention likelihood.

Reduce the lock scope

Now, after making sure it is the data we actually protect, not the code, we should make sure our solution is locking only what is necessary – for example when the code above is rewritten as follows:

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);
        }
      }
    }
  }
  //other methods skipped for brevity
}

then the potentially time-consuming operation of checking player account balance (which potentially can involve IO operations) is now outside the lock scope. Notice that the lock was introduced only to protect against exceeding the table capacity and the  account balance check is not anyhow part of this protective measure.

Split your locks

When we look at the last code example, you can clearly notice that the whole data structure is protected by the same lock. Considering that we might hold thousands of poker tables in this structure, it still poses a high risk for contention events  as we have to protect each table separately from overflowing in capacity.

For this there is an easy way to introduce individual locks per table, such as in the following example:

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);
        }
      }
    }
  }
  //other methods skipped for brevity
}

Now, we synchronize separately the access to the tables, to quickly get the required tablePlayers, and then separately lock the tablePlayers to work on it. By that we have significantly reduced the likelihood of locks becoming contended, as locks became more granular.

Use concurrent data structures

Another improvement is to drop the traditional single-threaded data structures and use data structures designed explicitly for concurrent usage. For example, when picking ConcurrentHashMap to store all your poker tables would result in code similar to following:

public class GameServer {
  public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();

  public void join(Player player, Table table) {
    if (player.getAccountBalance() > table.getLimit()) {
      List<Player> tablePlayers = tables.get(table.getId());
      
      synchronized (tablePlayers) {
        if (tablePlayers.size() < 9) {
          tablePlayers.add(player);
        }
      }
    }
  }

  public void leave(Player player, Table table) {/*Method body skipped for brevity*/}

  public void createTable() {
    Table table = new Table();
    tables.put(table.getId(), table);
  }

  public void destroyTable(Table table) {
    tables.remove(table.getId());
  }
}

The synchronization in join() and leave() methods is now simpler, as we no longer need to lock the tables, thanks to the ConcurrentHashMap. However, we still need to protect the integrity of individual tablePlayers. But as we are also creating new tables and destroying tables in createTable() and destroyTable() methods, all these operations to the ConcurrentHashMap are fully concurrent, permitting to increase or reduce the number of tables in parallel.

Other tips and tricks

  • Reduce the visibility of the lock. In the example above, the locks are declared public and are thus visible to the world, so there is a chance that someone else will ruin your work by also locking on your carefully picked monitors.
  • Check out java.util.concurrent.locks to see whether any of the locking strategies implemented there will improve the solution.
  • Use atomic operations. The simple increasing counter we used in the example above does not actually require a lock. Replacing the Integer in count tracking with AtomicInteger would most suit this example just fine.

Hope the article helped you solve the lock contention issues, independent of whether you are using Plumbr automatic lock detection solution or manually extracting the information from thread dumps.

ADD COMMENT

Comments

Hi there!
Great article… Concurrency is not really easy to understand.
I have a suggestion to the “Split your locks” section.
We can reduce the lock scope with a double check for the statement “if (tablePlayers.size() < 9)" and a volatile modifier to the "tables" attribute.

if (tablePlayers.size() < 9) {
synchronized (tablePlayers) {
if (tablePlayers.size() < 9) {
tablePlayers.add(player);
}
}
}
This code doesn't create any lock if the table is full. Even another player tries to join.

[]'s
Gustavo Ehrhardt

Gustavo Ehrhardt

Thanks for the comment!
Double checked locking with volatile is one possible further optimization, indeed.

Vladimir Šor Post author

>Split your locks

This will not work: it is prohibited to access HashMap without synchonization (i.e. tables.get) while concurrent writes might appear.

> Use concurrent data structures

The example is completely flawed: all the methods are synchronized just like in the example 1. In other words, the latest example has absolutely no profit for concurrency.

Vladimir Sitnikov

Thanks for the comments, you were right! I’ve updated the examples to address the comments!

Vladimir Šor Post author