English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Методы повышения производительности блокировки Java

Методы повышения производительности блокировки Java

We strive to think of solutions to the problems encountered in our products, but in this article, I will share several commonly used techniques, including lock separation, parallel data structures, protecting data rather than code, and narrowing the scope of locks, which can enable us to detect deadlocks without using any tools.

The problem is not the lock itself, but the competition between locks

When performance issues are encountered in multi-threaded code, it is generally complained that the problem is with the locks. After all, it is well known that locks can slow down the execution speed of the program and have low scalability. Therefore, if you start optimizing the code with this 'common sense', the result is likely to be annoying concurrency issues later on.

Therefore, it is very important to understand the difference between competitive locks and non-competitive locks. Lock contention is triggered when a thread tries to enter a synchronized block or method that is being executed by another thread. The thread will be forced into a waiting state until the first thread completes the synchronized block and releases the monitor. When only one thread tries to execute the synchronized code area at the same time, the lock remains in a non-competitive state.

In fact, in non-competitive situations and in most applications, JVM has optimized synchronization. Non-competitive locks do not incur any additional costs during execution. Therefore, you should not complain about locks due to performance issues, but rather about lock contention. After understanding this, let's see what we can do to reduce the possibility of contention or reduce the duration of contention.

Protect data rather than code

One quick way to solve thread safety issues is to lock the accessibility of the entire method. For example, in the following example, this method is used to create an online poker game 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*/}
}

Цель автора блага - при вступлении нового игрока в стол, необходимо обеспечить, что количество игроков за столом не будет превышать общее количество мест, которые может занять стол, 9.

Но это решение фактически требует контроля за входом игроков в стол в любое время - даже когда нагрузка на сервер较小, и те, кто ждет освобождения замка,注定 будут часто вызывать конкурентные события системы. Блокировка, которая включает проверку баланса счета и ограничения столов, может значительно увеличить затраты на операции вызова, что无疑 увеличит вероятность и продолжительность конкурентных событий.

Первым шагом к решению проблемы является обеспечение того, что мы защищаем данные, а не то, что синхронизация была перемещена из объявления метода в тело метода. В данном простом примере изменения могут быть不大. Но мы должны рассматривать это с точки зрения всего интерфейса игровой службы, а не только одного метода join().

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) {/* тело пропущено для краткости */}
 public void createTable() {/* тело пропущено для краткости */}
 public void destroyTable(Table table) {/* тело пропущено для краткости */}
}

Возможно, это всего лишь маленькое изменение, но оно может повлиять на поведение всего класса. В любое время, когда игрок присоединяется к столу, предыдущий метод синхронизации блокирует весь экземпляр GameServer, что может привести к конкуренции с теми, кто одновременно пытается покинуть стол. Перемещение замка из объявления метода в тело метода задерживает загрузку замка, что снижает вероятность конкурентных событий.

Уменьшение области действия замка

Теперь, когда мы уверены, что защищаем данные, а не программу, мы должны обеспечить, что мы блокируем только там, где это необходимо - например, после рефакторинга вышеуказанного кода:

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
}

Таким образом, код, который содержит проверку баланса счета игрока (возможные IO-операции) и может вызвать затратное время, был перемещен за пределы области действия замка. Обратите внимание, что теперь замок используется только для предотвращения превышения количества игроков, которые могут разместиться за столом, проверка баланса счета больше не является частью этой защиты.

Разделение замков

Вы можете清楚地看到 из последней строки примера, что вся структура данных защищена одним и тем же замком. Учитывая, что в такой структуре может быть несколько тысяч столов, и我们必须 обеспечить, чтобы количество игроков на любом из столов не превышало вместимость, в этом случае риск возникновения конкурентных событий все еще высок.

Один из способов решения этой проблемы - внедрение независимых блокировок для каждого стола, как показано в следующем примере:}}

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 = tables.get(table.getId());
   synchronized (tablePlayers) {
    if (tablePlayers.size() < 9) {
     tablePlayers.add(player);
    }
   }
  }
 }
 //other methods skipped for brevity
}

Теперь мы синхронизируем доступность только одного стола, а не всех столов, что значительно снижает вероятность возникновения конкуренции за блокировки. Давайте рассмотрим конкретный пример: в нашей структуре данных теперь есть 100 экземпляров столов, поэтому вероятность конкуренции за блокировки будет в 100 раз меньше, чем раньше.

Использование безопасных для многопоточности структур данных

Одна из возможных улучшений - отказаться от традиционной однопоточной структуры данных и перейти на структуры данных, явно спроектированные для работы в многопоточном режиме. Например, когда вы используете ConcurrentHashMap для хранения ваших экземпляров столов, код может выглядеть так:

public class GameServer {
 public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();
 public synchronized void join(Player player, Table table) {/*Методное тело пропущено для краткости*/}
 public synchronized void leave(Player player, Table table) {/*Методное тело пропущено для краткости*/}
 public synchronized void createTable() {
  Table table = new Table();
  tables.put(table.getId(), table);
 }
 public synchronized void destroyTable(Table table) {
  tables.remove(table.getId());
 }
}

Синхронные блоки в методах join() и leave() такие же, как и в предыдущем примере, потому что мы должны обеспечить целостность данных единственной карты стола. ConcurrentHashMap не помогает в этом вопросе. Но мы все еще будем использовать ConcurrentHashMap для создания и уничтожения новых столов в методах increaseTable() и destroyTable(), все эти операции для ConcurrentHashMap полностью синхронизированы и позволяют нам добавлять или уменьшать количество столов параллельно.

Другие советы и хитрости

Снижение видимости блокировки. В данном примере блокировка объявлена как public (внешне видна), что может позволить некоторым недобросовестным людям разрушить вашу работу, заблокировав ваш精心 спроектированный монитор.

Проверим API java.util.concurrent.locks, чтобы увидеть, есть ли другие уже реализованные стратегии блокировки, которые можно использовать для улучшения вышеуказанного решения.

Использование атомарных операций. В простом инкрементном счетчике, который в настоящее время используется, не требуется блокировка. В данном примере лучше использовать AtomicInteger вместо Integer в качестве счетчика.

В заключение, независимо от того, используете ли вы автоматическое обнаружение deadlock в решении Plumber или получаете информацию о решении из стека线程 вручную, я надеюсь, эта статья поможет вам решить проблемы с конкуренцией за блокировки.

Спасибо за чтение, надеюсь, это поможет вам, спасибо за поддержку нашего сайта!

Вам может понравиться