Вступление
Синхронизация потоков Java и параллелизм являются наиболее обсуждаемыми темами на различных этапах проектирования сложного приложения. Есть много аспектов потоков, методов синхронизации для достижения большого параллелизма в приложении. Эволюция процессоров за последние годы (многоядерные процессоры, регистры, кэш-память и оперативная память (RAM)) привела к тому, что разработчики обычно не замечают таких областей, как контекст потока, переключение контекста, видимость переменных, память JVM модель против модели памяти процессора.
В этой серии мы обсудим различные аспекты модели памяти Java , в том числе ее влияние на контексты потоков, методы синхронизации в Java для достижения параллелизма, условия гонки и т. Д. В этой статье мы сосредоточимся на понятиях потоков, синхронизации методы и модели памяти как Java, так и нашего процессора.
рекапитуляция
Давайте кратко остановимся на некоторых терминологиях и понятиях, связанных с потоками, прежде чем углубляться в тему потоков и синхронизации.
- Lock — блокировка — это механизм синхронизации потоков.
- Каждый объект в Java имеет встроенную блокировку, связанную с ним. Потоки используют монитор объекта для блокировки или разблокировки. Блокировка может рассматриваться как данные, которые логически являются частью заголовка объекта в памяти. Посмотрите ReentrantLock для расширенных возможностей, которые монитор не может достигнуть.
- Каждый объект в Java имеет методы синхронизации
wait()
иnotify()
[такжеnotifyAll()
]. Любой поток, вызывающий эти методы, получает блокировку этого объекта, используя свой монитор. Это должно быть вызвано с использованием синхронизированного ключевого слова else, и IllegealMonitorStateException будет брошен. - Сигнал является способом уведомить нить , что она должна продолжить ее выполнение. Это достигается с помощью методов объекта
wait()
,notify()
, иnotifyAll()
. Вызов методов,notify()
илиnotifyAll()
, выбирает поток (ы), чтобы разбудить то, что находится в фоновом режиме (путем вызова метода,wait()
). - Пропущенный сигнал — Методы
notify()
иnotifyAll()
не спасают вызовы методов, и они не знают , еслиwait()
были названы или не другими потоками. Если поток вызываетnotify()
до того, как будет вызван потокwait()
, сигнал которого будет пропущен ожидающим потоком. Это может заставить поток ждать бесконечно, потому что он пропустил сигнал. Runnable
это функциональный интерфейс, который может быть реализован любым классом в приложении, чтобы поток мог его выполнить.volatile
другое ключевое слово, назначаемое переменным, чтобы сделать классы потокобезопасными. Чтобы понять использование этого ключевого слова, необходимо понять архитектуру процессора и модель памяти JVM. Мы рассмотрим это позже.ThreadLocal
позволяет создавать переменные, которые могут быть прочитаны / записаны только потоком владельца. Это используется, чтобы сделать код потокобезопасным.- Пул потоков — это набор потоков, в которых потоки будут выполнять задачи. Создание и поддержка потоков очень контролируется сервисом. В Java пул потоков представлен экземпляром ExecutorService .
ThreadGroup
Этот класс предоставляет механизм для сбора нескольких потоков в один объект и позволяет нам манипулировать / контролировать эти потоки одновременно.- Поток демона — эти потоки работают в фоновом режиме. Хорошим примером потока демона является сборщик мусора Java. JVM не ждет, пока поток демона не завершит свое выполнение (в то время как JVM ожидает, когда потоки, не являющиеся демонами, или пользовательские потоки завершат свое выполнение).
- синхронизированный — ключевое слово для управления выполнением кода одним потоком, когда различные потоки должны выполнять один и тот же фрагмент функциональности в параллельном режиме. Это ключевое слово может применяться для методов и блоков кода для достижения безопасности потока. Обратите внимание, что для этого ключевого слова нет тайм-аута, поэтому существует вероятность возникновения тупиковых ситуаций.
- Dead-lock — ситуация, когда один или несколько потоков ожидают снятия блокировки объекта другим потоком. Возможный сценарий, который вызывает мертвые блокировки, может быть в том случае, когда потоки ждут друг друга, чтобы снять блокировку!
- Ложные пробуждения — По необъяснимым причинам потоки могут проснуться, даже если
notify()
иnotifyAll()
не были вызваны. Это ложное пробуждение. Чтобы покрыть эту проблему, поток пробуждал вращения вокруг условия в блокировке вращения.
Джава
xxxxxxxxxx
1
public synchronized doWait() {
2
while(!wasSignalled) { // spin-lock check to avoid spurious wake up calls
3
wait();
4
}
5
// do something
6
}
7
8
public synchronized doNotify() {
9
wasSignalled = true;
10
notify();
11
}
Нить Голод
Истощение потока происходит, когда потоку не предоставляется процессорное время, потому что другие потоки загружают все это. (Например, потоки, ожидающие объекта (который вызвал wait()
), остаются в ожидании бесконечно, потому что другие потоки постоянно пробуждаются вместо этого (путем вызова notify()
).
Чтобы смягчить такие условия, мы можем установить приоритет для потока, используя Thread.setPriority(int priority)
метод. Параметр приоритета должен находиться в пределах установленного диапазона от Thread.MIN_PRIORITY
до Thread.MAX_PRIORITY
. Проверьте официальную документацию по теме для получения дополнительной информации о приоритете потока.
Вам также может понравиться:
Учебник по Java Thread: создание потоков и многопоточность в Java
Интерфейс блокировки против синхронизированного ключевого слова
- Тайм-аут в синхронизированном блоке или методе невозможен. Это может закончиться в тех случаях, когда приложение кажется зависшим, в тупике и т. Д. Синхронизированный блок должен содержаться только в одном методе.
- Экземпляр интерфейса Lock может иметь свои вызовы
lock()
иunlock()
в отдельных методах. Кроме того, у замков также может быть время ожидания. Это два больших преимущества по сравнению с синхронизированным ключевым словом.
Ниже приведена простая реализация пользовательского класса блокировки с использованием native wait()
и notify()
методов. Пожалуйста , прочтите комментарии в блоке кода ниже, что дает более подробную информацию о wait()
и notify()
методах.
Джава
xxxxxxxxxx
1
class CustomLock {
2
private boolean isLocked = false;
4
public synchronized void lock()
6
throws InterruptedException {
7
8
isLocked = true;
9
while(isLocked) {
10
// calling thread releases the lock it holds on the monitor
11
// object. Multiple threads can call wait() as the monitor is released.
12
wait();
13
}
14
}
15
public synchronized void unlock() {
17
isLocked = false;
18
notify();
19
// only after the lock is released in this block, the wait() block
20
// above can re-acquire the lock on this object's monitor.
21
}
22
}
Выполнение потока
Есть два способа выполнения потока в Java. Они есть:
- Расширение класса Thread и вызов
start()
метода. (Это не предпочтительный способ подклассифицировать класс из Thread, поскольку он уменьшает возможности добавления большей функциональности класса.) - Реализация интерфейса
Runnable
илиCallable
. Оба интерфейса являются функциональными интерфейсами , что означает, что для обоих определен только один абстрактный метод. (Предпочтительный подход как класс может быть расширен в будущем за счет реализации других интерфейсов.)
Runnable интерфейс
Это фундаментальный интерфейс, используемый для выполнения определенной задачи потоком. Этот интерфейс описывает только один метод, вызываемый run()
с void
типом возврата. Реализуйте этот интерфейс, если какая-либо функциональность должна быть выполнена в потоке, но нет ожидаемого возвращаемого типа. По сути, результат потока или любое исключение или ошибка не могут быть восстановлены в случае сбоя.
Вызываемый интерфейс
Этот интерфейс используется для выполнения определенной задачи потоком, а также для получения результата выполнения. Этот интерфейс следует за дженериками. Он описывает только один вызванный метод call()
с возвращаемым типом для класса, реализующего этот интерфейс. Реализуйте этот интерфейс, если какая-либо функциональность должна быть выполнена в потоке, и результат выполнения должен быть зафиксирован.
Методы синхронизации
Как описано выше, поток может быть синхронизирован с использованием synchronized
ключевого слова или с помощью экземпляра блокировки. Фундаментальной реализацией интерфейса Lock является ReentrantLock
класс. Также существуют варианты интерфейсов Lock для операций чтения / записи.
Это помогает приложению достичь более высокого параллелизма, когда потоки пытаются читать или писать в ресурс. Эта реализация называется ReentrantReadWriteLock
. Основные различия между двумя классами показаны ниже:
Класс ReentrantLock | Класс ReentrantReadWriteLock |
Дайте доступ только к 1 потоку для чтения или записи, но не для обоих. | Предоставляет доступ к нескольким / всем потокам одновременно, если оператор читает ресурс. Только один поток за раз будет предоставлен доступ, если операция записи. |
Блокирует ресурс для операций чтения и записи, делая операции взаимоисключающими. | Имеет отдельные блокировки для операций чтения и записи. |
Снижает производительность, поскольку ресурс заблокирован даже для операций чтения. | Лучше с точки зрения производительности, поскольку дает одновременный доступ ко всем потокам, которые хотят выполнять операции чтения. |
См. ReentrantReadWriteLock
Пример ниже, чтобы узнать, как добиться одновременного чтения ресурса, позволяя обновлять ресурс только одному потоку.
Примечание . Ресурсом могут быть любые данные, к которым разные потоки приложения пытаются получить доступ одновременно.
Джава
xxxxxxxxxx
1
public class ConcurrentReadWriteResourceExample {
2
3
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
4
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
5
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
6
7
private void readResource() {
8
readLock.lock();
9
// read the resource from a file, cache, database or from memory
10
// this block can be accessed by 'N' threads concurrently for reading
11
readLock.unlock();
12
}
13
14
private void writeResource(String value) {
15
writeLock.lock();
16
// write or update value to either a file, cache, database or from memory
17
// this block can be accessed by at-most '1' thread at a time for writing
18
writeLock.unlock();
19
}
20
}
Создайте один экземпляр вышеупомянутого класса и передайте его нескольким потокам; будет обработано следующее:
- Либо
readLock
используется NwriteLock
-нитью, либо используется не более чем одним потоком. - Никогда ни чтение, ни запись не происходят одновременно.
Модель памяти Java и процессор
Замечание о моделях памяти Java и ЦП поможет нам лучше понять, как объекты и переменные хранятся в Java Heap / Thread-stack по сравнению с фактической памятью ЦП. Современный ЦП состоит из регистров, которые действуют как непосредственная память самого процессора, кэш-памяти - каждый процессор имеет уровень кэш-памяти для хранения данных и, наконец, ОЗУ или основную память, где присутствуют данные приложения.
На аппаратном или центральном процессоре как стек потоков, так и куча находятся в основной памяти. Части стека и кучи потоков могут иногда присутствовать в кэше ЦП и во внутренних регистрах. Ниже перечислены проблемы, которые могут возникнуть из-за вышеуказанной архитектуры:
- Видимость обновлений (записей) потоков в общие переменные видны не сразу всем потокам, обращающимся к переменным.
- Условия гонки при чтении, проверке и обновлении данных общих переменных.
Изменчивое ключевое слово
Ключевое слово volatile было введено в Java 5 и широко используется для обеспечения безопасности потоков. Это ключевое слово можно использовать как для примитивов, так и для объектов. Использование volatile
ключевого слова для переменной гарантирует, что данная переменная будет считана непосредственно из основной памяти и записана обратно в основную память при обновлении.
В DZone есть очень хорошая статья об изменчивом ключевом слове. Пожалуйста, обратитесь по этой ссылке, чтобы лучше понять эту концепцию и лучше всего ее использовать.
ThreadLocal Class
Последняя тема в синхронизации потоков - после того, как Lock - это класс Java ThreadLocal
. Этот класс позволяет создавать переменные, которые могут быть прочитаны / записаны только одним потоком. Это дает нам простой способ достижения безопасности потока путем определения локальной переменной потока. ThreadLocal
имеет значительное использование в пулах потоков или ExecutorService
, так что каждый поток использует свой собственный экземпляр какого-либо ресурса или объекта.
Например, для каждого потока требуется отдельное соединение с базой данных или отдельный счетчик. В таких случаях ThreadLocal
помогает. Это также используется в приложениях Spring Boot, где пользовательский контекст устанавливается для каждого входящего вызова (Spring Security), и пользовательский контекст будет совместно использоваться в потоке потока через различные экземпляры. Используйте ThreadLocal
для следующих случаев:
- Удержание нити
- Данные по потокам для производительности.
- В контексте потока.
Джава
xxxxxxxxxx
1
/**
2
* This is a demo class only. The ThreadLocal snippet can be applied
3
* to any number of threads and you can see that each thread gets it's
4
* own instance of the ThreadLocal. This achieves thread safety.
5
*/
6
public class ThreadLocalDemo {
7
8
public static void main(String...args) {
9
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
10
protected String initialValue() {
11
return "Hello World!";
12
}
13
};
14
15
// below line prints "Hello World!"
16
System.out.println(threadLocal.get());
17
18
// below line sets new data into ThreadLocal instance
19
threadLocal.set("Good bye!!!");
20
21
// below line prints "Good bye!!!"
22
System.out.println(threadLocal.get());
23
24
// below line removes the previously set message
25
threadLocal.remove();
26
27
// below line prints "Hello World!" as the initial value will be
28
// applied again
29
System.out.println(threadLocal.get());
30
}
31
}
Вот и все о синхронизации потоков и связанных с ними концепций. Параллелизм будет рассмотрен во второй части этой статьи.