Эта статья в основном составлена из "Java Concurrent Programming Practice".
1. Потокобезопасность
1.1 Проблемы с живучестью
Когда операция не может быть выполнена, возникают проблемы с живучестью, такие как взаимоблокировка, голодание, активная блокировка и т. д.
1.2 Код, запускающий другой поток
Timer, servlet/JSP,RMI,swing/AWT
1.3 Основные механизмы синхронизации
- Synchronized
- volatile: только обеспечивает видимость
- Явный замок
- атомарная переменная
Лучше всего поместить связанную операцию синхронизации в один и тот же потокобезопасный класс, и класс предоставляет запись, и выполнять меры синхронизации в классе, и клиенту не нужно учитывать проблемы синхронизации при вызове класса, такие как класс в параллельном пакете.
1.4 Проблемы безопасности потоков с изменяемыми переменными состояния
- Не делитесь переменной между потоками
- будетПеременные состоянияИзменить на неизменной переменной
- существуетстатус доступаСинхронизация при использовании переменных
1.5 Условия гонки
Когда правильность расчета зависит отАльтернативный порядок выполнения нескольких потоковВ то время, конкурентные условия будут возникать
Распространенным состоянием гонки является «проверка, затем действие», когда промежуток между проверкой и выполнением блокируется другими потоками и возникает ошибка.
Насколько это возможно, следует использовать потокобезопасные классы для управления состоянием класса, такие как атомарные классы (реализованные CAS, у алгоритма CAS есть проблемы ABA)
Когда есть несколько переменных состояния, и они связаны друг с другом, чистого атомарного класса недостаточно, и следует использовать синхронное управление кодом.В это время атомарные переменные можно опустить.
1.6 Повторный вход встроенной блокировки
Каждый объект Java можно использовать как синхронизированную блокировку, называемую встроенной блокировкой.
Встроенные блокировки являются реентерабельными, поэтому, если поток попытается получить блокировку, уже удерживаемую им самим, запрос будет выполнен успешно.Получение операции блокировки это нить, а не звонить
Обратите внимание, что синхронизированные методы экземпляра блокируют экземпляр вызывающего объекта.
class Widget {
public synchronized void doSomething() {
}
}
class LoggingWidget extends Widget {
/*
* 实例方法上的synchronized锁住的都是调用实例
* 这里肯定是用LoggingWidget实例去调用,锁住LoggingWidget实例
*/
public synchronized void doSomething() {
//这里依旧是LoggingWidget实例去调用父类的synchronized方法
//锁住的依然是调用者LoggingWidget实例
super.doSomething();
}
}
Каждая блокировка связана со счетчиком запросов и потоком, которому она принадлежит. Когда счетчик запросов равен 0, блокировку можно считать снятой. Когда поток запрашивает незаблокированную блокировку, JVM записывает владельца блокировки и помещает счетчик запросов блокировки увеличивается на 1, если тот же поток снова запрашивает блокировку, счетчик запросов увеличивается, когда поток выходит из синхронизированного блока, счетчик уменьшается на 1, а когда счетчик равен 0, блокировка снимается ( это гарантирует, что блокировка является повторной и не будет тупиковой).
1.7 Видимость синхронизированной реализации
Процесс, посредством которого потоки выполняют взаимоисключающий код.
- приобретать мьютекс
- очистить рабочую память
- Последняя копия мастер-копии из переменных памяти в рабочую память
- выполнить код
- Сбросить обновленное значение общей переменной в основную память
- освободить мьютекс
Lock -> 主内存 -> 工作内存 -> 主内存 -> unlock
1.8 Синхронизация требуется при чтении и записи общих переменных
Для изменяемых переменных состояния, к которым одновременно могут обращаться несколько потоков, при доступе необходимо удерживать одну и ту же блокировку, и переменные состояния защищены этой блокировкой.
1.9 Атомарные переменные и синхронизированные блоки
Если работа с атомарными переменными уже есть в синхронизируемом блоке кода, можно отказаться от использования атомарных переменных.Обычные переменные лучше - разные механизмы синхронизации легко вызвать путаницу.Когда достаточно одного механизма синхронизации, не добавляйте другие механизмы синхронизации .
1.10 Количество блокировок объектов
После того, как поток получает блокировку объекта, он может только помешать другим потокам получить ту же блокировку. Причина, по которой каждый объект имеет встроенную блокировку, заключается в том, чтобы избежать явного создания объекта блокировки.
Synchronized изменяет методы экземпляра и получает блокировки экземпляра (блокировки объекта), изменяет статические методы и получает блокировки класса То же самое верно для блоков кода.
Блокировки делятся на блокировки объектов и блокировки классов.
- блокировка объекта
Он используется для методов экземпляра объекта или для экземпляра объекта, и блокировки объектов разных экземпляров объектов не мешают друг другу. - блокировка класса
Он используется для статических методов класса или класса объекта класса, но у каждого класса есть только один класс замок. На самом деле, блокировка класса - это просто концептуальная вещь, не реальная, она используется только для того, чтобы помочь нам понять экземпляры замков Разница между методом и статическим методом -
Блокировки объектов и блокировки классов не мешают друг другу
Один поток получает блокировку объекта, а другой поток также получает блокировку класса, которая может выполняться поочередно. - Блокировки нескольких объектов не связаны
Блокировка объекта одного класса не связана с блокировкой объекта другого класса.Когда поток получает блокировку объекта класса A, он также может получить блокировку объекта класса B.
2. Объектная память
2.1 Минимальная безопасность
Модель памяти Java требует, чтобы чтение и запись переменных были атомарными операциями. При отсутствии синхронизации потоков значение переменной чтения должно быть значением, установленным потоком, а не случайным значением. Обратите внимание на концепцию чтения и записи здесь. , это jvm Чтение и запись в коде не чтение и запись в коде
Но для переменных long и double энергонезависимого типа JVM позволяет разбивать 64-битные операции чтения/записи на две 32-битные операции, что может вызвать проблему, заключающуюся в том, что старшие 32 бита и младшие 32 бита не являются исходной комбинацией. .
Решение: изменить с помощью volatile или защитить с помощью блокировки
2.2 Нестабильная видимость
volatile-модифицированные операции с переменнымиНе будунаряду с другими операциями памятиизменение порядка, volatile-переменные не будут кэшироваться в регистрах или других местах, которые не видны процессору (прямое чтение и запись значения в основную память, без копирования), поэтому при чтении переменной volatile-типа всегда будет последнее записанное значение вернулся
- сцены, которые будут использоваться
- Запись в переменные не зависит от текущего значения переменной или только один поток обновляет значение переменной.
- Эта переменная не будет включена в инвариантное условие вместе с другими переменными состояния.
- Не требуется блокировка для доступа к переменным
Example
volatile boolean asleep;
…
while(asleep)
countSomeSleep()
Если volatile не используется, может случиться так, что когда спящий модифицируется потоком, поток, выполняющий суждение, не может быть изменен.В настоящее время использование volatile проще и удобнее, чем механизм блокировки.
Однако volatile гарантирует только видимость, а не атомарность.Например, volatile не гарантирует атомарность count++ (count++ имеет два шага чтения и записи), но механизм блокировки может
Неизменяемые объекты могут быть опубликованы с использованием изменчивых типов P40
2.3 Проблемы оптимизации JVM и многопоточной отладки — сервер
Для серверных приложений, как при разработке, так и при тестировании, при запуске jvmуказать -серверПараметры командной строки, режим сервера jvm будет делать больше, чем режим клиента jvmоптимизацияНапример, поднятие немодифицированных переменных в цикле за пределы цикла может вызвать исключение из нормального кода в среде разработки (режим клиента jvm) в среде развертывания (режим сервера jvm).
В коде примера volatile, если спящий режим не объявлен как volatile, JVM серверного режима повысит условие оценки спящего режима за пределами цикла, в то время как JVM клиентского режима этого не сделает.
2.4 Освобождение и побег
- выпуск
Объект может использоваться в коде за пределами текущей области (другие объекты могут ссылаться на объект) - побег
Экранирует, когда публикуется объект, который не должен публиковаться
Когда объект высвобождается до завершения его строительствав другие темы, это разрушит потокобезопасность. В частности, когда объект освобождается из конструктора объекта, он освобождает только объект, который еще не был создан (то есть это не инициализировано, но ваше this может быть вновь выпущено в конструкторе (созданная) ссылка на объект)
Не убегайте с этой ссылкой во время строительства
Example
//Wrong
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
//Correct
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener(); //先构造完成
source.registerListener(safe.listener); //再发布
return safe;
}
2.5 Специальное закрытие резьбы
Это означает, что ответственность за закрытие потока полностью берет на себя программная реализация, не разделяет данные, обращается к данным только в пределах одного потока и закрывает объект для целевого потока; из-за его хрупкости его следует использовать как можно реже. возможно, и следует использовать более сильные потоки Методы закрытия, такие как закрытие стека или классы threadlocal
2.6 Закрытие стека
Переменные существуют только в стеке потока выполнения и используются только внутри потока.
Если объект, не являющийся потокобезопасным, используется во внутреннем контексте потока, объект по-прежнему является потокобезопасным.
2.7 Класс ThreadLoad
класс может бытьThreadLoad<T>
рассматривается какMap<Thread ,T>
Threadlocad предоставляет методы set get, которые сохраняют независимую копию для каждого потока, использующего переменную, поэтому get всегда возвращает последнее значение, установленное текущим исполняемым потоком, при вызове set
Лучше не помещать его в пул потоков, чтобы избежать повторного использования.
2.8 Неизменяемые объекты
- Неизменность объекта
- Статус не может быть изменен
- Все поля имеют окончательный тип: окончательные поля могут гарантировать безопасность процесса инициализации.
- правильный процесс строительства
Безопасный доступ любого потока без необходимости синхронизациинеизменяемый объект, даже если при публикации этих объектов не использовалась синхронизация
Если объект, на который указывает конечное поле типа, является изменяемым объектом, то ссылка является неизменной, но состояние изменяемого объекта является изменяемым, и при доступе к состоянию объекта по-прежнему требуется синхронизация.
public class Holder {
// private int n;
private final int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if (n != n)
throw new AssertionError("This statement is false.");
}
}
Установите значение final, чтобы убедиться, что объект построен правильно и является потокобезопасным.
3. Обмен объектами
3.1 Как правильно построить метод безопасного освобождения объекта
1. 在静态初始化函数中初始化一个对象引用:单例饿汉模式
2. 将对象的引用保存到volatile类型的域或者AtomicReferance对象中:原子类
3. 将对象引用保存到某个正确构造对象的final域中:不可变
4. 将对象引用保存到一个由锁保护的域中:锁
可以将对象放入到线程安全的容器中:
Hashtable synchronizedMap concurrentMap vector copyOnWriterArrayList copyOnWriterSet synchronizedList synchronizedSet blockingQueue concurrentLinkedQueue
3.2 Статически инициализированные ссылки на объекты
Статический инициализатор выполняется JVM на этапе инициализации класса.Благодаря механизму синхронизации внутри JVM объект может быть безопасно освобожден
public static Hodlder holder = new Hodeler(42);
3.2 Изменяемость и выпуск объекта
Неизменяемые объекты могут быть опубликованы любым механизмом
Тот факт, что неизменяемые объекты должны публиковаться безопасным способом
Изменяемые объекты должны освобождаться безопасным способом и должны быть потокобезопасными или защищенными блокировкой.
3.4 Использование и совместное использование объектов
- Тема закрыта
Объекты могут принадлежать только потоку, заключенному в поток - общий доступ только для чтения
- потокобезопасный обмен
Поточно-ориентированные объекты реализуют внутреннюю синхронизацию, и несколько потоков могут получить доступ к общему интерфейсу объекта без дальнейшей синхронизации. - объект охраны
Доступ к защищенным объектам можно получить, только удерживая определенную блокировку. К защищенным объектам относятся объекты, инкапсулированные в другие потокобезопасные объекты, а также опубликованные объекты, защищенные определенной блокировкой.
4. Комбинация объектов
4.1 Проектирование потокобезопасных классов
- Найти все переменные, составляющие состояние объекта
- Найдите условия инвариантности, которые ограничивают переменные состояния
- Стратегия параллельного доступа для установления состояния объекта
4.2 Синхронизация классов Java Basic Container
Некоторые базовые классы синхронизации Java не являются потокобезопасными, но могут быть выполнены с помощью методов фабрики-оболочки.collections.synchronizedList()
Тип пакета упаковочных контейнеров объектов в синхронизированном
4.3 Режим Java-монитора
Инкапсулируйте все изменяемые состояния объекта и защитите его собственной встроенной блокировкой объекта.
такие как вектор и хеш-таблица
Может оказаться выгоднее использовать объектно-приватные блокировки (private).
При этом при получении защищенного объекта вы можете вернуть скопированный объект, а при изменении объекта вы можете изменить его через общий метод защищенного объекта (не изменять напрямую возвращенный скопированный объект)
copyonwrite заключается в изменении возвращаемой коллекции, а затем в изменении ссылки
4.4 Комиссия по безопасности потоков
Если класс состоит из нескольких независимых и безремотных переменных состояний, и ни одна из операций не включает неверные переходы состояния, безопасность потоков может быть делегирована в основные переменные состояния
Потокобезопасностью можно управлять, назначая переменные состояния потокобезопасным классам, таким как потокобезопасные контейнеры, неизменяемые контейнеры, атомарные классы и т. д.
Переменные, связанные с потокобезопасностью, должны быть как можно чаще установлены в окончательный тип.
При возврате ссылки следует обратить особое внимание на то, вызовет ли она экранирование, можно ли вернуть скопированный объект, или неизменяемый объект (сам объект неизменен (обратите внимание, можно ли модифицировать ссылку), неизменяемый контейнер, синхронизированный контейнер )
4.5 Блокировка клиента
Объекты, которые необходимо синхронизировать, можно разместить в клиенте для синхронизации, нужно обратить вниманиеБлокировать тот же замок при синхронизации
Если вектор является классом синхронизации, его внутренние операции метода синхронизируются, но когда несколько операций выполняются синхронно последовательно, это может быть реализовано путем добавления блокировки на стороне клиента, при этом добавленная блокировка должна быть согласована с исходной замок векторного объекта, то есть сам векторный объектsynchronized(vector){ … }
5. Основные строительные блоки
5.1 Синхронные контейнеры и параллельные контейнеры
-
Контейнер синхронизации
Классы инкапсуляции Vector, Hashtable и синхронизации, которые могут бытьCollections.synchronizedXxxx
и т.д. способ создания
Хотя все классы контейнеров синхронизации являются потокобезопасными, в некоторых случаях (составные операции) их все равно необходимо защищать блокировкой;
Синхронизированный доступ контейнера ко всем состояниям контейнера сериализуется, что серьезно снижает параллелизм; когда несколько потоков конкурируют за блокировки, пропускная способность сильно снижается; -
параллельный контейнер
После java5.0 для повышения производительности синхронных контейнеров предоставляются различные параллельные контейнеры, такие как ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteArraySet, ConcurrentSkipListMap и т. д.
Возьмите ConcurrentHashMap в качестве примера.
использоватьТехнология раздельного замка, в контейнере синхронизации есть один контейнер и одна блокировка, но в ConcurrentHashMap часть массива хеш-таблицы разделена на несколько сегментов, и каждый сегмент поддерживает блокировку для обеспечения эффективного параллельного доступа;
итераторслабая консистенция, ConcurrentModificationException не будет выброшено во время итерации;
size()
,isEmpty()
и другие методы возвращают приближение;
Например, операция размера сохраняет последнее значение для записи общего количества статистики в последнем цикле.Она будет возвращаться только тогда, когда два значения статистики до и после равны.
Добавлено несколько атомарных методов работы, таких как putIfAbsent (без ключа, добавьте его)Обратите внимание, что в настоящее время клиент не может быть заблокирован, новые новые атомарные операции, клиент одновременно блокирует только сам контейнер, но не сам контейнер внутри одновременного использования блокировки
Копирование контейнера при записи: CopyonWrite, блокировка и создание новой копии контейнера, непосредственное изменение ссылки на контейнер, но не блокировка при чтении, прямое чтение исходного значения, проблема, вызванная записью, хотя она заблокирована, но все еще читается, он может быть прочитан до значения ошибки.Операция его доступа и записи в конечном итоге пройдет соответствующий метод Final, такой как
setArray()
,getArray()
Используйте Copyonwrite, когда больше читаете и меньше пишете -
Суммировать
Синхронные контейнеры используются только тогда, когда приложению требуется эксклюзивный доступ к блокировкам контейнера, в противном случае для обеспечения лучшей производительности используются неконкурентные контейнеры.
5.2 Метод блокировки и метод прерывания
Но когда вы вызываете метод в своем коде, который может генерировать InterruptedException, ваш собственный метод становится методом блокировки и должен обрабатывать прерванный ответ.
- Пропустить InterruptedException
Бросить InterruptedException вызывающему методу - возобновить прерывание
Если InterruptedException не может быть выброшен, например, когда код является частью исполняемого кода, InterruptedException должен быть перехвачен, а затем вызывается прерывание текущего потока для восстановления прерванного состояния, вызывая прерывание кода более высокого уровня.
public void run() {
try {
processTask(queue.take());
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
}
}
5.3 Класс инструментов синхронизации
Класс инструмента синхронизации может координировать поток управления потоком в соответствии со своим собственным состоянием.
- блокировка очереди блокировка очереди
Он может не только выступать в качестве контейнера для хранения объектов, но также может координировать поток управления между потоками, такими как производители и потребители, поскольку методы take и put будут блокироваться.
Метод блокировки: (сput()
метод как пример)
- При построении объекта blockingQueue создайте блокировку и соответствующее условие.
lock = new ReentrantLock(fair);
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
- При установке сначала заблокируйте, а затем оцените, заполнен ли он в цикле.Если он заполнен, подождите, пока условие не будет пробуждено такими операциями, как взятие
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
Обратите внимание, что должна быть петля.После пробуждения вам нужно вернуться к петле, чтобы оценить, соответствует ли она условиям снова.
- атрезия
может гарантировать, что некоторые мероприятия продолжают выполнять, пока другие действия не будут завершены,Как только конечное состояние будет достигнуто, состояние больше не будет изменено.
CountDownLatch: один или несколько потоков могут ожидать возникновения события. Зафиксированное состояние включает в себя метод счетчика countDown. Когда текущий поток вызывает этот метод, счетчик уменьшается на единицу; вызов этого метода блокирует текущий поток пока значение таймера не станет равным 0
//提供统一入口&出口
public class TestHarness {
public long timeTasks(int nThreads, final Runnable task)
throws InterruptedException {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++) {
Thread t = new Thread() {
public void run() {
try {
// 所有线程等在这里,直到计数为0,即调用了startGate.countDown();
startGate.await();
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InterruptedException ignored) {
}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
// 所有线程等在这里,直到计数为0,即调用了endGate.countDown() nThreads次;
endGate.await();
long end = System.nanoTime();
return end - start;
}
}
FutureTask: исполняемый объект, который может генерировать результаты, включая 3 состояния: ожидание запуска, выполнение и завершение выполнения. Если задача выполнена, тоfuture.get()
Немедленно вернет результат, в противном случае заблокирует до состояния завершения.После завершения остановите навсегда
Сценарии использования FutureTask: использованиеConcurrentMap <A, Future<V>>
Расчет кэша, значение параметра Future, P89
// 使用FutureTask来提前加载稍后需要的数据
public class Preloader {
ProductInfo loadProductInfo() {
return null; // 这里执行复杂计算or等待
}
private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(
new Callable<ProductInfo>() {
public ProductInfo call() throws InterruptedException {
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public void start() {
thread.start();
}
public ProductInfo get() throws InterruptedException {
try {
return future.get(); // 阻塞直到有结果
} catch (ExecutionException e) {
throw e;
}
}
interface ProductInfo {
}
}
- сигнал
Используется для управления количеством операций, которые одновременно обращаются к определенному ресурсу, или количеством одновременных выполнений указанной операции.
Semaphore управляет набором виртуальных лицензий.При выполнении операций сначала необходимо приобрести лицензию.Если нет,блокировать до получения лицензии.Освободить лицензию после использования.
public class BoundedHashSet <T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
} finally {
if (!wasAdded)
sem.release();
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
}
5.4 Заборы
Используется для ожидания других потоков, все потоки должны одновременно достичь позиции ограждения, чтобы продолжить выполнение.
CyclicBarier может включать определенное количество стороннеоднократноОбъединенные в местах забора, полезны в параллельных итерационных алгоритмах
Обменник — это двухсторонний барьер, где каждая сторона обменивается данными в позиции барьера, которая используется обеими сторонами для выполнения асимметричных операций.
private final CyclicBarrier barrier;
//barrier.await()调用了count次就执行内部线程mainBoard.commitNewValues()方法
this.barrier = new CyclicBarrier(count,
new Runnable() {
public void run() {
mainBoard.commitNewValues();
}});
public void run() {
while (!board.hasConverged()) {
//当循环走完,即表示计算完成
for (int x = 0; x < board.getMaxX(); x++)
for (int y = 0; y < board.getMaxY(); y++)
board.setNewValue(x, y, computeValue(x, y));
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
Количество ЦП потока P84 и пропускная способность
Когда не используются операции ввода-вывода или общий доступ к данным, оптимальная пропускная способность достигается, когда количество потоков равно количеству ЦП или количеству ЦП + 1. Поток под одним процессом такой, а если процессов несколько? Распределение времени между процессами?
6. Структурированные параллельные приложения
6.1 Executor
Предоставляет стандартный метод разделения процесса отправки и выполнения задач, а также поддерживает жизненные циклы, а также сбор статистики, механизмы управления приложениями и механизмы мониторинга производительности.
название | Потоки | аномальный | характеристика | внедрить очередь |
---|---|---|---|---|
newfixedThreadPool | фиксированный | Если возникает и завершается исключение, добавьте поток | постепенно увеличивать до максимального | LinkedBlockingQueue |
newCachedThreadPool | Maximuminter.max_value. | Пул потоков можно кэшировать. Если размер пула потоков превышает текущий спрос, простаивающие потоки повторно используются, и пул потоков можно расширять до бесконечности. | SynchronousQueue | |
newSingleThreadExecutor | 1 | Если возникает исключение и заканчивается, запустите новый поток | Один поток выполняется в порядке приоритета и т. д. | LinkedBlockingQueue |
newScheduledThreadPool | фиксированный | Выполнение с задержкой или по времени | Массив RunnableScheduledFuture[] |
ExecutorService exec = Executors.newSingleThreadExecutor();
6.2 Жизненный цикл исполнителя
Три состояния: работает, выключено и прекращено
Четыре этапа жизненного цикла: создание, фиксация, запуск и завершение
Задачи, которые были отправлены, но не запущены, могут быть отменены, задачи, которые начали выполняться, могут быть отменены только в том случае, если они могут реагировать на прерывания.
JVM завершится только после того, как все потоки, не являющиеся демонами, будут завершены, если исполнитель не может быть правильно завершен, то jvm не может завершиться
И есть два способа закрыть
1. Аккуратно завершите работу: прекратите принимать новые задачи и выполните все задачи, которые в данный момент выполняются и ожидают в очереди.
2. Принудительное завершение работы shutdownNow: отмените все запущенные задачи, больше не запускайте задачи в очереди ожидания, верните все задачи, которые были отправлены, но не запущены, и занесите задачи в журналы и т. д.
6.3 Отложенные задачи и периодические задачи
Выполняется класс java.util.TimerвсеПри планировании задачи создается только один поток. Если задача выполняется слишком долго, это нарушит точность синхронизации других TimerTasks.
Если Timer выдает исключение, он отменяет все потоки синхронизации в классе timer и не возобновляет выполнение.
6.4 Future
Все методы отправки в ExecutorService будут возвращены в будущее, чтобы получить результат задачи.
Future имеет метод отмены, который может отменить задачу.
6.5 CompletionService
CompletionService: объединяет функции executor и blockingqueue, отправляет вызываемую задачу ему на выполнение, а затем использует такие методы, как take и poll, аналогичные операциям с очередью, для получения будущего, а затемfuture.get()
Вернуть результат. Вот набор результатов расчета, с которыми можно иметь дело, и которые можно получить после возврата.
Например, ExecutorCompletionService фактически помещает результат вычисления в очередь блокировки.
void renderPage(CharSequence source) {
final List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor);
for (final ImageInfo imageInfo : info)
completionService.submit(new Callable<ImageData>() {
public ImageData call() {
return imageInfo.downloadImage();
}
});
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++) {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
6.6 Сроки проектирования для задач
Future устанавливает ограничение по времени для задачи: если есть результат в течение ограничения по времени, get возвращается немедленно, а при превышении ограничения по времени выдается исключение TimeOutException.Future.get(long,timeType)
Отправьте набор задач
InvokeAll: отправьте несколько задач в ExecutorService и получите результаты.InvokeAll добавляется в возвращаемую коллекцию в порядке итераторов в коллекции задач, чтобы можно было связать каждое будущее и вызываемое
Когда выполнение задачи завершено/вызывающий поток прерывается/превышается тайм-аут, invokeAll вернется, и вы можете судить о ситуации с помощью get или isCancle
List<QuoteTask> tasks = new ArrayList<QuoteTask>();
for (TravelCompany company : companies)
tasks.add(new QuoteTask(company, travelInfo));
List<Future<TravelQuote>> futures = exec.invokeAll(tasks, time, unit);
List<TravelQuote> quotes = new ArrayList<TravelQuote>(tasks.size());
for (Future<TravelQuote> f : futures) {
try {
quotes.add(f.get());
} catch (ExecutionException e) {
quotes.add(...); //按序放回关联,需要放入对象
} catch (CancellationException e) {
quotes.add(...); //按序放回关联,需要放入对象
}
}
class QuoteTask implements Callable<TravelQuote> {
public TravelQuote call() throws Exception {
return company.solicitQuote(travelInfo);
}
}
7. Отмена и закрытие
Java не предоставляет никакого механизма для безопасного завершения потоков, а скорее предоставляет прерывания, которые позволяют одному потоку завершить текущую работу другого потока.
7.1 Callable
Callable считает, что основная точка входа вернет значение и может сгенерировать исключение
Нет возвращаемого значения, используйте Callable
7.2 Interrupt
Вызов прерывания не сразу останавливает работу, выполняемую целевым потоком, а просто передает сообщение с запросом на прерывание, и поток прерывает себя в следующий подходящий момент.
Обычно наиболее разумным способом добиться отмены являются прерывания, а не установка флагов: если флаги используются, блокировка ввода-вывода всегда будет зависать, а запросы на прерывание могут только устанавливать состояние прерывания потока, а они также зависают и может быть только закрыт Интерфейс ввода/вывода
7.3 Interrupted
Получить статус прерывания и очистить статус прерывания текущего потока.
возвращает true при вызове прерывания, затемОчистить статус прерывания потока, в следующий раз, когда вы вызовете interrupted, он больше не находится в прерванном состоянии, поэтому вам нужно обработать прерывание — бросить прерываниеException или снова вызвать прерывание, чтобы восстановить прерванное состояние:Thread.currentThread().interrupt();
7.4 Стратегия прерывания
Наиболее разумная отмена — это некоторая форма отмены на уровне потока или на уровне обслуживания: выйти как можно скорее, очиститься, если необходимо, уведомить какого-либо владельца о завершении потока.
Поток должен быть прерван только его владельцем, который может инкапсулировать информацию о политике прерывания потока в какой-либо подходящий механизм отмены, такой как метод завершения работы.
- Базовая политика прерывания
Pass interruptedException: передать прерываниеException вызывающей стороне метода.
Возобновить прерывание: когда текущий код является частью исполняемого, исключение не может быть выдано, исключение должно быть перехвачено, а прерванное состояние восстанавливается путем вызова метода прерывания в текущем потоке, так что код, находящийся выше в стеке вызовов, будет видеть, что было возбуждено исключение
При попытке отменить задачу нецелесообразно прерывать пул потоков напрямую, и отмена может быть достигнута только через будущее задачи.public void run() { try { processTask(queue.take()); } catch (InterruptedException e) { // restore interrupted status Thread.currentThread().interrupt(); } }
Future<?> task = taskExec.submit(r); try { task.get(timeout, unit); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } finally { task.cancel(true); }
7.5 Непрерывная блокировка
При блокировке, такой как ввод-вывод сокета, или при ожидании получения встроенной блокировки запрос на прерывание может только установить статус прерывания потока, и это не имеет большого эффекта. В этот момент базовая блокирующая операция должна быть прервана, и в ответ на прерывание должно быть создано исключение.
Пример:
Блокировка чтения сокета
Переопределить метод прерывания потока
public void interrupt() {
try {
socket.close();
} catch (IOException ignored) {
} finally {
super.interrupt();
}
}
7.6 newTaskFor пользовательское прерывание
Поток может быть прерван или основной метод блокировки может быть отменен.
Обратите внимание, что при отмене операции производитель-потребитель необходимо отменить и производителя, и потребителя.
public abstract class SocketUsingTask <T> implements CancellableTask<T> {
@GuardedBy("this") private Socket socket;
protected synchronized void setSocket(Socket s) {
socket = s;
}
//自定义的取消方法
public synchronized void cancel() {
try {
if (socket != null)
socket.close();
} catch (IOException ignored) {
}
}
public RunnableFuture<T> newTask() {
return new FutureTask<T>(this) {
public boolean cancel(boolean mayInterruptIfRunning) {
try {
//先调用自身取消方法
SocketUsingTask.this.cancel();
} finally {
return super.cancel(mayInterruptIfRunning);
}
}
};
}
}
//新增两个方法
interface CancellableTask <T> extends Callable<T> {
void cancel();
RunnableFuture<T> newTask();
}
@ThreadSafe
class CancellingExecutor extends ThreadPoolExecutor {
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
if (callable instanceof CancellableTask)
return ((CancellableTask<T>) callable).newTask(); //返回扩展对象
else
return super.newTaskFor(callable);
}
}
7.6 Прерывание по биту флага
Логический флаг может быть установлен, чтобы указать, следует ли отменить. В то же время устанавливается счетчик для подсчета количества задач в текущей очереди задач.При закрытии флага поток прерывается.Основной метод производителя определяет, был ли закрыт флаг, генерирует исключение и потребитель отменяет только тогда, когда значения флага и счетчика равны 0. В противном случае очередь задач обрабатывается до тех пор, пока все задачи не будут выполнены.
public class LogService {
private final BlockingQueue<String> queue;
private final LoggerThread loggerThread;
private final PrintWriter writer;
@GuardedBy("this") private boolean isShutdown;
@GuardedBy("this") private int reservations;
public LogService(Writer writer) {
this.queue = new LinkedBlockingQueue<String>();
this.loggerThread = new LoggerThread();
this.writer = new PrintWriter(writer);
}
public void start() {
loggerThread.start();
}
public void stop() {
synchronized (this) {
isShutdown = true;
}
loggerThread.interrupt();
}
public void log(String msg) throws InterruptedException {
synchronized (this) {
if (isShutdown)
throw new IllegalStateException(/*...*/);
++reservations;
}
queue.put(msg);
}
private class LoggerThread extends Thread {
public void run() {
try {
while (true) {
try {
synchronized (LogService.this) {
//要把对象消费完
if (isShutdown && reservations == 0)
break;
}
String msg = queue.take();
synchronized (LogService.this) {
--reservations;
}
writer.println(msg);
} catch (InterruptedException e) { /* retry */
}
}
} finally {
writer.close();
}
}
}
}
7.8 Объекты с ядовитыми пилюлями
Добавьте согласованный объект в очередь задач, и потребитель каждый раз проверяет объект, чтобы определить, следует ли выйти
Вы также можете сделать другую статистику и выйти, когда число будет достигнуто, чтобы вы могли убедиться, что несколько потоков отменены.
Полезно только тогда, когда известны производитель и потребитель, чтобы подтвердить количество произведенных объектов ядовитых пилюль.
Примечание: только еслинеограниченная очередь, объект ядовитой таблетки может работать надежно
public class IndexingService {
private static final int CAPACITY = 1000;
private static final File POISON = new File("");
private final IndexerThread consumer = new IndexerThread();
private final CrawlerThread producer = new CrawlerThread();
private final BlockingQueue<File> queue;
private final FileFilter fileFilter;
private final File root;
public IndexingService(File root, final FileFilter fileFilter) {
this.root = root;
this.queue = new LinkedBlockingQueue<File>(CAPACITY);
this.fileFilter = new FileFilter() {
public boolean accept(File f) {
return f.isDirectory() || fileFilter.accept(f);
}
};
}
private boolean alreadyIndexed(File f) {
return false;
}
public void start() {
producer.start();
consumer.start();
}
public void stop() { //中断机制
producer.interrupt();
}
public void awaitTermination() throws InterruptedException {
consumer.join();
}
}
потребитель
class IndexerThread extends Thread {
public void run() {
try {
while (true) {
File file = queue.take();
if (file == POISON)
break;
else
indexFile(file);
}
} catch (InterruptedException consumed) {
}
}
public void indexFile(File file) {
/*...*/
};
}
режиссер
class CrawlerThread extends Thread {
public void run() {
try {
crawl(root);
} catch (InterruptedException e) { /* 被打断就放入毒丸对象 */
} finally {
while (true) {
try {
queue.put(POISON);
break;
} catch (InterruptedException e1) { /* retry */
}
}
}
}
private void crawl(File root) throws InterruptedException {
File[] entries = root.listFiles(fileFilter);
if (entries != null) {
for (File entry : entries) {
if (entry.isDirectory())
crawl(entry);
else if (!alreadyIndexed(entry))
queue.put(entry);
}
}
}
}
Закрыть 7.9 ExecutorService
Мягкое завершение работы: прекратите принимать новые задачи и выполните все задачи, которые в данный момент выполняются и ожидают в очереди.
public void stop() {
try {
exec.shutdown();
exec.awaitTermination(3000, TimeUnit);//等待执行完成,这里不是冗余吗?
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
...
}
}
7.10 Обработка исключений
Основной причиной преждевременной смерти нити является Runtimeexception. В коде резьбы вы можете использовать блок кода Try-Catch, чтобы поймать исключение и обработать его.
Неперехваченное исключение
UncaughtExceptionHandler, класс обработки исключений, предоставляемый в Thread API, может обнаруживать завершение потока из-за неперехваченного исключения и, по крайней мере, печатать информацию об исключении в таблице журнала. Необходимо предоставить ThreadFactory для конструктора ThreadPoolExecutor.
public class MyAppThread extends Thread {
public MyAppThread(Runnable runnable, String name) {
super(runnable, name);
setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
log.log(Level.SEVERE, "UNCAUGHT in thread " + t.getName(), e);
}
});
}
}
Только задачи, отправленные через execute, могут передать созданное им исключение обработчику неперехваченных исключений, а задачи, отправленные через submit, независимо от того, генерирует ли он непроверенное или проверенное исключение, будут рассматриваться как часть состояния возврата задачи.
Если задача, отправленная с помощью submit, завершается из-за создания исключения, то это исключение будет повторно сгенерировано в ExecutionException, инкапсулированном future.get.
Если вы хотите получать уведомления о сбое задачи из-за исключения и выполнять некоторые операции восстановления для конкретной задачи, вы можете инкапсулировать задачу в исполняемый или вызываемый объект, который может перехватывать исключения, или переопределить метод afterExecute класса ThreadPoolExecutor.
7.11 Ловушка отключения JVM
Перехватчик выключения: при обычном завершении работы JVM JVM сначала вызывает все зарегистрированные перехватчики выключения, ссылаясь наRuntime.addShutdownHook()
Тема зарегистрирована, но не запущена
JVM не останавливает и не прерывает какие-либо потоки приложения, которые все еще работают при завершении работы, когда JVM окончательно завершается, поток демона будет принудительно завершен.
Runtime.getRuntime().addShutdownHook(new Thread(){...});
7.12 Потоки демона
Обычный поток: все потоки, созданные основным потоком, являются обычными потоками, а обычные потоки наследуют защитное состояние создавшего его потока.
Поток демона: необычный поток. Когда поток завершается, jvm проверяет текущий поток. Если это поток демона, jvm завершает работу. Когда jvm останавливается, все потоки демона прекращаются и завершаются напрямую.
Потоки демона лучше всего выполняют «внутренние задачи».
8. Использование пула потоков
8.1 ThreadLocal не работает с пулами потоков
Использовать threadlocal в потоках пула потоков имеет смысл только в том случае, если время жизни локального значения потока ограничено временем жизни задачи, а threadlocal не следует использовать в потоках пула потоков для передачи значений между задачами
8.2 Тупик голодания
Всякий раз, когда задача в пуле потоков должна неопределенно долго ждать некоторого ресурса или условия, которые должны быть предоставлены другими задачами в пуле, если пул потоков не является достаточно большим, возникает взаимоблокировка голодания потока.
Всякий раз, когда вы отправляете задачу зависимого исполнителя, вам нужно знать, что может возникнуть взаимоблокировка голодания потока, поэтому вам необходимо записать ограничение размера или ограничение конфигурации пула потоков в коде или файле конфигурации исполнителя.
Установка границ для рабочих очередей пула потоков разумна только в том случае, если задачи не зависят друг от друга. Если между задачами есть зависимости, то ограниченные пулы потоков или очереди могут привести к проблемам «тупиковой блокировки» потока. В этом случае следует использовать неограниченный поток. пулы, такие как newCachePool
8.3 ограничение времени работы
Большинство блокируемых методов определяют ограниченные по времени и неограниченные версии, такие какThread.join
, blockingQueue.put
, countDownLatch.await
, selector.select
Ждать. Если время ожидания истекло, задача может быть помечена как неудачная и либо прекращена, либо повторно поставлена в очередь для последующего выполнения.
8.4 Размер пула потоков
Размер пула потоков не должен быть фиксированным и должен предоставляться с помощью механизма конфигурации или на основеRuntime.getRuntime().availableProcessors()
для динамического расчета
Если необходимо выполнять разные классы задач, и поведение между нимиБольшая разница, то вам следует рассмотреть возможность использованиянесколько пулов потоков, чтобы каждый пул потоков можно было настроить в соответствии с соответствующей рабочей нагрузкой.
Интенсивность вычислений: размер пула потоков равен количеству ЦП + 1.
Операции ввода-вывода или другие блокирующие операции: поток не будет выполняться все время, масштаб должен быть больше, и необходимо оценить отношение времени ожидания задачи к времени расчета
N_cpu=number of CPUs
U_cpu=期望CPU利用率,0≤U_cpu≤1
W/C=等待时间/计算时间
Для достижения желаемого использования процессора оптимальный размер пула потоков равен:
N_threads =N_cpu*U_cpu*(1+W/C)
Количество процессоров можно получить во время выполненияint cpu = Runtime.getRuntime().availableProcessors();
threadPoolExecutor позволяет предоставить BlockingQueue для сохранения задач, ожидающих выполнения.Существует три основных метода постановки задач в очередь: неограниченная очередь, ограниченная очередь и синхронная передача обслуживания.
8.5 Неограниченные очереди
Фабричные методы newFixedPoolExecutor , newSingleThreadExecutor по умолчанию используют неограниченную linkedBlockedQueue.
8.6 Ограниченные очереди
ArrayBlockingQueue, ограниченная linkedBlockingQueue, PriorityBlockingQueue, ограниченная очередь помогает избежать исчерпания ресурсов
8.7 Синхронная передача обслуживания
Для очень большого или неограниченного пула нитей вы можете избежать поставленных задач в очередь, и задачи напрямую от производителя до рабочей нити
Если ни один поток не ожидает принятия задачи и пул потоков не заполнен, вновь созданный поток принимает задачу, в противном случае задача будет отклонена в соответствии с политикой насыщения.
Фабричный метод NewCachedThreadPool использует synchronousQueue
synchronousQueue полезен только в том случае, если пул потоков не ограничен или может отклонять задачи.
8.8 Стратегия насыщения
Когда ограниченная очередь заполнена или Executor закрыт, в игру вступает стратегия насыщения, которую можно изменить, вызвав setRejectedExecutionHandler.
Различные реализации RejectedExecutionHandler
- Прервать Прервать
Стратегия насыщения по умолчанию, выдает непроверенное исключение RejectedExecutionHandlerException - отбросить отбросить
Когда вновь отправленная задача не может быть сохранена в очереди для выполнения, задача тихо отбрасывается. - отбросить самое старое отбросить самое старое
Откажитесь от выполнения следующей задачи, а затем попробуйте отправить задачу повторно.Если очередь работ является приоритетной, задача с наивысшим приоритетом будет отклонена. - Вызывающий запускает вызывающий абонент
будет задаватьвозвращатьсявызывающему абоненту, управляемому вызывающим абонентом, таким образомзанять поток вызывающего абонента, уменьшить поток новых задач
В Webserver, когда основной поток выполняет задачи из-за отката, вновь поступившие запросы будут храниться в очереди TCP-уровня, а не в очереди прикладной программы, подбрасываются слой за слоем и, наконец, доходят до клиента, достигая мягкое снижение производительности
threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
8.9 Настройка политики безопасности PrivilegedThreadFactory
Может контролировать доступ к некоторой специальной кодовой базе, созданный поток будет иметь такой же доступ и создавать поток PrivilegedThreadFactory, AccessControlContext и contextClassLoader.
Если PrivilegedThreadFactory не используется, вновь созданные потоки будут наследовать права доступа от клиентской программы, которая вызывает выполнение или отправку.
8.10 Расширение ThreadPoolExextor
Предоставьте подклассы для переопределения методов перед выполнением, после выполнения, завершением
Если метод Run возвращает или выдает исключение, afterExecute будет выполнено, в случае ошибки afterExecute не будет выполнено.
Если beforeExecute выдает RuntimeException, задача не будет выполнена и afterExecute не будет вызвана
9. Избегайте активных опасностей
9.1 Взаимная блокировка ордера на блокировку
Тема должна начинаться сФиксированный и последовательныйполучить замки в порядке
Следует отметить, что хотя порядок ссылок на объекты является фиксированным, при выполнении двух блокировок осуществляется обмен фактическими объектами, что на самом деле не является фиксированным порядком блокировки, что может легко привести к тупиковой ситуации.
При блокировке в качестве основы сортировки для блокировки может использоваться уникальное и неизменное значение, такое как номер счета, идентификатор и т. д.
При формулировании порядка замков можно использоватьsystem.identityHashCode()
Получите значение хэш-кода объекта и последовательно заблокируйте значение хэш-кода, и объект может иметь одно и то же значение хэш-кода, тогда вы можете использовать блокировку сверхурочной работы, то есть, когда будет установлено, что значение хэш-кода одинаковое, блокировка блокировку сверхурочной работы, а затем используйте блокировку фиксированной последовательности
Если вы вызываете внешний метод, удерживая блокировку, проверьте, есть ли у вызванного внешнего метода операция синхронизации, чтобы избежать проблем взаимоблокировки.
9.2 Открытый вызов
Открытый вызов: не нужно удерживать блокировку при вызове метода
В программе следует максимально использовать открытые вызовы, чтобы упростить анализ взаимоблокировок.
тупик
//Class A
public synchronized void setLocation(Point location) {
...
if (location.equals(destination))
dispatcher.notifyAvailable(this); //方法调用也加锁
}
//class B
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
открытый вызов
public void setLocation(Point location) {
boolean reachedDestination; //新增中间变量
synchronized (this) {
…
reachedDestination = location.equals(destination);
}
if (reachedDestination)
dispatcher.notifyAvailable(this);
}
9.3 Временная блокировка TryLoAck
Можно указать период ожидания, по истечении которого будет возвращено сообщение об ошибке
9.4 Дамп потока Дамп потока идентифицирует тупиковые ситуации.
JVM помогает идентифицировать возникновение взаимоблокировок с помощью дампов потоков, которые включают информацию о трассировке стека для каждого выполняющегося потока, информацию о блокировках (какие блокировки есть у каждого потока, какие стеки кадров получают эти блокировки, и заблокированные потоки ожидают, какую блокировку получить) Перед сбросом потока jvm найдет взаимоблокировку путем поиска цикла в графе отношений ожидания.Если взаимоблокировка найдена, она получит соответствующую информацию о взаимоблокировке.
9.5 Голод Голод
Голодание происходит, когда поток не может продолжать выполнение, потому что он не может получить доступ к необходимым ему ресурсам.Наиболее распространенным ресурсом, вызывающим голодание, является такт ЦП.
Измените приоритет потока и неправильное использование или выполнение некоторых структур не может заканчиваться при удержании блокировки (бесконечная петля, беспроводное ожидание и т. Д.)
Чтобы избежать использования приоритета потока, потому что это увеличит зависимость от платформы и может привести к активной опасности
9.6 Livelock
Поток продолжает повторять одну и ту же операцию, но всегда терпит неудачу. Например, при обработке транзакционных сообщений, если сообщение не может быть успешно обработано, механизм обработки сообщений откатывает всю транзакцию и возвращает ее в начало очереди.
Когда несколько взаимодействующих потоков реагируют друг на друга, чтобы изменить их соответствующие состояния и предотвратить продолжение выполнения потоков, например, пешеходы уступают друг другу дорогу одновременно, введение случайных чисел в это время может избежать проблем.
10. Производительность и масштабируемость
Избегайте преждевременных оптимизаций, сначала получите программу, затем сделайте ее быстрее - если это недостаточно быстро
10.1 Закон Амдала
В случае увеличения вычислительных ресурсов максимальный коэффициент ускорения, которого теоретически может достичь программа, зависит от доли параллельных компонентов и последовательных компонентов в программе.
F: 串行执行比率
N:处理器个数
Speedup≤1/(F+(1-F)/N)
10.2 Переключение контекста
Процесс планирования потоков должен получить доступ к структуре данных, совместно используемой операционной системой и jvm, и его накладные расходы включаютНакладные расходы на выполнение кода jvm и ОС, в то же время, поскольку структура данных, необходимая новому потоку, может отсутствовать в локальном кеше текущего процессора при его включении, существуютНакладные расходы на переключение кеша
JVM может приостановить заблокированный поток и разрешить его замену.Когда поток часто блокируется, программа с интенсивным использованием ЦП будет иметь больше переключений контекста, тем самым увеличивая накладные расходы планирования и снижая пропускную способность.
В большинстве процессоров общего назначения накладные расходы на переключение контекста эквивалентны 5000-10000 тактовым циклам, то есть нескольким микросекундам.
Команда vmstat систем Unix и инструмент выполнения Windows могут сообщать такую информацию, как количество переключений контекста и процент времени выполнения в ядре. Если загруженность ядра превышает 10 %, это обычно указывает на частую активность планирования, которая может быть вызвана блокировкой, вызванной вводом-выводом или конкурирующими блокировками.
10.3 Синхронизация памяти
- Барьер памяти
Ограждения памяти могут использоваться в гарантиях видимости, обеспечиваемых синхронизированными и энергозависимыми, которые могут сбрасывать кэши, сбрасывать аппаратное обеспечение для конфликтов записи и останавливать конвейеры выполнения. Заборы памяти могут препятствовать некоторым оптимизациям компилятора, например, большинство операций нельзя переупорядочить. - JVM оптимизированный замок
Современные JVM могут оптимизировать и удалять некоторые блокировки, которые не конкурируют, тем самым уменьшая ненужные издержки синхронизации.
Если к блокировке может получить доступ только текущий поток, например, только что созданный объект в потоке, JVM может удалить операцию получения блокировки.
Когда JVM может обнаружить ссылки на локальные объекты, которые не будут выпущены в кучу с помощью анализа побега, операция получения блокировки может быть удалена или степень детализации блокировки может быть увеличена.
10.4 Блокировка
Когда JVM реализует поведение блокировки, она может
- Spin-waiting (цикл spin --waiting пытается получить блокировку до тех пор, пока не добьется успеха)
- Приостановить заблокированный поток операционной системой
Его эффективность зависит от накладных расходов на переключение контекста и времени ожидания до успешного получения блокировки.
Если время ожидания короткое, выберите ожидание отжима; если время ожидания долгое, выберите приостановку нити.
При блокировке происходит два переключения контекста, в том числе две необходимые системные операции и операции кэширования:
- Заблокированный поток выгружается до истечения времени его выполнения.
- Последующий поток переключается обратно
10.5 Уменьшение конкуренции за блокировку
- Сокращение времени удержания блокировки
- Уменьшить частоту запросов на блокировку
- Используйте эксклюзивные блокировки с механизмами координации, которые обеспечивают более высокий уровень параллелизма.
10.6 Уменьшение детализации блокировок
- декомпозиция замка
Если замок предназначен для защиты несколькихНезависимыйпеременные состояния, тогда блокировку можно разбить на несколько блокировок, каждая блокировка защищает только одну переменную, что снижает частоту запроса каждой блокировки - блокирующий сегмент
Когда частота состязаний за блокировки выше, чем частота состязаний за данные, защищенные блокировками, метод декомпозиции блокировок может быть дополнительно расширен для декомпозиции блокировок на набор независимых объектов.Например, ConcurrentHashMap содержит массив из 16 блокировок, и каждая блокировка защищает Итак, 1/16 блока хэша, N-й блок хеширования защищен N mod 16
Недостаток: сложность и накладные расходы на получение блокировок выше, а в некоторых случаях необходимо блокировать весь контейнер. Например, когда ConcurrentHashMap необходимо расширить диапазон сопоставления, а хеш-значение значения повторного ключа необходимо распределить по большему набору сегментов, необходимо получить все блокировки в наборе блокировок сегмента.
10.7 Избегайте горячих точек
Функция size в ConcurrentHashMap напрямую не возвращает глобальное значение счетчика, хранящееся в карте, потому что это приведет к тому, что это значение станет горячим значением (каждая операция добавления и удаления будет изменена, даже если это не один и тот же поток, она будет изменена). вызвать конкуренцию блокировок), и каждый сегмент независимо поддерживает значение счетчика своего собственного сегмента.Чтобы вычислить значение размера карты, необходимо напрямую перечислить и добавить значения счетчика сегмента.
10.8 Альтернатива эксклюзивным замкам
Использование параллельных контейнеров, блокировок чтения-записи, неизменяемых объектов, атомарных переменных
10.9 Определение загрузки ЦП
Инструментальная команда UNIX vmstat/mpstat, windows perfmom
Причины, по которым ЦП не используется полностью
- Несбалансированная нагрузка
увеличить тестовую нагрузку - Интенсивный ввод-вывод
Используйте iostat/perfmom, чтобы определить, является ли приложение интенсивным вводом-выводом, или отслеживайте уровни сетевого трафика, чтобы определить, нужно ли увеличить пропускную способность. - внешние ограничения
Используйте инструменты анализа или инструменты управления базами данных для определения внешних ограничений - блокировка конфликта
Заблокированный поток будет иметь соответствующий стек кадров в информации о дампе потока, включая «ожидание блокировки монитора…». Чем интенсивнее конкуренция за блокировку, тем чаще он будет появляться в дампе потока.
10.10 Не используйте пул объектов для параллелизма
Если поток блокируется при запросе объекта из пула потоков, затраты на блокировку будут в сотни раз больше, чем операция выделения памяти (создание нового объекта).
Кроме того, вам необходимо убедиться, что вы сбрасываете объект в правильное состояние при его повторном использовании.
11. Тестирование параллельных программ
11.1 Блокирующие тесты
Может использовать прерывание для разблокировки, Запустите тестовый поток с операцией блокировки в основном потоке.В это время тестовый поток заблокирован, тестовый поток прерывается в основном потоке, тестовый поток генерирует InterruptException, тестовый поток выполняет операцию соединения, чтобы убедиться, что тест поток завершен, а затем, когда тестовый поток .isAlive()==false означает, что тест блокировки прошел успешно
Используйте Thread.getState, чтобы проверить, может ли поток заблокироваться при условном ожидании, что ненадежно.
11.2 Тестирование безопасности
Для тестирования ошибок, вызванных гонками данных, требуется несколько потоков для выполнения операций ввода и вывода соответственно.
Главный вопрос: узнатьлегко проверяемые свойства, и эти свойства, скорее всего, не сработают в случае ошибки, если код проверки ошибок не будет искусственно ограничивать параллелизм.
Функцию вычисления контрольной суммы можно использовать для вычисления контрольной суммы элементов, поставленных в очередь и исключенных из очереди, и если они равны, код правильный. Необходимо учитывать, чувствительны ли к порядку
При тестировании вы можете использовать CyclicBarrier или CountDownLatch для равномерного запуска многопоточной тестовой программы и выполнения ее в одном и том же месте в одно и то же время, избегая асинхронной проблемы, вызванной созданием потоков.
Когда возникает исключение или бесконечный цикл, тест может никогда не закончиться.В это время тестовая программа может установить максимальное время ожидания, и она не будет выполняться, когда она устарела, и проблема будет проверена позже.
Количество тестовых потоков должно быть больше, чем количество ЦП, тогда есть потоки, работающие и выгружаемые в любое время, что увеличивает чередующееся поведение.
11.3 Тест управления ресурсами
Уничтожить ссылку на объект, когда объект не нужен
11.4 Больше чередующихся операций
- использовать
Thread.yield()
илиThread.sleep()
.(спать будет лучше) Используйте АОП для удобства - Использование порождает большее количество активных потоков, по крайней мере, больше, чем количество процессоров.
11.5 Подводные камни тестирования производительности
11.5.1 Сбор мусора
- Не выполнять сборку мусора
Убедитесь, что операции по сборке мусора не выполняются в течение всего тестового прогона.–verbose:gc
- Выполнить сборку мусора несколько раз
Полностью отразить накладные расходы на выделение памяти и сборку мусора во время выполнения.
11.5.2 Динамическая компиляция
Когда класс загружается в первый раз, JVM выполняется путем интерпретации байт-кода, и код активной области может быть скомпилирован в машинный код динамическим компилятором во время работы, и код изменяет код активной области для прямого выполнения; код также может вернуться к интерпретируемому исполнению, перекомпилировать
- Сокращение времени интерпретации/компиляции
Пусть тестовая программа работает достаточно долго - Избегайте времени интерпретации/компиляции
использовать-XX:+PrintCompilation
, когда динамическая компиляция выводит информацию, убедитесь, что динамическая компиляция выполняется до запуска теста
11.5.3 Нереалистичная выборка путей кода
Динамический компилятор может сделать некоторые специальные оптимизации для однопоточной тестовой программы, но простое включение некоторого параллелизма в реальное приложение заставит эти оптимизации исчезнуть.Сочетание однопоточного тестирования производительности с многопоточным тестированием производительности
11.5.4 Устранение бесполезного кода
точка доступа,-server
коэффициент моды-client
режим лучше,-server
Компиляторы шаблонов создают более эффективный код, и этот шаблон упрощает устранение мертвого кода за счет оптимизации.
- Избегайте оптимизации операций без чрезмерных накладных расходов
Вычислите хеш-значение поля в объекте и сравните его с произвольным значением, напримерSystem.nanoTime
Текущее значение , если равно, вывести сообщение, которое бесполезно и может быть проигнорировано
if ( f.x.hashCode() == System.nanoTime() ) {
System.out.println(" ");
}
11.6 Распространенные ошибки параллелизма
11.6.1 Запуск потока в конструкторе
Если вы запускаете поток в конструкторе, у вас могут возникнуть проблемы с подклассами, а также вызвать выход ссылки this из конструктора.
11.6.2 Ошибки в условном ожидании
При ожидании в условной очередиobject.wait
иcondition.await
Метод должен, после проверки предиката состояния,циклзвонить, и нужно держатьПравильный замок, если звонитьobject.wait
иcondition.await
метод не удерживает блокировку, или не находится в цикле, или не проверяет какой-либо предикат состояния, то обычно это ошибка
11.6.3 Блокировка во время сна или ожидания
Если вы удерживаете блокировку при вызове thread.sleep, это приведет к тому, что другие потоки не смогут выполняться в течение длительного времени, что потенциально может вызвать серьезные проблемы с живучестью.object.wait
илиcondition.await
Времядержать два замка, то это также может вызвать ту же проблему
12. Явные блокировки
Блокировка должна быть снята в блоке finally
В синхронизированной встроенной блокировке, когда возникает взаимоблокировка, единственный способ восстановить программу — перезапустить программу, а единственный способ предотвратить взаимоблокировку — избежать непоследовательного порядка блокировки при построении программы.
12.1 ReentrantLock
Особенности: синхронизированные, опрашиваемые, прерываемые операции получения блокировки, справедливая очередь, неблочная структураreentrantLock.lockInterruptibly();
Прерываемые операции получения блокировки
12.2 Блокировка опроса и временная блокировка
Предоставляет альтернативу, чтобы избежать взаимоблокировок
Если блокировка не может быть получена, она освобождает полученную блокировку, а затем снова пытается получить все блокировки.
Временная блокировка может установить ограничение по времени в зависимости от оставшегося времени.Если операция не может быть завершена в течение указанного срока, программа завершится досрочно.
12.3 Справедливость
Когда конкуренция ожесточена, причина выполнения непубличных блокировке выше, чем у исполнения общественного блокировки: существует серьезная задержка, прежде чем восстановить вешалка, и нить действительно начинает работать.
Справедливые блокировки следует использовать, когда блокировка удерживается в течение длительного времени или средний интервал времени между запросами на блокировку велик.
12.4 Synchronized VS ReentrantLock
Используйте ReentrantLock только в том случае, если встроенная блокировка не может удовлетворить ваши потребности.
Сценарии, использующие ReentrantLock: синхронизированные, опрашиваемые и прерываемые операции получения блокировки, справедливые очереди и неблочные структурированные блокировки.
12.5 Блокировки чтения-записи
Блокировки чтения-записи могут повысить скорость чтения структур данных в многопроцессорных системах, в то время как блокировки чтения-записи плохо работают в других случаях.
Когда блокировка удерживается потоком чтения, а блокировка запрашивается потоком записи, другие потоки чтения могут удерживать блокировку чтения только до тех пор, пока поток записи не завершит ее использование и не освободит блокировку записи.
Поток записи имеет более высокий приоритет, поток записи может быть понижен до потока чтения, а поток чтения не может быть повышен до потока записи, иначе это легко приведет к взаимоблокировке: если два потока чтения попытаются обновиться до блокировки записи при этом ни один из них не будет заблокирован.сбросит блокировку чтения
13. Создайте собственный инструмент синхронизации
13.1 Проблемы зависимости состояния
Некоторые операции зависят от состояния, например, элементы не могут быть удалены из пустой очереди, а для получения результата расчета незавершенной задачи необходимо дождаться, пока очередь перейдет в состояние «непустое» или задача перейдет в завершенное государство
Операции, зависящие от состояния, могут блокироваться до тех пор, пока они не смогут продолжать выполняться. Простая блокировка может быть достигнута путем опроса (цикла) и ожидания. Идея состоит в том, чтобы использовать метод цикла и повторять попытки до тех пор, пока они не будут успешными, что не является хорошей реализацией.
Использовать условную очередь на основе защелки LinkedBlockingQueue Semaphore FutureTask
13.2 Условные предикаты
Оценка цикла:
- Условный предикат может стать истинным, когда уведомляющий поток вызывает notifyAll, но снова станет ложным, когда блокировка будет повторно получена: какой-то другой поток мог получить это в то время, когда поток был пробужден, чтобы дождаться повторного получения блокировки блокировки и изменить объект. флаг
- Условный предикат никогда не был истинным с момента вызова ожидания, возможно, другой поток вызвал notifyAll, потому что другой условный предикат был истинным.
public synchronized void put(V v) throws InterruptedException {
while (isFull())
wait();
doPut(v);
notifyAll();
}
13.3 Уведомления
Всякий раз, ожидая условия, обязательно уведомляйте каким-либо образом, когда предикат условия становится истинным.
Например, в условном предикате всякий раз, когда ставится элемент, выполняется notifyAll (после его установки выходим как можно быстрее, методы notify и notifyAll не снимают блокировку, а только уведомляют поток, находящийся в состоянии ожидания, для подготовки для получения блокировки) и уведомить поток, ожидающий пробуждения
Обратите внимание, что объекты, которые заблокировали;
- notifyAll
Использование notifyAll вместо notify позволяет избежать нескольких типов условий.При ожидании блокировки поток на блокировке, который вы хотите разбудить, не пробуждается, что позволяет избежать проблемы потери сигнала. - notify
Использование уведомления должно одновременно соответствовать двум следующим условиям.
- Все ожидающие потоки одного типа
С условной очередью связан только один условный предикат, и каждый поток будет делать то же самое после выхода из ожидания. - один в один из
При каждом уведомлении об условной переменной вы можете только разбудить поток для выполнения.
В очереди, если она не пуста, ее можно взять, а если она не заполнена, ее можно только поставить, то выполняется только 2, а нить, ожидающая в очереди условий, имеет два связанных условных предиката
13.4 Проблемы безопасности подкласса
Для классов, зависящих от состояния, либо полностью раскрывайте протоколы, такие как ожидание и уведомление, для подклассов и пишите формальные документы, либо полностью запрещайте подклассам участвовать в таких процессах, как ожидание и уведомление.
13.5 Соглашение об импорте и соглашение об экспорте
Используется для описания правильного использования методов ожидания и уведомления.
Для каждой операции, зависящей от состояния, и каждой операции, изменяющей состояние других операций, должны быть определены протокол входа и протокол выхода.
Протокол входа является условным предикатом операции, а протокол выхода состоит из проверки всех переменных состояния, измененных операцией, и подтверждения того, делают ли они какой-либо другой условный предикат истинным, и, если это так, уведомления соответствующей условной очереди.
13.6 Условия отображения
- Создайте Условия могут быть созданы с помощью связанных блокировок, и каждая блокировка может иметь любое количество объектов Condition.
private final Condition notEmpty = lock.newCondition();
- справедливость
Объекты условия наследуют справедливость объектов блокировки.Для справедливых блокировок потоки будут освобождены от ожидания условия в порядке FIFO. - переопределение метода
В объекте Condition методы wait, notify и notifyAll соответствуют await, signal и signalAll соответственно — убедитесь, что используется правильная версия. - сцены, которые будут использоваться
Используйте справедливые операции с очередью или несколько операций на каждом замке
набор ожидающих потоков
13.7 AQS
AbstractQueuedSynchronizer (AQS) — это платформа для создания блокировок и синхронизаторов. Многие синхронизаторы могут быть легко и эффективно созданы с помощью AQS, например ReentrantLock, Semaphore, FutureTask, Защелка
14. Атомарные переменные и неблокирующие механизмы синхронизации
14.1 Параллельный CAS
CAS содержит 3 операнда: ячейку памяти V для чтения и записи, значение A для сравнения и новое значение B для записи.
CAS атомарно обновляет значение V новым значением B тогда и только тогда, когда значение V равно A, иначе ничего не делает.
CAS — оптимистичный метод, он ожидает, что операция обновления будет успешной, и если другой поток обновит переменную после последней проверки, то CAS может обнаружить ошибку и не выполнять операцию обновления для этого обновления.
Поскольку CAS обнаруживает помехи от других потоков, атомарные последовательности чтения-изменения-записи могут быть реализованы даже без блокировок.
Эмпирическое правило: на большинстве процессоров накладные расходы на «путь сокращенного кода» для получения и освобождения неоспариваемой блокировки примерно вдвое превышают накладные расходы CAS.
14.2 Классы атомарных переменных
12 классов атомарных переменных, разделенных на 4 группы
- Скалярный класс
Поддержка CAS, AtomicInteger, AtomicBoolean, AtomicLong, AtomicReference - класс массива
Поддерживает только элементы в классе атомарного массива Integer, Long и Reference и может выполнять атомарное обновление. - Класс обновления
- соответствующий переменный класс
14.3 Неблокирующие алгоритмы
Неблокирующий алгоритм: сбой или приостановка одного потока не приводит к сбою или приостановке других потоков.
Алгоритм без блокировки: на каждом шаге алгоритма есть поток, который может продолжать выполняться
Хитрость построения неблокирующих алгоритмов заключается в том, чтобы сузить область выполнения атомарных модификаций до одной переменной.
14.4 Проблемы АВА
Во время выполнения алгоритма значение изменяется, а затем возвращается к исходному значению, и в оценке CAS возникает ошибка.
Решение: вместо обновления значения ссылки обновите два значения, включая ссылку и номер версии.
15. Модель памяти Java
15.1 Happens-before
Если между двумя операциями отсутствует связь Happens-before, то JVM может произвольно изменить порядок
правила для volatile-переменных: запись в volatile-переменные должна выполняться перед чтением переменной
Правила запуска потока: вызов Thread.start() в потоке должен быть выполнен до того, как в этом потоке будут выполнены какие-либо операции.
Переходный: A завершается раньше B, B завершается раньше C, затем A завершается раньше C.
15.2 Небезопасный выпуск
Проблемы с переупорядочиванием могут возникнуть, когда отношение «Происходит до» отсутствует, поэтому публикация объекта без достаточной синхронизации может привести к тому, что другой поток увидит объект, который только частично создан.
16. Вопросы и ответы
16.1 Разница между синхронизированными коллекциями Java и параллельными коллекциями
Синхронизированные классы коллекций, Hashtable и Vector также имеют синхронизированные классы-оболочки коллекций.Collections.synchronizedMap()
иCollections.synchronizedList()
Предоставляет базовую условную поточно-ориентированную реализацию Map и List.
Параллельные коллекции, такие как ConcurrentHashMap, не только обеспечивают безопасность потоков, но и улучшают масштабируемость с помощью современных методов, таких как разделение блокировок и внутреннее разделение.
16.2 Как избежать взаимоблокировок
Возникновение взаимоблокировки должно соответствовать следующим четырем условиям:
- Условие взаимного исключения: ресурс может использоваться только одним процессом одновременно.
- Условие запроса и удержания: когда процесс блокируется запросом ресурса, он будет удерживать полученный ресурс.
- Условие отсутствия лишения: ресурсы, полученные процессом, не могут быть принудительно лишены до тех пор, пока ресурсы не будут израсходованы.
- Условие циклического ожидания: между несколькими процессами формируются отношения ресурсов циклического ожидания. Самый простой способ — предотвратить циклическое ожидание, установить флаги и отсортировать все ресурсы в системе, а также установить, что все процессы, обращающиеся за ресурсами, должны работать в определенном порядке (по возрастанию или по убыванию), чтобы избежать взаимоблокировки.
16.3 Как создавать неизменяемые объекты в Java
- Инициализировать все члены конструктором
- Не предоставляйте методы установки для переменных
- Объявите всех участников как закрытых, чтобы прямой доступ к этим членам был запрещен.
- В методе получения верните клонированный объект
- поле является окончательным
16.4 В чем разница между volatile и атомарными переменными
Изменчивая переменная может обеспечить упреждающую связь, то есть операция записи произойдет до последующей операции чтения, но не гарантирует атомарности. Например, измените переменную count с помощью volatile, затемcount++
операции не атомарны
Атомарный метод, предоставляемый классом AtomicInteger, может сделать эту операцию атомарной.
16.5 Почему нельзя синхронизировать конструктор Java
хорошее объяснение не найдено
16.6 Метод Collections.synchronized()
Инкапсулировать методы синхронизации для всех операций
public int size() {
synchronized (mutex) {return m.size();}
}