Предложения по улучшению производительности блокировки

задняя часть

Недавно я прочитал книгу "Java High Concurrency Programming" и резюмировал несколько статей, которые также являются содержанием книги.

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

Например, если 100 человек приходят в банк для ведения бизнеса, им нужно заполнить 100 форм, но есть только одна ручка.Очевидно, что чем меньше времени каждый человек использует ручку, тем выше эффективность: посмотрите на код :

/*
othercode1和othercode2很耗时间,里面没有涉及资源同步,只有mutexMethod方法要对资源同步,
所有优化代码让持有锁时间尽量短
*/

public synchronized void syncMethod(){
        othercode1();
        mutexMethod();
        othercode2();
}

public  void syncMethod(){
        othercode1();
        synchronized(this){
            mutexMethod();
        }
        othercode2();
}
//在jdk源码里面也很容易找到这种手段,比如处理正则表达式的Pattern类
public Matcher matcher(CharSequence input) {
        if (!compiled) {
            synchronized(this) {
                if (!compiled)
                    compile();
            }
        }
        Matcher m = new Matcher(this, input);
        return m;
}
//只有在表达式未编译的时候进行局部加锁,这种方法大大提高了matcher的执行效率和可靠性

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

2. Уменьшить прочность замка

Реализация concurrentHashMap, ее внутренняя часть разделена на несколько известных хэш-карт, называемых сегментами (SEGMENT), по умолчанию 16 сегментов.

Уменьшение детализации блокировки приведет к новой проблеме. Когда необходимо получить глобальную блокировку, она будет потреблять больше ресурсов, что не так хорошо, как метод size() в concurrenthashMap. Видно, что при вычислении размера, необходимо вычислить блокировки всех допустимых сегментов

public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
}

final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

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

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

3. Разделение чтения и записи для замены монопольных блокировок

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

package com.high.concurrency;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author: zhangzeli
 * @date 8:43 2018/4/10
 * <P></P>
 */
public class ReadWriteLockDemo {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock =readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException{
        try {
            lock.lock();
            Thread.sleep(1000);
            return value;
        }finally {
            lock.unlock();
        }
    }

    public void handleWrite(Lock lock,int index) throws InterruptedException{
        try {
            lock.lock();
            Thread.sleep(1000);
            value =index;
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnale = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.handleRead(lock);
                    //demo.handleRead(readLock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Runnable write = new Runnable() {
            @Override
            public void run() {
                try {
                    //demo.handleWrite(writeLock,new Random().nextInt());
                    demo.handleWrite(lock,new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for(int i=0;i<18;i++){
            new Thread(readRunnale).start();
        }
        for(int i=18;i<20;i++){
            new Thread(write).start();
        }
    }
}

Разница очевидна.

3. Блокировка разделения

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

Если используется эксклюзивная блокировка, то take и put не могут завершить настоящий параллелизм, поэтому вместо этого jdk не использовал этот метод. Вместо этого две разные блокировки разделяют операции put и take. См. исходный код ниже.

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();//take函数需要持有takeLock

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();//put函数需要持有putLock

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();
public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly(); //不能有两个线程同时取数据
        try {
            while (count.get() == 0) { //如果当前没有可用数据,一直等待
                notEmpty.await();      //等待,put操作的通知
            }
            x = dequeue();         //取得第一个数据
            c = count.getAndDecrement();//数量减一,原子操作因为回合put同时访问count.注意变量c是count减一
            if (c > 1)
                notEmpty.signal();  //通知其他take操作
        } finally {
            takeLock.unlock(); //释放锁
        }
        if (c == capacity)
            signalNotFull(); //通知put,已有空余空间
        return x;
    }


public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();//不能有两个线程同时进行put
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) { //如果队列已满
                notFull.await();   //等待
            }
            enqueue(node);  //插入数据
            c = count.getAndIncrement(); //更新总数,变量c是count加1前的值
            if (c + 1 < capacity)
                notFull.signal();   //有足够的空间,通知其他线程
        } finally {
            putLock.unlock();  //释放锁
        }
        if (c == 0)
            signalNotEmpty();  //插入成功后,通知take操作
    }

4. Огрубление блокировки

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

for (int i=0;i<20;i++){
     synchronized (lock){
                
     }
}

//优化后
synchronized (lock){
    for (int i=0;i<20;i++){
     
                
     }
}

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

5. Усилия виртуальной машины Java по оптимизации блокировок

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

Biased lock, проще говоря, в заголовке объекта блокировки есть поле ThreaddId, если это поле пустое, то при первом получении блокировки в поле ThreadId блокировки будет записан его собственный ThreadId, и поле ThreadId блокировки будет записано.Смещена ли головка блокировки в сторону положения блокировки 1. Таким образом, при получении блокировки в следующий раз напрямую проверьте, соответствует ли ThreadId собственному идентификатору потока. является непротиворечивым, считается, что текущий поток получил блокировку, поэтому нет необходимости получать блокировку снова.Он прошел стадию блокировки облегченных блокировок и тяжелых блокировок. Повышенная эффективность.

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

Параметр -XX:+UseBiasedLocking

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。它通过消除资源无竞争情况下的同步原语,
进一步提高了程序的运行性能。偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,
该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。如果在运行过程中,遇到了其他线程抢占锁,
则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用)
因此 流程是这样的 偏向锁->轻量级锁->重量级锁

5.2 Легкий замок

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

Затем поток пытается использовать CAS для замены Mark Word в заголовке объекта указателем на запись блокировки. Если это удается, текущий поток получает блокировку, если это не удается, это означает, что другие потоки конкурируют за блокировку, и текущий поток пытается использовать вращение, чтобы получить блокировку.

Разблокировка облегченной блокировки: при облегченной разблокировке используется атомарная операция CAS для замены слова Displaced Mark Word обратно в заголовок объекта.В случае успеха конкуренция не возникает.

Если это не удается, это означает, что текущая блокировка конкурирует, и блокировка превратится в тяжеловесную блокировку.

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

На следующем рисунке показана блок-схема двух потоков, одновременно конкурирующих за блокировки, что приводит к раздуванию блокировок.

Замки не будут понижены

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

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,
就不会再恢复到轻量级锁状态。
当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,
被唤醒的线程就会进行新一轮的夺锁之争。

5.3 Устранение блокировки

Устранение блокировок — это когда виртуальная машина Java компилируется в JIT. Путем сканирования работающего контекста удаляются блокировки, которые вряд ли будут иметь конкуренцию за общие ресурсы. Благодаря устранению блокировок вы можете сэкономить бессмысленное время запроса блокировки.

public class TestLockEliminate {
    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

    public static void main(String[] args) {
        long tsStart = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            getString("TestLockEliminate ", "Suffix");
        }
        System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
    }
}

Номер StringBuffer в методе getString() — это локальная переменная внутри функции, которая действует внутри метода, и избежать этого метода невозможно, поэтому к нему не могут обращаться несколько потоков одновременно, и не является конкуренцией за ресурсы, но StringBuffer Операция добавления должна выполнять синхронную операцию:

@Override
public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
}

Анализ выхода и снятие блокировок можно включить с помощью параметров -XX:+DoEscapeAnalysis и -XX:+EliminateLocks соответственно (удаление блокировок должно быть в режиме -server). Запустите указанную выше программу со следующими параметрами:

这里写图片描述

Запустите программу с помощью следующей команды: -XX:+DoEscapeAnalysis -XX:+EliminateLocks

这里写图片描述

Сравнение преимуществ и недостатков замков

Замок

преимущество

недостаток

Применимая сцена

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

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

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

Подходит для сценариев, когда к синхронизированному блоку обращается только один поток.

Легкий замок

Конкурирующие потоки не будут блокироваться, что повышает скорость отклика программы.

Использование spin будет потреблять ЦП, если потоки, которые никогда не получают блокировку.

Соблюдайте время отклика.

Синхронизированные блоки выполняются очень быстро.

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

Конкуренция потоков не использует вращение и не потребляет ресурсы ЦП.

Поток заблокирован, и время ответа медленное.

Следите за пропускной способностью.

Синхронизированные блоки выполняются дольше.