Статьи

Оптимизация параллелизма — снижение степени детализации блокировки

Производительность очень важна в многопоточных приложениях с высокой нагрузкой. Разработчики должны знать о проблемах параллелизма для достижения лучшей производительности. Когда нам нужен параллелизм, у нас обычно есть ресурс, который должен совместно использоваться двумя или более потоками.

В таких случаях у нас есть условие гонки , когда только один из потоков получит блокировку (для ресурса), а все остальные потоки, которые хотят получить блокировку, будут заблокированы. Эта функция синхронизации не предоставляется бесплатно; 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, даже если вы хотите быть готовым).
    • Запустите соответствующие тесты на своем тестовом стенде, основываясь на новом трафике.
    • Сравните оба решения и только потом выберите наиболее подходящее.