Глубокое понимание основного механизма параллелизма в Java

Java
Глубокое понимание основного механизма параллелизма в Java

📦 Эта статья и пример исходного кода были заархивированы вjavacore

1. Введение в JUC

Javajava.util.concurrentПакет (сокращенно J.U.C) предоставляет большое количество классов инструментов параллелизма, которые являются основным проявлением возможностей параллелизма Java (обратите внимание, что не все, некоторые возможности параллелизма поддерживаются в других пакетах). По функциям его можно условно разделить на:

  • Атомарные классы, такие как:AtomicInteger,AtomicIntegerArray,AtomicReference,AtomicStampedReferenceЖдать.
  • Блокировка - например:ReentrantLock,ReentrantReadWriteLockЖдать.
  • Параллельные контейнеры, такие как:ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySetЖдать.
  • Блокирующие очереди - например:ArrayBlockingQueue,LinkedBlockingQueueЖдать.
  • Неблокирующая очередь - например:ConcurrentLinkedQueue,LinkedTransferQueueЖдать.
  • ExecutorFramework (пул потоков) - например:ThreadPoolExecutor,ExecutorsЖдать.

Мое личное понимание состоит в том, что инфраструктура параллелизма Java может быть разделена на следующие уровни.

Несложно видеть параллельные рамки Java. Класс инструмента в пакете j.u.cc основан наsynchronized,volatile,CAS,ThreadLocalСоздается такой параллельный основной механизм. Итак, чтобы понять характеристики класса инструментов J.U.C, почему у вас есть такая характеристика, вы должны сначала понять эти основные механизмы.

Два, синхронизировано

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

synchronizedГарантируется, что одновременно только один поток может выполнить метод или блок кода..

если не нуженLock,ReadWriteLockПредоставляемые расширенные функции синхронизации должны иметь приоритетsynchronized, по следующим причинам:

  • После Java 1.6,synchronizedБыло сделано много оптимизаций, и его производительность сравнивалась сLock,ReadWriteLockв принципе плоский. С точки зрения тенденций, Java будет продолжать оптимизироваться в будущем.synchronized, вместоReentrantLock.
  • ReentrantLockЭто API Oracle JDK, который может не поддерживаться в других версиях JDK; а такжеsynchronizedЭто встроенная функция JVM, и поддерживаются все версии JDK.

Использование синхронизированного

synchronizedЕсть 3 способа подачи заявки:

  • Синхронизированные методы экземпляра- метод обычного синхронизации, замок является текущим экземпляром объекта
  • Синхронизированный статический метод- Для статических синхронизированных методов блокировка относится к текущему классу.Classобъект
  • Блок синхронизированного кода- Для блоков синхронизированных методов блокировкаsynchonizedОбъекты, указанные в скобках

инструкция:

похожийVector,HashtableЭтот тип класса синхронизации должен использоватьsynchonizedУкрасьте его важные методы, чтобы обеспечить его потокобезопасность.

На самом деле такие синхронизированные контейнеры не являются абсолютно потокобезопасными: при обходе итератора и удалении элементов по условиям могут возникать потоконебезопасные условия. Кроме того, Java 1.6 нацеленаsynchonizedДо оптимизации его производительность была невысокой из-за блокировки.

Подводя итог, этот вид контейнера синхронизации постепенно стал ненужным в современных программах Java.

Синхронизированные методы экземпляра

❌ Пример ошибки - несинхронизированный пример

public class NoSynchronizedDemo implements Runnable {

    public static final int MAX = 100000;

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        NoSynchronizedDemo instance = new NoSynchronizedDemo();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

    @Override
    public void run() {
        for (int i = 0; i < MAX; i++) {
            increase();
        }
    }

    public void increase() {
        count++;
    }

}
// 输出结果: 小于 200000 的随机数字

Синхронизация метода экземпляра Java синхронизируется с объектом, которому принадлежит метод. Таким образом, синхронизация метода каждого экземпляра синхронизируется с другим объектом, то есть с экземпляром, которому принадлежит метод. Только один поток может выполняться в синхронизированном блоке метода экземпляра. Если существует несколько экземпляров, то один поток может одновременно выполнять операции в одном синхронизированном блоке экземпляра. Один экземпляр один поток.

public class SynchronizedDemo implements Runnable {

    private static final int MAX = 100000;

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo instance = new SynchronizedDemo();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

    @Override
    public void run() {
        for (int i = 0; i < MAX; i++) {
            increase();
        }
    }

    /**
     * synchronized 修饰普通方法
     */
    public synchronized void increase() {
        count++;
    }

}

Синхронизированный статический метод

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

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

public class SynchronizedDemo2 implements Runnable {

    private static final int MAX = 100000;

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo2 instance = new SynchronizedDemo2();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

    @Override
    public void run() {
        for (int i = 0; i < MAX; i++) {
            increase();
        }
    }

    /**
     * synchronized 修饰静态方法
     */
    public synchronized static void increase() {
        count++;
    }

}

Блок синхронизированного кода

Иногда вам не нужно синхронизировать весь метод, но часть метода синхронизации. Java можно синхронизировать со стороны метода.

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

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

public class SynchronizedDemo3 implements Runnable {

    private static final int MAX = 100000;

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo3 instance = new SynchronizedDemo3();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

    @Override
    public void run() {
        for (int i = 0; i < MAX; i++) {
            increase();
        }
    }

    /**
     * synchronized 修饰代码块
     */
    public static void increase() {
        synchronized (SynchronizedDemo3.class) {
            count++;
        }
    }

}

Принцип синхронного

synchronizedПосле компиляции он будет сформирован до и после синхронизированного блока.monitorenterиmonitorexitЭти два поступления инструкции, которые требуют, требуют параметра ссылочного типа, чтобы указать объект, который будет заблокирован и разблокирован. еслиsynchronizedЕсли параметр объекта указан явно, то это ссылка на этот объект, если не указан, то согласноsynchronizedНезависимо от того, является ли модификация методом экземпляра или статическим методом, соответствующий экземпляр объекта или объект класса используется в качестве объекта блокировки.

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

synchronizedСинхронизированные блоки являются взаимоисключающими, что означает, что другие потоки, пытающиеся войти, будут заблокированы до тех пор, пока вошедший поток не завершится.

замок механизм

Замок имеет следующие две характеристики:

  • мутность: то есть только одному потоку разрешено одновременно удерживать блокировку объекта, и эта функция используется для реализации механизма координации в многопоточности, так что только один поток может получить доступ к блоку кода (составной операции), который требуется синхронизироваться одновременно. Взаимное исключение также часто называют атомарностью операций.
  • видимость: Необходимо обеспечить, чтобы допустить замок, модификации, сделанные в общую переменную, видно для другой резьбы, которая впоследствии приобретает блокировку (т. Е. Значение последней общей переменной должно быть получено, когда замок приобретен), в противном случае другой нить Может быть продолжение операции на копии локального кэша, вызывающего несоответствие.

тип замка

  • блокировка объекта- В Java каждый объект будет иметьmonitorObject, этот объект на самом деле является блокировкой объекта Java, который обычно называют «встроенной блокировкой» или «блокировкой объекта». Может быть несколько объектов класса, поэтому каждый объект имеет свою собственную блокировку объекта, не мешая друг другу.
  • блокировка класса— В Java также есть блокировка для каждого класса, которую можно назвать «блокировкой класса», и блокировка класса фактически реализуется через блокировку объекта, то есть классовую блокировку объекта класса. Для каждого класса существует только один объект класса, поэтому для каждого класса существует только одна блокировка класса.

СИНХРОНИЗИРОВАННЫЙ

После Java 1.6,synchronizedБыло сделано много оптимизаций, и его производительность сравнивалась сLock,ReadWriteLockв принципе плоский.

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

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

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

Адаптивные спин-блокировки появились в Java 1.6. Адаптивный означает, что количество вращений больше не фиксировано, а определяется количеством предыдущих вращений на одном и том же замке и состоянием владельца замка.

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

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

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

Для некоторого кода, который не кажется заблокированным, на самом деле неявно добавляется множество блокировок. Например, следующий код объединения строк неявно добавляет блокировку:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

String — это неизменяемый класс, и компилятор автоматически оптимизирует конкатенацию String. До Java 1.5 это переводилось в последовательную операцию append() над объектами StringBuffer:

public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

В каждом методе Append() есть блок синхронизации. Виртуальная машина наблюдает за переменной SB, вскоре она обнаружит, что ее динамическая область действия ограничена методом created(). То есть все ссылки SB никогда не убегут в метод contATString(), другие потоки не смогут получить к нему доступ, поэтому их можно исключить.

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

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

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

Легкий замок

В Java 1.6 появились предвзятые блокировки и облегченные блокировки, позволяющие блокировкам иметь четыре состояния:

  • Незаблокированное состояние (разблокировано)
  • Состояние предвзятой блокировки (biasble)
  • Легкое заблокированное состояние
  • Тяжелое состояние блокировки (надутое)

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

При попытке получить объект блокировки, если объект блокировки помечен как 0 01, это указывает на то, что блокировка объекта блокировки находится в разблокированном (разблокированном) состоянии. В этот момент виртуальная машина создает запись блокировки в стеке виртуальной машины текущего потока, а затем использует операцию CAS для обновления слова метки объекта до указателя записи блокировки. Если операция CAS выполнена успешно, поток получает блокировку объекта, а флаг блокировки слова метки объекта становится равным 00, указывая на то, что объект находится в состоянии облегченной блокировки.

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

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

Три, волатильные

Точка изменчивости

Волатильный - это легкий синхронизированный, который гарантирует «видимость» общих переменных в многопроцессорном развитии.

Видимость означает, что когда один поток изменяет общую переменную, другой поток может прочитать измененное значение.

Как только общая переменная (переменная-член класса, статическая переменная-член класса) изменяется с помощью volatile, она имеет два уровня семантики:

  1. Это обеспечивает видимость, когда разные потоки работают с этой переменной, то есть поток изменяет значение переменной, и новое значение сразу видно другим потокам.
  2. Переупорядочивание инструкций отключено.

Если поле объявлено как volatile, модель памяти потоков Java гарантирует, что все потоки будут видеть одно и то же значение переменной.

Использование изменчивого

еслиvolatileМодификаторы переменных при правильном использовании меньшеsynchronizedИспользование и реализация более низкой стоимости, потому что это не вызывает переключение контекста потока и планирование. но,volatileнезаменимыйsynchronized,так какvolatileАтомарность операции не гарантируется.

как правило,использоватьvolatileВы должны соответствовать следующим двум условиям:

  • Запись в переменную не зависит от текущего значения
  • Переменная не содержится в инварианте с другими переменными

Пример: количество тегов состояния

volatile boolean flag = false;

while(!flag) {
    doSomething();
}

public void setFlag() {
    flag = true;
}

Пример: двойная блокировка, реализующая потокобезопасный одноэлементный класс

class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

Принцип энергозависимости

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

lockПрефиксная инструкция фактически эквивалентна барьеру памяти.(также называемый барьером памяти), барьер памяти будет выполнять 3 функции:

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

4. КАС

Основные моменты CAS

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

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

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

Зачем нужна оптимистическая блокировка?Разработка набора аппаратных инструкцийпродолжать?因为需要操作和冲突检测这两个步骤具备原子性。而这点是由硬件来完成,如果再使用互斥同步来保证就失去意义了。硬件支持的原子性操作最典型的是:CAS。

CAS (Сравнить и поменять местами)Закон, буквальноСравните и обменяйте. CAS имеет 3 операнда, а именно: значение памяти V, старое ожидаемое значение A и новое значение B, которое необходимо изменить. Измените значение памяти V на B тогда и только тогда, когда ожидаемое значение A и значение памяти V совпадают, в противном случае ничего не делайте.

Принципы КАС

Как Java реализует CAS?

Java в основном используетUnsafeОперации CAS, предоставляемые этим классом.

UnsafeCAS зависит от реализации JVM для разных операционных систем.Atomic::cmpxchgинструкция.

Atomic::cmpxchgРеализация использует операции CAS в сборке и использует предоставляемые ЦПlockСигналы гарантируют свою атомарность.

Применение КАС

Атомный класс

Атомарные классы — наиболее типичное применение CAS в Java.

Давайте сначала посмотрим на общий фрагмент кода.

if(a==b) {
    a++;
}

еслиa++Что, если значение a будет изменено перед выполнением? Могу ли я получить ожидаемое значение? Причина этой проблемы в том, что в параллельной среде приведенные выше фрагменты кода не являются атомарными операциями и могут быть изменены другими потоками в любое время.

Самый классический способ решить эту проблему — применить атомарные классы.incrementAndGetметод.

public class AtomicIntegerDemo {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        final AtomicInteger count = new AtomicInteger(0);
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    count.incrementAndGet();
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(3, TimeUnit.SECONDS);
        System.out.println("Final Count is : " + count.get());
    }

}

Поставляется в пакете J.U.C.AtomicBoolean,AtomicInteger,AtomicLongсоответственно дляBoolean,Integer,LongДля выполнения атомарных операций операции в целом аналогичны вышеприведенным примерам и подробно описываться не будут.

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

Используя атомарные классы (по сути, CAS), можно реализовать спин-блокировки.

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

Пример: пример без поддержки потоков

public class AtomicReferenceDemo {

    private static int ticket = 10;

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new MyThread());
        }
        executorService.shutdown();
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            while (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票");
                ticket--;
            }
        }

    }

}

Выходной результат:

pool-1-thread-2 卖出了第 10 张票
pool-1-thread-1 卖出了第 10 张票
pool-1-thread-3 卖出了第 10 张票
pool-1-thread-1 卖出了第 8 张票
pool-1-thread-2 卖出了第 9 张票
pool-1-thread-1 卖出了第 6 张票
pool-1-thread-3 卖出了第 7 张票
pool-1-thread-1 卖出了第 4 张票
pool-1-thread-2 卖出了第 5 张票
pool-1-thread-1 卖出了第 2 张票
pool-1-thread-3 卖出了第 3 张票
pool-1-thread-2 卖出了第 1 张票

Очевидно, что имело место дублирование продажи билетов.

Пример: использование спин-блокировок для безопасности потоков

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

public class AtomicReferenceDemo2 {

    private static int ticket = 10;

    public static void main(String[] args) {
        threadSafeDemo();
    }

    private static void threadSafeDemo() {
        SpinLock lock = new SpinLock();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new MyThread(lock));
        }
        executorService.shutdown();
    }

    static class SpinLock {

        private AtomicReference<Thread> atomicReference = new AtomicReference<>();

        public void lock() {
            Thread current = Thread.currentThread();
            while (!atomicReference.compareAndSet(null, current)) {}
        }

        public void unlock() {
            Thread current = Thread.currentThread();
            atomicReference.compareAndSet(current, null);
        }

    }

    static class MyThread implements Runnable {

        private SpinLock lock;

        public MyThread(SpinLock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            while (ticket > 0) {
                lock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票");
                    ticket--;
                }
                lock.unlock();
            }
        }

    }

}

Выходной результат:

pool-1-thread-2 卖出了第 10 张票
pool-1-thread-1 卖出了第 9 张票
pool-1-thread-3 卖出了第 8 张票
pool-1-thread-2 卖出了第 7 张票
pool-1-thread-3 卖出了第 6 张票
pool-1-thread-1 卖出了第 5 张票
pool-1-thread-2 卖出了第 4 张票
pool-1-thread-1 卖出了第 3 张票
pool-1-thread-3 卖出了第 2 张票
pool-1-thread-1 卖出了第 1 张票

Проблемы с CAS

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

Однако есть некоторые проблемы с CAS.

АВА-проблема

Если переменная была сначала прочитана со значением A, ее значение было изменено на B, а затем изменено обратно на A, операция CAS ошибочно приняла бы это за тот факт, что оно никогда не изменялось.

J.U.C Пакет предоставляет атомный эталонный класс с тегомAtomicStampedReferenceЧтобы решить эту проблему, он может гарантировать правильность CAS, контролируя версию значения переменной. В большинстве случаев проблема ABA не повлияет на корректность параллелизма программ.Если вам нужно решить проблему ABA, может быть эффективнее использовать традиционную синхронизацию взаимного исключения, чем атомарный класс.

Длительное время цикла и высокие накладные расходы

Spin CAS (продолжайте попытки до тех пор, пока не добьетесь успеха) приводит к очень большим накладным расходам на выполнение ЦП, если он не работает в течение длительного времени.

Если JVM может поддерживатьpauseЭффективность инструкции будет улучшена в определенной степени.pauseДирективы служат двумя целями:

  • Он может задерживать выполнение конвейера (de-pipeline), чтобы ЦП не потреблял слишком много ресурсов выполнения, время задержки зависит от конкретной версии реализации, а на некоторых процессорах время задержки равно нулю.
  • Это может избежать очистки конвейера ЦП, вызванной нарушением порядка памяти при выходе из цикла, тем самым повышая эффективность выполнения ЦП.

Он потребляет больше ресурсов ЦП и будет выполнять бесполезную работу, даже если она бесполезна.

Только одна общая переменная гарантированно будет атомарной.

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

Или есть хитрый способ объединить несколько общих переменных в одну общую переменную для работы. Например, есть две общие переменныеi = 2, j = aСлияниеij=2a, а затем используйте CAS для работыij. Начиная с Java 1.5 JDK предоставляетAtomicReferenceКласс для обеспечения атомарности между ссылочными объектами, вы можете поместить несколько переменных в один объект для выполнения операций CAS.

5. Локальный поток

ThreadLocalэто служебный класс для хранения локальных копий потока.

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

на ЯвеНет схемы синхронизацииимеют:

  • реентерабельный код- Также называется чистым кодом. Если метод, тоРезультаты возврата предсказуемыОднако до тех пор, пока можно ввести одни и те же данные, он может вернуть тот же результат, который должен удовлетворять повторное использование, конечно, поток безопасен.
  • Локальное хранилище потоков- использоватьThreadLocalЛокальная копия создается в каждом потоке для общих переменных., эта копия может быть доступна только текущему потоку и не может быть доступна другим потокам, поэтому она естественно потокобезопасна.

Использование ThreadLocal

ThreadLocalМетоды:

public class ThreadLocal<T> {
    public T get() {}
    public void set(T value) {}
    public void remove() {}
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
}

инструкция:

  • get- для полученияThreadLocalКопия переменной, сохраненная в текущем потоке.
  • set- Используется для установки копии переменной в текущем потоке.
  • removeinitialValueПовторная инициализация метода, если его значение не установлено текущим потоком в промежуточном потоке. Это может привести к множественным вызовам в текущем потоке.initialValueметод.
  • initialValue- установить значение по умолчанию для ThreadLocalgetИсходное значение, необходимо переписатьinitialValueметод .

ThreadLocalЧасто используется для предотвращения совместного использования изменяемых переменных Singleton или глобальных переменных. Типичные сценарии применения: управление подключениями и сеансами базы данных.

Пример - соединение с базой данных

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    @Override
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}

Пример — управление сеансом

private static final ThreadLocal<Session> sessionHolder = new ThreadLocal<>();

public static Session getSession() {
    Session session = (Session) sessionHolder.get();
    try {
        if (session == null) {
            session = createSession();
            sessionHolder.set(session);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return session;
}

Пример - полный пример использования

public class ThreadLocalDemo {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(new MyThread());
        }
        executorService.shutdown();
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            int count = threadLocal.get();
            for (int i = 0; i < 10; i++) {
                try {
                    count++;
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            threadLocal.set(count);
            threadLocal.remove();
            System.out.println(Thread.currentThread().getName() + " : " + count);
        }

    }

}

Количество всех выходов = 10

Принцип ThreadLocal

структура хранения

Threadкласс поддерживаетThreadLocal.ThreadLocalMapчлены типаthreadLocals. Этот член используется для хранения копии исключительной для потока переменной.

ThreadLocalMapдаThreadLocalвнутренний класс, который поддерживаетEntryМножество,EntryДля хранения пар клавиши, которые являются ключомThreadLocalObjects, Value — переданный объект (копия переменной).

Как разрешить хеш-конфликты

ThreadLocalMapХотя похожиMapструктурировать структуру данных, но не реализуетMapинтерфейс. это не поддерживаетMapв интерфейсеnextметод, а значитThreadLocalMapСпособ разрешения хеш-конфликтов встол на молнииСпособ.

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

проблема с утечкой памяти

ThreadLocalmapEntryнаследоватьWeakReference, поэтому его ключ (ThreadLocalobject) — слабая ссылка, а value (копия переменной) — сильная ссылка.

  • еслиThreadLocalобъект не имеет внешней сильной ссылки для ссылки на него, тоThreadLocalОбъект будет собран на следующей сборке мусора.
  • В настоящее время,EntryKEY в середине был переработан, но Value не будет переработан сборщиком мусора, потому что это сильная ссылка. Если вы создадитеThreadLocalПоток продолжает работать, тогда значение не будет постоянно перерабатываться, что приведет к утечке памяти.

Итак, как избежать утечек памяти? Метод:использоватьThreadLocalизsetПосле метода отображаемый вызовremoveметод.

ThreadLocal<String> threadLocal = new ThreadLocal();
try {
    threadLocal.set("xxx");
    // ...
} finally {
    threadLocal.remove();
}

использованная литература