В таких случаях у нас есть условие гонки , когда только один из потоков получит блокировку (для ресурса), а все остальные потоки, которые хотят получить блокировку, будут заблокированы. Эта функция синхронизации не предоставляется бесплатно; JVM и ОС потребляют ресурсы, чтобы предоставить вам действующую модель параллелизма. Три наиболее фундаментальных фактора, делающих ресурсоемкую реализацию параллелизма:
- Переключение контекста
- Синхронизация памяти
- блокировка
Чтобы написать оптимизированный код для синхронизации, вы должны знать об этих 3 факторах и о том, как их уменьшить. Есть много вещей, на которые вы должны обращать внимание при написании такого кода. В этой статье я покажу вам метод уменьшения этих факторов за счет уменьшения степени детализации блокировки.
Начиная с основного правила: не держите замок дольше, чем необходимо.
Сделайте все, что вам нужно сделать, прежде чем получить блокировку, используйте блокировку только для воздействия на синхронизированный ресурс и немедленно отпустите ее. Смотрите простой пример:
01
02
03
04
05
06
07
08
09
10
|
public class HelloSync { private Map dictionary = new HashMap(); public synchronized void borringDeveloper(String key, String value) { long startTime = ( new java.util.Date()).getTime(); value = value + "_" +startTime; dictionary.put(key, value); System.out.println( "I did this in " + (( new java.util.Date()).getTime() - startTime)+ " miliseconds" ); } } |
В этом примере мы нарушаем основное правило, потому что мы создаем два объекта Date, вызываем System.out.println () и делаем много конкатенаций String. Единственное действие, которое требует синхронизации — это действие: « dictionary.put (ключ, значение); «Измените код и переместите синхронизацию из области действия метода в эту строку. Немного лучший код такой:
01
02
03
04
05
06
07
08
09
10
11
12
|
public class HelloSync { private Map dictionary = new HashMap(); public void borringDeveloper(String key, String value) { long startTime = ( new java.util.Date()).getTime(); value = value + "_" +startTime; synchronized (dictionary) { dictionary.put(key, value); } System.out.println( "I did this in " + (( new java.util.Date()).getTime() - startTime)+ " miliseconds" ); } } |
Выше код может быть написан еще лучше, но я просто хочу дать вам идею. Если вам интересно, как проверить java.util.concurrent.ConcurrentHashMap .
Итак, как мы можем уменьшить степень детализации блокировки ? В кратком ответе, спрашивая о замках как можно меньше. Основная идея состоит в том, чтобы использовать отдельные блокировки для защиты нескольких независимых переменных состояния класса вместо того, чтобы иметь только одну блокировку в области видимости класса. Проверьте этот простой пример, который я видел во многих приложениях.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public class Grocery { private final ArrayList fruits = new ArrayList(); private final ArrayList vegetables = new ArrayList(); public synchronized void addFruit( int index, String fruit) { fruits.add(index, fruit); } public synchronized void removeFruit( int index) { fruits.remove(index); } public synchronized void addVegetable( int index, String vegetable) { vegetables.add(index, vegetable); } public synchronized void removeVegetable( int index) { vegetables.remove(index); } } |
Владелец магазина может добавлять / удалять фрукты и овощи в / из своего продуктового магазина. Эта реализация Grocery защищает как фрукты, так и овощи, используя базовую блокировку Grocery, поскольку синхронизация выполняется в области действия метода. Вместо этого жирного замка мы можем использовать двух отдельных охранников, по одному на каждый ресурс (фрукты и овощи). Проверьте улучшенный код ниже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public class Grocery { private final ArrayList fruits = new ArrayList(); private final ArrayList vegetables = new ArrayList(); public void addFruit( int index, String fruit) { synchronized (fruits) fruits.add(index, fruit); } public void removeFruit( int index) { synchronized (fruits) {fruits.remove(index);} } public void addVegetable( int index, String vegetable) { synchronized (vegetables) vegetables.add(index, vegetable); } public void removeVegetable( int index) { synchronized (vegetables) vegetables.remove(index); } } |
После использования двух сторожей (разделив блокировку), мы увидим меньше блокирующего трафика, чем было бы у оригинальной жирной блокировки. Этот метод работает лучше, когда мы применяем его к блокировкам со средним конфликтом блокировок. Если мы применяем его к блокировкам, которые имеют небольшую конкуренцию, то выигрыш небольшой, но все же положительный. Если мы применяем его к замкам с сильным конфликтом, то результат не всегда лучше, и вы должны знать об этом.
Пожалуйста, используйте эту технику с совестью. Если вы подозреваете, что это серьезная конфликтная блокировка, выполните следующие действия.
- Подтвердите трафик ваших производственных требований, умножьте его на 3 или 5 (или даже 10, даже если вы хотите быть готовым).
- Запустите соответствующие тесты на своем тестовом стенде, основываясь на новом трафике.
- Сравните оба решения и только потом выберите наиболее подходящее.
Есть больше методов, которые могут улучшить производительность синхронизации, но для всех методов основное правило одно: не держите блокировку дольше, чем необходимо .
Это основное правило можно перевести как «как можно меньше запрашивать блокировки», как я уже объяснил вам, или в другие переводы (решения), которые я постараюсь описать в следующих статьях.
Еще два важных совета:
- Помните о классах в пакете java.util.concurrent (и подпакетах), так как есть очень умные и полезные реализации.
- Код параллелизма в большинстве случаев можно свести к минимуму, используя хорошие шаблоны проектирования. Всегда имейте в виду шаблоны интеграции предприятия , они могут сэкономить ваши ночи.
Ссылка: Уменьшите детализацию блокировки — Оптимизация параллелизма от нашего партнера JCG Адрианоса Дадиса в Java, Интеграция и достоинства источника .
- Учебник по параллелизму Java — Семафоры
- Учебник по параллелизму Java — повторяющиеся блокировки
- Учебник по параллелизму Java — Пулы потоков
- Учебник по параллелизму Java — Callable, будущее
- Учебник по параллелизму Java — Блокировка очередей
- Учебник по параллелизму Java — CountDownLatch
- Java Fork / Join для параллельного программирования
- Модель памяти Java — краткий обзор и на что обратить внимание
- Список учебных пособий по Java и Android