Если при разработке программ на Java используется блокировка, это означает, что используется блокирующая форма параллелизма — наихудший уровень параллелизма. Оптимизация блокировки заключается в максимально возможном улучшении соответствующего кода, который включает действия блокировки в многопоточных программах с высокой степенью параллелизма, чтобы можно было максимально повысить эффективность выполнения. Конечно, даже если код, использующий блокировки, оптимизирован до предела, его производительность не может превзойти lock-free, ведь блокировки будут вызывать зависание потоков (относительно, это занимает много времени и тратит ресурсы). Но мы должны найти способ минимизировать эту потерю, что является отправной точкой оптимизации блокировок.
Вообще говоря, оптимизация блокировки Java имеет следующие идеи или методы:
Сокращение времени удержания блокировки
Уменьшить детализацию блокировки
разделение замка
блокировка огрубления
Ниже приводится подробное введение в различные методы или идеи оптимизации блокировок:
Сокращение времени удержания блокировки
Блокировка может удерживаться только одним потоком за раз, а другие потоки, которые хотят занять блокировку, должны ждать освобождения блокировки за пределами критической секции Это время ожидания может быть длинным или коротким в зависимости от фактического приложения и кода. запись, например следующий код:
public synchronized void syncMethod(){
noneLockedCode1();//不需要加锁的代码
needLockedMethed();//需要线程安全的代码
noneLockedCode2();//不需要加锁的代码
}
В методе syncMethod вызываются три метода, и каждый метод представляет собой блок кода. Если только один метод, needLockedMethod(), должен быть потокобезопасным, два других метода не нужно выполнять в блоке синхронизированного кода, то вы можете поставить Приведенный выше код улучшен следующим образом:
public void syncMethod(){
noneLockedCode1();
synchronized(this){
needLockedMethed();
}
noneLockedCode2();
}
Таким образом, выполнение методов noneLockedCode1 и noneLockedCode2 не будет занимать время блокировки, что сократит время ожидания блокировки другими потоками, тем самым повысив производительность программы и оптимизировав использование блокировки.
Уменьшить детализацию блокировки
Блокировка может быть для очень тяжелого объекта (объект будет заблокирован многими потоками).В это время, если большой объект разделить на более мелкие объекты с меньшей степенью детализации, можно увеличить параллелизм программы и блокировку между несколькими потоками Конкуренция повышает вероятность успеха блокировки, тем самым достигая цели оптимизации блокировки.
Важным примером уменьшения детализации блокировок является реализация ConcurrentHashMap.
Мы знаем, что HashMap может обеспечить потокобезопасность следующим образом: Collections.synchronizedMap(Map
public static <K,V>Map<K,V> synchronizedMap(Map<K,V>m) {
return new SynchronizedMap<>(m);
}
Его реализация также очень проста, он просто синхронизирует методы get и set для взаимного исключения, код реализации выглядит следующим образом:
public V get(Object key) {
synchronized (mutex) {
return m.get(key);
}
}
public V put(K key, V value) {
synchronized (mutex) {
return m.put(key, value);
}
}
Что плохого в этом?
Хеш-карта здесь на самом деле очень тяжелый объект, потому что он может хранить много данных.Когда несколько потоков одновременно обращаются к нему, будь то чтение (получение) или запись (помещение), они должны получить пару мьютексов. ( mutex ), поэтому чтение и запись будут блокировать друг друга, то есть SynchronizedMap на самом деле поддерживает только чтение и запись в один объект, хранящийся в нем, что, несомненно, принесет большую потерю производительности, и чем больше данных в карте, чем больше потоков обращается к карте, тем больше потеря производительности.
Условно говоря, ConcurrentHashMap — это высокопроизводительная хеш-таблица, и эта высокая производительность обусловлена только тем, что она выполняет операцию, которая снижает степень детализации блокировки. В исходниках ConcurrentHashMap мы можем найти, что он разбивает весь Hashmap на несколько небольших сегментов, и каждый сегмент представляет собой небольшой hashmap, когда есть поток для обработки данных в нем, операция в реальном времени разделяется. небольшой сегмент, ConcurrentHashMap позволяет нескольким потокам входить одновременно, тем самым повышая степень параллелизма и достигая цели оптимизации блокировок.
разделение замка
Если для системы существуют требования чтения и записи, обычные блокировки (например, синхронизированные) заставят чтение блокировать запись, запись также блокирует чтение, и в то же время чтение, чтение и запись также блокируются. оптимизация заключается в том, чтобы найти способ сделать блокировку как можно меньше, и определенную оптимизационную роль здесь будет играть блокировка чтения-записи.
Основная идея блокировки чтения-записи состоит в том, чтобы разделить чтение и запись, потому что чтение не изменит данные, поэтому нет необходимости в синхронизации между чтением и чтением, другие ситуации чтения-записи, записи-чтения, записи-записи следующим образом:
Читать замок | блокировка записи | |
---|---|---|
блокировка чтения | может получить доступ | не доступный |
блокировка записи | не доступный | не доступный |
Как видно из таблицы, пока есть блокировка записи, требуется обработка синхронизации, но для большинства приложений сценарий чтения намного больше, чем сценарий записи, поэтому после использования блокировки чтения-записи в сценарий большего чтения и меньшего количества записи, вы можете очень хорошо улучшить производительность системы, что является разделением блокировок. После разделения блокировки блокировка чтения и блокировка чтения перестают быть заблокированным параллелизмом, а являются параллелизмом без ожидания.Этот метод оптимизации блокировки значительно повысит производительность системы в определенных сценариях.
Примером расширения разделения блокировок в java является LinkedBlockingQueue:
Он полностью использует идею разделения горячих точек, беря данные из головы (чтение), добавляя данные (запись) в хвост, а данные для операций чтения и записи находятся в разных частях, поэтому ими можно управлять. в то же время повышая уровень параллелизма Высокий, если только в очереди или связанном списке нет только одного элемента данных. Это дальнейшее расширение идеи разделения чтения-записи: блокировки можно разделять до тех пор, пока операции не влияют друг на друга.
блокировка огрубления
Обычно, чтобы обеспечить эффективный параллелизм между несколькими потоками, каждый поток должен удерживать блокировку в течение как можно более короткого времени, но в некоторых случаях программа запрашивает, синхронизирует и освобождает определенное количество системных ресурсов, потому что упор на блокировки, синхронизацию и само освобождение принесет потери производительности, поэтому высокочастотные запросы на блокировку не способствуют оптимизации производительности системы, хотя время одной операции синхронизации может быть очень долгим. Огрубление блокировки говорит нам, что все имеет степень.В некоторых случаях мы хотим объединить много запросов на блокировку в один запрос, чтобы уменьшить потерю производительности, вызванную большим количеством запросов на блокировку, синхронизацией и освобождением за короткий период времени. .
Крайний случай следующий:
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
В приведенном выше коде есть две части кода, которые необходимо синхронизировать, но между двумя частями кода, которые необходимо синхронизировать, необходимо выполнить некоторую другую работу, и эта работа займет совсем немного времени, поэтому мы можем поместить эти work Код помещается в блокировку, и два блока кода синхронизации объединяются в один, чтобы снизить потребление производительности системы, вызванное несколькими запросами на блокировку, синхронизацией и освобождением.Объединенный код выглядит следующим образом:
public void doSomethingMethod(){
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
Примечание: Это делается исходя из того, что код, не требующий синхронизации в середине, может быть выполнен быстро.Если код, не требующий синхронизации, занимает много времени, это приведет к тому, что выполнение синхронизированного блока займет много времени. делать это тоже неразумно.
Другой крайний случай, требующий укрупнения блокировки:
for(int i=0;i<size;i++){
synchronized(lock){
}
}
Приведенный выше код будет запрашивать, синхронизировать и освобождать блокировку каждый раз, когда он зациклится.Похоже, что нет никаких проблем, и некоторые оптимизации будут сделаны для этого типа запроса блокировки кода внутри jdk, но код блокировки лучше написать вне тела цикла. , чтобы запрос на блокировку мог удовлетворить наши требования, если только нет особой необходимости: цикл занимает много времени, но другие потоки не могут ждать и дают им возможность выполниться.
Код после огрубления блокировки выглядит следующим образом:
synchronized(lock){
for(int i=0;i<size;i++){
}
}