Основное внимание в этой статье уделяется производительности многопоточных приложений. Сначала мы определим производительность и масштабируемость, а затем тщательно изучим правило Amdahl. В следующем контенте мы рассмотрим, как использовать различные технические методы для снижения конкуренции за блокировки и как ее реализовать с помощью кода.
1. Производительность
Мы все знаем, что многопоточное чтение может использоваться для повышения производительности программы, и причина этого заключается в том, что у нас есть многоядерные процессоры или несколько процессоров. Каждое ядро процессора может самостоятельно выполнять задачи, поэтому нарушение большой задачи на серию небольших задач, которые могут выполняться независимо друг от друга, может улучшить общую производительность программы. Вы можете привести пример. Например, существует программа, которая изменяет размер всех изображений в папке на жестком диске, и применение технологии многопоточника может улучшить его производительность. Использование одного потока подхода может пройти только все файлы изображений в последовательности и выполнять модификации. Если в нашем процессоре есть несколько ядер, нет сомнений в том, что он может использовать только один из них. Используя многопоточное, мы можем сделать сканирование потока производителя, чтобы добавить каждое изображение в очередь, а затем использовать несколько рабочих потоков для выполнения этих задач. Если количество рабочих потоков совпадает с общим количеством ядер ЦП, мы можем убедиться, что каждое ядро ЦП может выполнять работу, пока не будут выполнены все задачи.
Для другой программы, которая требует большего количества ожидания ввода-вывода, общая производительность также может быть улучшена с использованием многопоточной технологии. Предположим, мы хотим написать такую программу, что нам нужно ползти все HTML -файлы определенного веб -сайта и сохранить их на локальном диске. Программа может начинаться с определенной веб -страницы, а затем разобрать все ссылки на этот веб -сайт на этой веб -странице, а затем повернуть эти ссылки по очереди, чтобы она повторилась. Поскольку требуется некоторое время, чтобы ждать с момента, когда мы инициируем запрос на удаленный веб -сайт, до того времени, когда мы получаем все данные веб -страницы, мы можем передать эту задачу нескольким потокам для выполнения. Пусть один или немного больше потока проанализируют полученную HTML -страницу и поместите найденную ссылку в очередь, оставив все другие потоки, ответственные за запрос на страницу. В отличие от предыдущего примера, в этом примере вы все равно можете получить улучшение производительности, даже если вы используете больше потоков, чем количество ядер ЦП.
В двух вышеупомянутых примерах говорится, что высокая производительность состоит в том, чтобы делать как можно больше вещей за короткое время. Это, конечно, самое классическое объяснение термина. Но в то же время использование потоков также может хорошо улучшить скорость отклика наших программ. Представьте себе, что у нас есть такое графическое приложение интерфейса, с окном ввода выше и кнопкой с именем «процесс» под окном ввода. Когда пользователь нажимает эту кнопку, приложение необходимо повторно Render статус кнопки (кнопка, по-видимому, нажата, и она возвращается в исходное состояние, когда выпускается левая кнопка мыши) и начинает обрабатывать ввод пользователя. Если эта задача занимает много времени для обработки пользовательского ввода, однопоточная программа не сможет продолжать реагировать на другие действия пользователя, такие как пользователь, щелкший событие мыши или указатель мыши, перемещающий событие, передаваемое из операционной системы, и т. Д. Ответы на эти события должны быть независимым потоком для ответа.
Масштабируемость означает, что программы имеют возможность получить более высокую производительность путем добавления вычислительных ресурсов. Представьте, что нам нужно регулировать размер многих изображений, потому что количество сердечников процессоров нашей машины ограничено, увеличение количества потоков не всегда улучшает производительность соответственно. Напротив, поскольку планировщик должен нести ответственность за создание и остановку большего количества потоков, он также будет занимать ресурсы процессора, что может снизить производительность.
1.1 Правило Амдала
В предыдущем параграфе упоминалось, что в некоторых случаях добавление дополнительных вычислительных ресурсов может улучшить общую производительность программы. Чтобы рассчитать, сколько улучшения эффективности мы можем получить, когда добавляем дополнительные ресурсы, необходимо проверить, какие части программы запускаются последовательно (или синхронно) и какие детали работают параллельно. Если мы количественно определим долю кода, который необходимо выполнить синхронно с B (например, количество строк кода, которые необходимо выполнить синхронно), и записать общее количество ядер CPU как N, то в соответствии с законом Amdahl, верхний предел повышения производительности, который мы можем получить, является::
Если n имеет тенденцию к бесконечности, (1-B)/N сходится к 0. Следовательно, мы можем игнорировать значение этого выражения, поэтому количество битов улучшения производительности сходится к 1/B, где B представляет собой долю кода, которые должны выполнять синхронно. Если B равен 0,5, это означает, что половина кода программы не может работать параллельно, а взаимный составляет 0,5, так что, даже если мы добавим бесчисленные ядра процессора, мы получаем максимум 2 -кратного улучшения производительности. Предположим, что сейчас мы изменили программу, и после модификации только 0,25 код должен выполняться синхронно. Теперь 1/0,25 = 4 означает, что если наша программа работает на оборудовании с большим количеством процессоров, она будет примерно в 4 раза быстрее, чем на одноядерном оборудовании.
С другой стороны, через закон Amdahl мы также можем рассчитать долю кода синхронизации, что программа должна основываться на цели ускорения, которую мы хотим получить. Если мы хотим достичь 100 -кратного ускорения, и 1/100 = 0,01 означает, что максимальное количество кода, которое наша программа выполняет синхронно, не может превышать 1%.
Подводя итог закона Amdahl, мы видим, что максимальное повышение производительности, которое мы получаем, добавив дополнительный процессор, зависит от того, насколько мала доля программы выполняет часть кода синхронно. Хотя в действительности не всегда легко рассчитать это соотношение, не говоря уже о некоторых крупных коммерческих системных приложениях, закон Amdahl дает нам важное вдохновение, то есть мы должны рассмотреть код, который должен быть выполнен синхронно и попытаться уменьшить эту часть кода.
1.2 Влияние на производительность
Как пишет здесь статья, мы сделали момент, что добавление большего количества потоков может повысить производительность программы и отзывчивость. Но, с другой стороны, нелегко достичь этих преимуществ, и это также требует некоторой цены. Использование потоков также повлияет на улучшение производительности.
Во -первых, первое влияние исходит от времени создания потока. Во время создания потоков JVM должен подать заявку на соответствующие ресурсы из базовой операционной системы и инициализировать структуру данных в планировщике, чтобы определить порядок потоков выполнения.
Если ваше количество потоков такое же, как и количество ядер ЦП, каждый поток будет работать на ядре, чтобы их не часто прерывали. Но на самом деле, когда ваша программа работает, операционная система также будет иметь некоторые из собственных операций, которые необходимо обрабатывать процессором. Таким образом, даже в этом случае ваш поток будет прерван и ждал, пока операционная система возобновит свою работу. Когда количество потоков превышает количество ядер ЦП, ситуация может стать хуже. В этом случае планировщик процессов JVM будет прервать определенные потоки, чтобы позволить другим потокам выполняться. Когда потоки переключаются, текущее состояние используемого потока необходимо сохранить, чтобы состояние данных могло быть восстановлено в следующий раз, когда оно будет запущено. Мало того, планировщик также обновит свою собственную внутреннюю структуру данных, которая также требует циклов ЦП. Все это означает, что переключение контекста между потоками потребляет вычислительные ресурсы ЦП, тем самым обеспечивая накладные расходы на производительность в одном резервом случае.
Еще одна накладная расходы, вызванные многопоточными программами, поступает от синхронной защиты доступа общих данных. Мы можем использовать синхронизированное ключевое слово для защиты синхронизации, или мы можем использовать летучие ключевое слово для обмена данными между несколькими потоками. Если более одного потока хочет получить доступ к общей структуре данных, произойдет споры. В настоящее время JVM должен решить, какой процесс является первым, а какой процесс отстает. Если для выполнения потока не является в настоящее время запущенным потоком, происходит переключение потока. Текущий поток должен подождать, пока он успешно приобретет объект блокировки. JVM может решить, как выполнить это «подождать». Если JVM ожидает успешного приобретения заблокированного объекта, JVM может использовать агрессивные методы ожидания, такие как постоянно пытаться приобрести заблокированный объект до тех пор, пока он не станет успешным. В этом случае этот метод может быть более эффективным, потому что он все еще быстрее сравнить переключение контекста процесса. Перемещение резьбы ожидания обратно в очередь выполнения также принесет дополнительные накладные расходы.
Поэтому мы должны стараться изо всех сил, чтобы избежать переключения контекста, вызванного соревнованиями. Следующий раздел объяснит два способа уменьшения возникновения такой конкуренции.
1.3 Блокировка конкуренции
Как упоминалось в предыдущем разделе, конкурирующий доступ к блокировке двумя или более потоками принесет дополнительные вычислительные накладные расходы, потому что конкуренция возникает, чтобы заставить планировщика ввести агрессивное состояние ожидания или позволить ему выполнить состояние ожидания, что вызывает два переключателя контекста. Есть некоторые случаи, когда последствия конкуренции с блокировкой могут быть смягчены:
1. Уменьшите объем замков;
2. Уменьшите частоту замков, которые необходимо приобрести;
3. Постарайтесь использовать оптимистичные операции блокировки, поддерживаемые аппаратным обеспечением, а не синхронизированным;
4. Попробуйте использовать синхронизированный как можно меньше;
5. Уменьшите использование кеша объектов
1.3.1 Снижение домена синхронизации
Если код содержит блокировку более чем необходимо, то этот первый метод может быть применен. Обычно мы можем перемещать одну или несколько строк кода из области синхронизации, чтобы сократить время, когда текущий поток содержит блокировку. Меньший код запускается в области синхронизации, тем раньше текущий поток отпустит блокировку, позволяя другим потокам приобрести блокировку ранее. Это согласуется с законом Amdahl, потому что это уменьшает объем кода, который необходимо выполнить синхронно.
Для лучшего понимания, посмотрите на следующий исходный код:
Общедоступный класс Reducelockduration реализует Runnable {private static final int number_of_threads = 5; Частная статическая конечная карта <строка, целое число> map = new Hashmap <String, Integer> (); public void run () {for (int i = 0; i <10000; i ++) {synchronized (map) {uuid randomuuid = uuid.randomuuid (); Целочисленное значение = integer.valueof (42); String key = randomuuid.toString (); map.put (ключ, значение); } Thread.yield (); }} public static void main (string [] args) бросает прерывания {thread [] Threads = новый поток [number_of_threads]; for (int i = 0; i <number_of_threads; i ++) {threads [i] = new Thread (новый ReduceLockDuration ()); } long startmillis = System.currentTimeMillis (); for (int i = 0; i <number_of_threads; i ++) {threads [i] .start (); } for (int i = 0; i <number_of_threads; i ++) {threads [i] .join (); } System.out.println ((System.CurrentTimeMillis ()-startMillis)+"ms"); }}В приведенном выше примере мы позволяем пять потоков конкурировать, чтобы получить доступ к экземпляру общей карты. Чтобы только один поток может одновременно получить доступ к экземпляру карты одновременно, мы размещаем операцию добавления клавиши/значения в карту в блок синхронизированного защищенного кода. Когда мы тщательно рассмотрим этот код, мы видим, что несколько предложений кода, которые вычисляют ключ и значение, не должны выполняться синхронно. Ключ и значение принадлежат только к потоку, который в настоящее время выполняет этот код. Он имеет значение только для текущего потока и не будет изменен другими потоками. Поэтому мы можем вывести эти предложения из защиты от синхронизации. следующее:
public void run () {for (int i = 0; i <10000; i ++) {uuid randomuuid = uuid.randomuuid (); Целочисленное значение = integer.valueof (42); String key = randomuuid.toString (); синхронизированный (map) {map.put (key, value); } Thread.yield (); }}Эффект уменьшения кода синхронизации является измеримым. На моей машине время выполнения всей программы было сокращено с 420 мс до 370 мс. Посмотрите, просто перемещение трех строк кода из блока защиты синхронизации может сократить время выполнения программы на 11%. Код Thread.yield () должен вызвать переключение контекста потока, потому что этот код сообщит JVM, что текущий поток хочет передать в настоящее время используемые в настоящее время ресурсы вычислений, чтобы другие потоки, ожидающие запуска. Это также приведет к большему количеству конкуренции с блокировкой, потому что, если это не так, поток будет занимать определенное ядро дольше, тем самым уменьшая переключение контекста потока.
1.3.2 Сплит блокировки
Другим способом сокращения конкуренции с блокировкой является распространение блока защищенного блокировки кода в несколько меньших блоков защиты. Этот метод будет работать, если вы используете блокировку в своей программе для защиты нескольких разных объектов. Предположим, мы хотим подсчитать некоторые данные через программу и внедрить простой класс счета для хранения нескольких различных статистических индикаторов, и представлять их с помощью основной переменной счета (длинный тип). Поскольку наша программа многопоточная, нам нужно синхронно защитить операции, которые получают доступ к этим переменным, потому что эти действия исходят из разных потоков. Самый простой способ достичь этого - добавить синхронизированное ключевое слово в каждую функцию, которая обращается к этим переменным.
Public Static Class ContrOnelock реализует счетчик {private long customercount = 0; Private Long ShippingCount = 0; public Synchronized void incrementCustomer () {CustomerCount ++; } public synchronized void urcementshipping () {ShippingCount ++; } public synchronized long getCustomerCount () {return CustomerCount; } public synchronized long getShippingCount () {return ShippingCount; }}Это означает, что каждая модификация этих переменных будет вызывать блокировку для других встречных экземпляров. Если другие потоки хотят вызвать метод приращения на другой различной переменной, они могут дождаться предыдущего потока, чтобы выпустить контроль блокировки, прежде чем у них появится возможность завершить его. В этом случае использование отдельной синхронизированной защиты для каждой различной переменной повысит эффективность выполнения.
Public Static Class CounterSparatelock реализует счетчик {частный статический конечный объект CustomerLock = new Object (); Частный статический конечный объект ShippingLock = new Object (); Private Long CustomerCount = 0; Private Long ShippingCount = 0; public void IncrementCustomer () {Synchronized (CustomerLock) {CustomerCount ++; }} public void IncrementShiping () {Synchronized (ShippingLock) {ShippingCount ++; }} public long getCustomerCount () {synchronized (customerlock) {return customercount; }} public long getShippingCount () {synchronized (ShippingLock) {return ShippingCount; }}}Эта реализация вводит отдельный синхронизированный объект для каждой метрики подсчета. Поэтому, когда поток хочет увеличить количество клиентов, она должна ждать другого потока, который увеличивает количество клиентов, а не ожидает еще одного потока, который увеличивает количество доставки для завершения.
Используя следующие классы, мы можем легко рассчитать улучшения производительности, вызванные разделенными замками.
Общедоступный класс Locksplating реализует runnable {private static final int number_of_threads = 5; частный счетчик; Общественный интерфейс счетчик {void incrementCustomer (); void увеличение (); long getCustomerCount (); long getshippingCount (); } public Static Class ContrOnelock реализует счетчик {...} public Static Class CounterSparatelock реализует счетчик {...} public lockspliting (счетчик) {this.counter = counter; } public void run () {for (int i = 0; i <100000; i ++) {if (threadlocalrandom.current (). nextboolean ()) {counter.incrementCustomer (); } else {counter.incrementshipping (); }}} public static void main (string [] args) бросает прерывание {thread [] Threads = новый поток [number_of_threads]; Счетчик счетчика = new ControNelock (); for (int i = 0; i <number_of_threads; i ++) {threads [i] = new Thread (New Locksplating (счетчик)); } long startmillis = System.currentTimeMillis (); for (int i = 0; i <number_of_threads; i ++) {threads [i] .start (); } for (int i = 0; i <number_of_threads; i ++) {threads [i] .join (); } System.out.println ((System.CurrentTimeMillis () - startMillis) + "ms"); }}На моей машине метод реализации одного блокировки занимает в среднем 56 мс, а реализация двух отдельных замков составляет 38 мс. Труто-потребляющее уменьшается примерно на 32%.
Другой способ улучшить это то, что мы можем даже пойти дальше, чтобы защитить чтение и писать с помощью разных замков. Оригинальный класс Counter предоставляет методы для чтения и письма подсчетов, соответственно. Однако на самом деле операции чтения не требуют защиты от синхронизации. Мы можем быть уверены, что несколько потоков могут прочитать значение текущего индикатора параллельно. В то же время операции записи должны быть синхронно защищены. Пакет java.util.concurrent предоставляет реализацию интерфейса readwritelock, который может легко достичь этого различия.
Реализация ReenterTreadWritelock поддерживает два разных замка, один защищает операцию чтения, а другой защищает операцию записи. Оба замка имеют операции для приобретения и выпуска замков. Замок записи может быть успешно получен только тогда, когда никто не приобретает блокировку чтения. И наоборот, до тех пор, пока блокировка записи не получена, блокировка чтения может быть получена несколькими потоками одновременно. Чтобы продемонстрировать этот подход, в следующем классе Counter используется ReadWritelock следующим образом:
Public Static Class CounterReadWriteLock реализует счетчик {частный финальный reenterTrareadWriteLock CustomerLock = new ReenterTreadWriteLock (); Private Final Lock CustomerWriteLock = CustomerLock.Writelock (); Private Final Lock CustomerReadLock = CustomerLock.readlock (); Частный финал reenterTreadWriteLock ShippingLock = new ReenterTreadWriteLock (); Private Final Lock ShippingWritelock = ShippingLock.Writelock (); private final Lock ShippingReadlock = ShippingLock.readlock (); Private Long CustomerCount = 0; Private Long ShippingCount = 0; public void incrementCustomer () {customerwritelock.lock (); CustomerCount ++; customerwritelock.unlock (); } public void IncrementShipPing () {ShippingWriteLock.lock (); ShippingCount ++; ShippingWritelock.unlock (); } public long getCustomerCount () {customerReadClock.lock (); Long Count = CustomerCount; customer readreadlock.unlock (); возврат подсчет; } public long GetShipCount () {ShippingReadChod.lock (); длинный счет = ShippingCount; ShippingReadLock.unlock (); возврат подсчет; }}Все операции чтения защищены блокировками чтения, и все операции записи защищены блокировками записи. Если операции чтения, выполненные в программе, намного больше, чем операции записи, эта реализация может принести большие улучшения производительности, чем в предыдущем разделе, поскольку операции чтения могут выполняться одновременно.
1.3.3 Отделение блокировки
Приведенный выше пример показывает, как разделить одну блокировку на несколько отдельных замков, чтобы каждый поток мог просто получить блокировку объекта, который они собираются изменить. Но, с другой стороны, этот метод также увеличивает сложность программы и может вызвать тупики, если он будет реализован ненадлежащим образом.
Замок отряда - это аналогичный метод с блокировкой отряда, но блокировка отряда состоит в том, чтобы добавить блокировку для защиты различных фрагментов кода или объектов, в то время как блокировка отряда состоит в том, чтобы использовать другую блокировку для защиты различных диапазонов значений. Concurrenthashmap в пакете JDK java.util.concurrent использует эту идею для повышения производительности программ, которые в значительной степени зависят от Hashmap. С точки зрения реализации, Concurrenthashmap использует 16 различных замков внутри, вместо инкапсулирования синхронно защищенной Hashmap. Каждый из 16 замков отвечает за защиту синхронного доступа к одной десятой от битов ведра (ведра). Таким образом, когда разные потоки хотят вставить ключи в разные сегменты, соответствующие операции будут защищены различными замками. Но это также принесет некоторые плохие проблемы, такие как завершение определенных операций, теперь требуется несколько замков вместо одного блока. Если вы хотите скопировать всю карту, все 16 замков должны быть получены для завершения.
1.3.4 Атомная операция
Другим способом сокращения конкуренции с блокировками является использование атомных операций, которые будут подробно рассмотреть принципы в других статьях. Пакет java.util.concurrent предоставляет атомно инкапсулированные классы для некоторых обычно используемых основных типов данных. Реализация класса атомной работы основана на функции «сравнения перестановки» (CAS), предоставленной процессором. Операция CAS будет выполнять операцию обновления только тогда, когда значение текущего регистра совпадает, как старое значение, предоставленное операцией.
Этот принцип может быть использован для увеличения значения переменной оптимистичным образом. Если наш поток знает текущее значение, он попытается использовать операцию CAS для выполнения операции приращения. Если другие потоки изменили значение переменной в течение этого периода, так называемое текущее значение, предоставленное потоком, отличается от реального значения. В настоящее время JVM пытается восстановить текущее значение и повторить еще раз, повторяя его снова, пока не преуспеет. Несмотря на то, что операции по петле будут тратить некоторые циклы ЦП, преимущество этого заключается в том, что нам не нужна никакая форма контроля синхронизации.
Внедрение приведенного ниже класса счетчика используется атомные операции. Как видите, не используется синхронизированный код.
Общедоступный статический класс Class Contratomic реализует счетчика {частное атомиклонг customercount = new Atomiclong (); Частный Atomiclong ShippingCount = new Atomiclong (); public void incrementCustomer () {customercount.incrementAndget (); } public void urcementShiping () {ShippingCount.incrementAndget (); } public long getCustomerCount () {return customercount.get (); } public long GetShipCount () {return ShippingCount.get (); }}По сравнению с классом CounterSparatelock среднее время работы было сокращено с 39 мс до 16 мс, что составляет около 58%.
1.3.5 Избегайте сегментов кода горячей точки
Типичная реализация списка записывает количество элементов, содержащихся в самом списке, поддержав переменную в содержимое. Каждый раз, когда элемент удаляется или добавляется из списка, значение этой переменной изменится. Если список используется в однопоточном приложении, этот метод понятен. Каждый раз, когда вы называете размер (), вы можете просто вернуть значение после последнего расчета. Если эта переменная подсчета не поддерживается внутри списка, каждый вызов To Size () приведет к тому, что список будет привести к повторной трансверсии и вычислению количества элементов.
Этот метод оптимизации, используемый многими структурами данных, станет проблемой, когда он находится в многопоточной среде. Предположим, мы делимся списком среди нескольких потоков, и несколько потоков одновременно добавляем или удаляем элементы в список и запросите большую длину. В настоящее время переменная графа внутри списка становится общим ресурсом, поэтому весь доступ к ней должен обрабатывать синхронно. Следовательно, переменные подсчета становятся горячей точкой во всей реализации списка.
Следующий фрагмент кода показывает эту проблему:
Общественный статический класс CarrePositoryWithCounter реализует CarrePository {Private Map <String, Car> Cars = New HashMap <String, car> (); частная карта <строка, car> trucks = new hashmap <string, car> (); частные объекты Carcountsync = new Object (); private int carcount = 0; public void AddCar (Car Car) {if (car.getLicenceplate (). startSwith ("c")) {synchronized (cars) {car foundcar = cars.get (car.getlicenceplate ()); if (foundcar == null) {cars.put (car.getlicence play (), car); синхронизированный (CarcountSync) {carcount ++; }}}} else {synchronized (trucks) {car foundcar = trucks.get (car.getlicenceplate ()); if (foundcar == null) {trucks.put (car.getlicenceplate (), car); синхронизированный (CarcountSync) {carcount ++; }}}}}} public int getCarcount () {synchronized (carcountync) {return carcount; }}}Вышеупомянутая реализация каррипозиции имеет две переменные списка внутри, одна используется для размещения элемента автомобильной стирки, а другая используется для размещения элемента грузовика. В то же время он предоставляет метод для запроса общего размера этих двух списков. Используемый метод оптимизации заключается в том, что каждый раз, когда добавляется элемент автомобиля, значение переменной внутреннего количества будет увеличиваться. В то же время увеличенная операция защищена синхронизированной, и то же самое относится и к возврату значения счета.
Чтобы избежать этой дополнительной синхронизации кода, см. Другую реализацию CarrePository ниже: он больше не использует переменную внутреннего количества, но считает это значение в реальном времени в методе возврата общего числа автомобилей. следующее:
Общественный статический класс CarrePository в связи с реализацией CarrePository {Private Map <String, Car> Cars = New HashMap <String, car> (); частная карта <строка, car> trucks = new hashmap <string, car> (); public void AddCar (Car Car) {if (car.getLicenceplate (). startSwith ("c")) {synchronized (cars) {car foundcar = cars.get (car.getlicenceplate ()); if (foundcar == null) {cars.put (car.getlicence play (), car); }}} else {synchronized (trucks) {car foundcar = trucks.get (car.getlicenceplate ()); if (foundcar == null) {trucks.put (car.getlicenceplate (), car); }}} / }}}}Теперь, только в методе GetCarcount (), доступ к защите от двух списков потребности. Как и в предыдущей реализации, синхронизация накладных расходов каждый раз, когда добавляется новый элемент, больше не существует.
1.3.6 Избегайте повторного использования кеша объектов
В первой версии Java VM накладные расходы использования нового ключевого слова для создания новых объектов относительно высоки, поэтому многие разработчики привыкли к использованию режима повторного использования объекта. Чтобы снова и снова избегать повторного создания объектов, разработчики поддерживают буферный пул. После каждого создания экземпляров объекта они могут быть сохранены в бассейне буферов. В следующий раз, когда другие потоки должны их использовать, их можно извлечь непосредственно из бассейна буферов.
На первый взгляд, этот метод очень разумный, но этот шаблон может вызвать проблемы в многопоточных приложениях. Поскольку буферный пул объектов используется между несколькими потоками, операции всех потоков при доступе к объектам в них нуждаются в синхронной защите. Накладные расходы этой синхронизации больше, чем создание самого объекта. Конечно, создание слишком большого количества объектов увеличит бремя сбора мусора, но даже учитывая это, все еще лучше избегать улучшений производительности, вызванных синхронизацией кода, чем использовать пул объектов.
Схемы оптимизации, описанные в этой статье, еще раз показывают, что каждый возможный метод оптимизации должен быть тщательно оценивается, когда она фактически применяется. Незреловое решение оптимизации, по -видимому, имеет смысл на поверхности, но на самом деле оно, вероятно, станет узким местом производительности.