Чувак, улучшай свои навыки параллелизма, начни с оптимизации блокировок!

Java
Чувак, улучшай свои навыки параллелизма, начни с оптимизации блокировок!

Привет всем, я Сяо Цай, Сяо Цай, который хочет быть Цай Буцаем в интернет-индустрии. Она может быть мягкой или жесткой, как она мягкая, а белая проституция жесткая!Черт~ Не забудьте поставить мне тройку после прочтения!

Эта статья в основном знакомитJava并行的入门

При необходимости вы можете обратиться к

Если это поможет, не забудьтеподобно

Блокировки являются одним из наиболее часто используемых методов синхронизации. В среде с высокой степенью параллелизма интенсивная конкуренция за блокировку снизит производительность программы.

Для однозадачных или однопоточных приложений основное потребление ресурсов тратится на саму задачу,Ему не нужно ни поддерживать согласованное состояние между параллельными структурами данных, ни тратить время на переключение и планирование потоков.. Для многопоточных приложений, помимо обработки функциональных требований, системаТакже необходимо дополнительно поддерживать уникальную информацию многопоточной среды, такую ​​как метаданные самого потока, планирование потока, переключение контекста потока и т. д.. Причина, по которой параллельные вычисления могут повысить производительность системы, заключается не в том, что они «делают меньше работы», а в том, что параллельные вычисления могут более разумно планировать задачи и полностью использовать каждый ресурс ЦП.

Как улучшить работу блокировки

Сокращение времени удержания блокировки

Для приложений, которые используют блокировки для управления параллелизмом, в процессе конкуренции блокировок время удержания одного потока на блокировке напрямую связано с производительностью системы. Чем дольше удерживается замок, тем интенсивнее конкуренция за замок.

简单来讲就是:Для заполнения информационной формы требуется 100 человек, но есть только одна ручка.Если каждый человек не знает, как заполнить информационную форму, то каждый человек будет держать ручку в течение длительного времени, и общее время будет дольше.

следовательноУменьшите время удержания блокировки, чтобы уменьшить взаимное исключение между потоками.. Например следующий код:

public synchronized void synMethod(){
    method1();
    mainMethod();
    method2();
}

В приведенном выше коде толькоmainMethod()метод должен выполнять синхронное управление, иmethod1()а такжеmethod2()Нет необходимости делать контроль синхронизации, тогда вышеприведенный абзац синхронизирует весь метод в случае высокого параллелизма.method1()а такжеmethod2()Если два метода займут много времени, время выполнения всей программы увеличится. Таким образом, мы можем выбрать оптимизацию следующим образом:

public void synMethod(){
    method1();
    synchronized(this){
     mainMethod();   
    }
    method2();
}

Преимущество этого в том, что толькоmainMethod()Метод управляется синхронно, и блокировка занимает относительно короткое время, поэтому он может иметь высокую степень параллелизма.Меньшее время удержания блокировки помогает снизить вероятность конфликтов блокировок, тем самым улучшая возможности параллелизма в системе..

Уменьшить детализацию блокировки

Уменьшить детализацию блокировкиЭто также эффективное средство ослабления конкуренции за многопоточные блокировки. Типичным примером использования этой техники являетсяConcurrentHashMapреализация класса. правильноConcurrentHashMapТе, кто что-то знают, должны знать, что традиционныеHashTableЭто потокобезопасно, потому что он блокирует весь метод. а такжеConcurrentHashMapПроизводительность относительно высока, потому что он внутренне подразделяется на несколько небольшихHashMap, называемый отрезком (SEGMENT). По умолчаниюConcurrentHashMapКлассы можно разделить на16В конце концов, производительность эквивалентна улучшению16раз.

существуетConcurrentHashMapдобавление данных вHashMapзамок, но сначала согласноhashcodeУзнайте, в каком сегменте он должен храниться, затем заблокируйте сегмент и завершитеput()работать. когда несколько потоковput()При работе, если замок не является одним и тем же сегментом, можно добиться параллельной работы.

но,Уменьшение детализации блокировки приводит к новой проблеме: Когда системе необходимо получить глобальную блокировку, она потребляет больше ресурсов. Например: когдаConcurrentHashMapперечислитьsize()требуется блокировка одного или всех подразделов. Хотя на самом деле,size()Метод сначала будет использовать метод без блокировки для суммирования и попытается использовать этот метод в случае сбоя, но в случае высокого параллелизмаConcurrentHashMapпроизводительность все же слабее синхроннойHashMap.

Уменьшение степени детализации блокировок означает уменьшение объема заблокированных объектов, тем самым снижая вероятность конфликтов блокировок и тем самым улучшая возможности параллелизма в системе.

Замена эксклюзивных блокировок блокировками чтения-записи

Разделительные блокировки чтения-записи могут эффективно снизить конкуренцию замков и повысить производительность системы. Например, три потока A1, A2 и A3 выполняют операции записи, а три потока B1, B2 и B3 выполняют операции чтения.Если используются реентерабельные блокировки или внутренние блокировки, то все операции чтения, между операциями чтения и записи и между операциями записи Все являются последовательными операциями. ноПоскольку операция чтения не нарушает целостности данных, такое ожидание нецелесообразно.

Таким образом, вы можете использовать разделенную блокировку для чтения и записи.ReadWriteLockдля улучшения производительности системы. Пример использования следующий:

блокировка огрубления

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

Пример ошибки:

public void synMethod(){
    synchronized(this){
        method1();
    }
    synchronized(this){
        method2();
    }
}

Оптимизировано:

public void synMethod(){
    synchronized(this){
        method1();
        method2();
    }
}

Особенно в петле обратите внимание на огрубение замка

Пример ошибки:

public void synMethod(){
    for (int i = 1; i < n; i++) {
     synchronized(lock){
            //do sth ...
        }            
 }
}

Оптимизировано:

synchronized(lock){
    for (int i = 1; i < n; i++) {
     //do sth ...         
 }
}

Оптимизация блокировки с помощью JVM

Блокировка смещения

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

Однако в случае жесткой конкуренции замков эффект нехороший, так как в случае жесткой конкуренции наиболее вероятная ситуация заключается в том, что каждый раз запрашиваются разные потоки, поэтому режим смещения не сработает, поэтому лучше не включать замок смещения. Может передавать параметры JVM-XX:+UseBiasedLockingВключите блокировку смещения.

Легкий замок

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

блокировка спина

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

тяжелый замок

Если блокировка не может быть получена после вращения, поток будет приостановлен на уровне операционной системы и обновлен дотяжелый замок **

снятие блокировки

Во время JIT-компиляции виртуальная машина Java сканирует работающий контекст, чтобы удалить блокировки, которые вряд ли будут иметь конкуренцию за общие ресурсы.снятие блокировки, что может сэкономить время на бессмысленный запрос блокировки.

public String[] createArrays() {
    Vector<Integer> vector = new Vector<>();
    for (int i = 1; i < 100; i++) {
        vector.add(i);
    }
    return vector.toArray(new String[]{});
}

В приведенном выше коде, потому чтоvectorЭта переменная определена вcreateArrays()В этом методе это локальная переменная, которая размещается в стеке потока и относится к данным, закрытым для потока, поэтому конкуренция за ресурсы отсутствует. а такжеVectorВся внутренняя синхронизация блокировки не требуется.Если виртуальная машина обнаружит эту ситуацию, эти бесполезные операции блокировки будут удалены.

Ключевой технологией, задействованной в устранении блокировок, являетсяанализ побега, так называемый анализ побега заключается в наблюдении за тем, выйдет ли переменная из определенной области. В приведенном выше примере переменнаяvectorне сбежалcreateArrays()Ориентация этой функции, поэтому виртуальная машина снимет блокировку операции этой переменной. еслиcreateArrays()Возвращается не массив String, а сам вектор, тогда считается, что переменная vector экранирует текущую функцию и будет доступна другим потокам. Например следующий код:

public Vector<Integer> createList() {
    Vector<Integer> vector = new Vector<>();
    for (int i = 1; i < 100; i++) {
        vector.add(i);
    }
    return vector;
}

ThreadLocal

Помимо управления доступом к ресурсам, мы можем обеспечить потокобезопасность всех объектов за счет увеличения ресурсов.简单来讲就是:Для 100 человек, заполняющих информационную форму, мы можем выделить им 100 ручек, по одной на человека, и скорость заполнения будет значительно увеличена.

Приведенный выше код появится, если нет контроля синхронизацииjava.lang.NumberFormatException: multiple pointsа такжеjava.lang.NumberFormatException: For input string: ""исключение, потому чтоSimpleDateFormatНе является потокобезопасным, если не контролируется блокировками. Но есть ли у нас какие-либо другие методы, кроме блокировки?Ответ - да, то есть использоватьThreadLocal, каждый поток выделяет одинSimpleDateFormat.

Чтобы назначить разные объекты каждому потоку, необходимо убедиться, что ThreadLocal действует только как простой контейнер на уровне приложения.

Принцип реализации ThreadLocal

set()метод:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

Сначала получите текущий объект потока, а затем передайтеgetMap()способ получить нитьThreadLocalMap, и сохраните значение вThreadLocalMapсередина. можно просто поставитьThreadLocalMapпонимается как Карта, гдеkeyтекущий объект потока,valueэто значение, которое нам нужно.

get()метод:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

Сначала получить текущий потокThreadLocalMap, затем установив себя какkeyПолучите актуальные данные внутри

Если мы хотим перерабатывать объекты вовремя, мы должны использоватьThreadLocal.remove()метод удаляет эту переменную, в противном случае, если какой-то большой объект установлен вThreadLocalЕсли он не будет переработан вовремя, это может привести к утечке памяти.

нет замка

Замок делится наоптимистическая блокировкаа такжепессимистический замок, а lock-free — оптимистичная стратегия, использующая比较并交换(CAS,Compare And Swap)технология для выявления конфликтов потоков, и после обнаружения конфликта текущая операция повторяется до тех пор, пока конфликты не исчезнут.

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

CASАлгоритм процесса: содержит три параметраCAS(V,E,N)Vпредставляет переменную для обновления,Eпредставляет ожидаемое значение,Nпредставляет новое значение. только еслиVзначение, равноеEзначение,Vустановлено значениеNценность. Наконец вернитесь к текущемуVистинное значение . Когда несколько потоков используются одновременноCASПри манипулировании переменной только одна выиграет и успешно обновится, остальные потерпят неудачу. Неудавшийся поток не приостанавливается, он просто уведомляется о сбое и может повторить попытку, и, конечно же, отказавший поток может прервать операцию.

Поточно-ориентированное целое число (AtomicInteger)

AtomicIntegerнаходится в параллельном пакете JDKatomic, которое можно рассматривать как целое число, сIntegerРазница в том, что он изменчив и поточно-ориентирован. Любые изменения, подобные его изменению, выполняются с помощью инструкций CAS. НижеAtomicIntegerОбычный способ:

public final int get()          //取得当前值 
public final void set(int newValue)       //设置当前值
public final int getAndSet(int newValue)     //设置新值,并返回旧值
public final boolean compareAndSet(int expect,int u)  //如果当前值为expect,则设置为u
public final int getAndIncrement()       //当前值加1,返回旧值
public final int getAndDecrement()       //当前值减1,返回旧值
public final int getAndAdd(int delta)      //当前值增加delta。返回旧值
public final int incrementAndSet()       //当前值加1,返回新值
public final int decrementAndSet()       //当前值减1,返回新值
public final int addAndGet(int delta)      //当前值增加delta,返回新值

Что касается внутренней реализации,AtomicIntegerОсновное поле сохраняется в:

private volatile int value;

Пример использования:

Видно, что в случае многопоточностиAtomicIntegerявляется потокобезопасным.

Справочник по объектам без блокировки (AtomicReference)

AtomicReferenceа такжеAtomicIntegerочень похоже, разница в том, чтоAtomicIntegerявляется инкапсуляцией целых чисел, иAtomicReferenceЭто ссылка на обычный объект, то есть гарантирует безопасность потока при изменении ссылки на объект.

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

Ссылка на объект с отметкой времени (AtomicStampedReference)

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

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

看完不赞,都是坏蛋
Не понравилось после прочтения, все хреново

Если вы будете усердно работать сегодня, завтра вы сможете сказать на одну вещь меньше, чтобы попросить о помощи!

Я Сяо Цай, человек, который учится у вас. 💋