В параллельном программировании часто используются блокировки.Сегодня давайте рассмотрим механизм блокировки в Java: синхронизированный и блокирующий.
1. Типы замков
Существует много типов блокировок, в том числе: спин-блокировки, другие типы спин-блокировок, блокирующие блокировки, повторные блокировки, блокировки чтения-записи, блокировки мьютекса, пессимистические блокировки, оптимистические блокировки, честные блокировки, повторные блокировки и т. д., остальные нет в списке. Здесь мы сосредоточимся на следующих типах: повторные блокировки, блокировки чтения-записи, прерываемые блокировки и честные блокировки.
1.1 Реентерабельные блокировки
Если блокировка является повторно используемой, она называется повторно используемой блокировкой. И синхронизированный, и ReentrantLock являются реентерабельными блокировками, и реентерабельность, на мой взгляд, фактически указывает на механизм распределения блокировок: выделение на основе потоков, а не выделение на основе вызова метода. Например, когда поток выполняет синхронизированный метод метода 1, в методе 1 будет вызван другой синхронизированный метод, метод 2. В это время потоку не нужно снова запрашивать блокировку, но он может напрямую выполнить метод метода 2.
1.2 Блокировка чтения-записи
Блокировка чтения-записи разделяет доступ к ресурсу на две блокировки, такие как файл, блокировка чтения и блокировка записи. Именно из-за блокировки чтения-записи операции чтения между несколькими потоками не будут конфликтовать.ReadWriteLock
Это блокировка чтения-записи, представляющая собой интерфейс, и ReentrantReadWriteLock реализует этот интерфейс. Блокировку чтения можно получить с помощью readLock(), а блокировку записи можно получить с помощью writeLock().
1.3 прерываемый замок
Прерываемые блокировки — это блокировки, которые можно прервать. В Java synchronized не является прерываемой блокировкой, а Lock — прерываемой блокировкой. Если поток A выполняет код в блокировке, а другой поток B ожидает получения блокировки, это может быть связано с тем, что время ожидания слишком велико, поток B не хочет ждать и хочет сначала заняться другими делами. , мы можем позволить ему прервать себя или в другом Прервать его в потоке, это прерываемая блокировка.
Метод lockInterruptably() в интерфейсе блокировки отражает возможность прерывания блокировки.
1.4 Честный замок
Честные блокировки пытаются получить блокировки в том порядке, в котором они запрашиваются. В то же время несколько потоков ожидают блокировки. Когда блокировка снимается, блокировку получает поток, ожидавший дольше всех (поток, запросивший первым). Это справедливая блокировка.
Несправедливая блокировка означает, что нет гарантии, что получение блокировок выполняется в том порядке, в котором запрашиваются блокировки, что может привести к тому, что один или несколько потоков никогда не получат блокировку.
synchronized
является несправедливой блокировкой, она не может гарантировать порядок, в котором ожидающие потоки получают блокировку. заReentrantLock
иReentrantReadWriteLock
, что по умолчанию является несправедливой блокировкой, но может быть установлено справедливой блокировкой.
2. Использование синхронизации и блокировки
2.1 synchronized
Synchronized — это ключевое слово Java. Когда оно используется для изменения метода или блока кода, оно может гарантировать, что не более чем один поток выполняет код одновременно. Четыре использования кратко резюмируются следующим образом.
2.1.1 Кодовые блоки
Используется для блока кода, синхронизированного, за которым следуют круглые скобки, круглые скобки являются переменными, и только один поток входит в блок за раз.
public int synMethod(int m){
synchronized(m) {
//...
}
}
2.1.2 Когда объявлен метод
Используется при объявлении метода после оператора области действия и перед объявлением возвращаемого типа. То есть только один поток может войти в метод за раз, а другие потоки могут ждать в очереди только в том случае, если они хотят вызвать метод в это время.
public synchronized void synMethod() {
//...
}
2.1.3 Объект в скобках после синхронизации
В круглых скобках есть объект после синхронизации, и в это время поток получает блокировку объекта.
public void test() {
synchronized (this) {
//...
}
}
2.1.4 Классы указаны в круглых скобках после синхронизированного
Класс в круглых скобках после синхронизируется.Если поток входит, все операции потока в классе не могут быть выполнены, включая статические переменные и статические методы.Для синхронизации блоков кода, содержащих статические методы и статические переменные, этот метод обычно использовал.
2.2 Lock
Основные связанные классы и интерфейсы интерфейса Lock следующие.
ReadWriteLock — это интерфейс блокировки чтения-записи, а его класс реализации — ReetrantReadWriteLock. ReetrantLock реализует интерфейс блокировки.
2.2.1 Lock
Блокировка имеет следующие методы:
public interface Lock {
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
-
lock: используется для получения блокировки, если блокировка получена другим потоком, он находится в состоянии ожидания. Если используется Lock, блокировка должна быть снята активно, и блокировка не будет снята автоматически при возникновении исключения. Поэтому, как правило, использование Lock должно выполняться в блоке try{}catch{}, а операция снятия блокировки должна выполняться в блоке finally, чтобы гарантировать снятие блокировки и предотвратить возникновение тупика.
-
lockInterruptably: при получении блокировки с помощью этого метода, если поток ожидает получения блокировки, поток может ответить на прерывание, то есть на состояние ожидания потока прерывания.
-
tryLock: метод tryLock имеет возвращаемое значение, что означает, что он используется для попытки получения блокировки.Если получение прошло успешно, он возвращает true, а если получение не удалось (т. е. блокировка была получена другим потоком ), он возвращает false, а это значит, что этот метод все равно вернет сразу. Он не будет ждать там вечно, если не сможет получить замок.
-
tryLock(long, TimeUnit): Аналогичен tryLock, за исключением того, что есть время ожидания.Блокировка устанавливается в течение времени ожидания и возвращает true, а timeout возвращает false.
-
разблокировка: снимите блокировку, обязательно снимите ее в блоке finally
2.2.2 ReetrantLock
Реализован интерфейс блокировки, повторные блокировки и внутренне определенные справедливые и нечестные блокировки. По умолчанию используется несправедливая блокировка:
public ReentrantLock() {
sync = new NonfairSync();
}
Честную блокировку можно установить вручную:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.2.3 ReadWriteLock
public interface ReadWriteLock {
Lock readLock(); //获取读锁
Lock writeLock(); //获取写锁
}
Один для получения блокировки чтения и один для получения блокировки записи. Другими словами, операции чтения и записи файла разделены и разделены на две блокировки, которые должны быть назначены потокам, так что несколько потоков могут выполнять операции чтения одновременно. ReentrantReadWirteLock реализует интерфейс ReadWirteLock, но не реализует интерфейс Lock. Но учтите, что:
Если поток уже занял блокировку чтения, если другие потоки хотят применить блокировку записи в это время, поток, применяющий блокировку записи, будет ждать снятия блокировки чтения.
Если поток уже занял блокировку записи, если в это время другие потоки запрашивают блокировку записи или блокировку чтения, примененный поток будет ждать снятия блокировки записи.
2.2.4 ReetrantReadWriteLock
ReetrantReadWriteLock также поддерживает выбор справедливости, повторный вход и ослабление блокировки.
public class RWLock {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
static Lock r = rwLock.readLock();
static Lock w = rwLock.writeLock();
//读
public static final Object get(String key){
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
//写
public static final Object put(String key, Object value){
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
}
Просто установите блокировку чтения для операций чтения и блокировку записи для операций записи. Когда блокировка записи установлена, последующие операции чтения и записи блокируются.После снятия блокировки записи все операции продолжают выполняться.
3. Сравнение двух замков
3.1 Разница между синхронизацией и блокировкой
- Блокировка — это интерфейс, а синхронизация — это ключевое слово в Java, а синхронизация — это встроенная языковая реализация;
- Когда возникает исключение, synchronized автоматически освобождает блокировку, занятую потоком, поэтому это не вызовет феномен взаимоблокировки; а когда возникает исключение, если Lock не снимает активно блокировку с помощью unLock(), это, вероятно, вызовет явление взаимоблокировки, поэтому при использовании блокировки вам необходимо снять блокировку в блоке finally;
- Блокировка может заставить поток, ожидающий блокировки, реагировать на прерывание, а синхронизированный - нет.При использовании синхронизированного ожидающий поток будет ждать вечно и не сможет ответить на прерывание;
- С помощью Lock вы можете узнать, успешно ли получена блокировка, но не синхронизировано.
- Блокировка может повысить эффективность операций чтения несколькими потоками. (Разделение чтения и записи может быть достигнуто с помощью блокировки чтения и записи)
- С точки зрения производительности, когда конкуренция за ресурсы не является жесткой, производительность Lock немного хуже, чем у synchronized (компиляторы обычно максимально оптимизируют synchronized). Но когда синхронизация очень интенсивная, производительность synchronized может упасть сразу в десятки раз. И ReentrantLock поддерживает нормальную работу.
3.2 Сравнение производительности
Ниже приведен тест производительности для синхронизированных и блокированных, соответственно открывающих 10 потоков, каждый поток считает до 1 000 000 и подсчитывает время, затраченное на синхронизацию двух блокировок. Примеры этого также можно найти в Интернете.
public class TestAtomicIntegerLock {
private static int synValue;
public static void main(String[] args) {
int threadNum = 10;
int maxValue = 1000000;
testSync(threadNum, maxValue);
testLocck(threadNum, maxValue);
}
//test synchronized
public static void testSync(int threadNum, int maxValue) {
Thread[] t = new Thread[threadNum];
Long begin = System.nanoTime();
for (int i = 0; i < threadNum; i++) {
Lock locks = new ReentrantLock();
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
locks.lock();
try {
synValue++;
} finally {
locks.unlock();
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main线程等待前面开启的所有线程结束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用lock所花费的时间为:" + (System.nanoTime() - begin));
}
// test Lock
public static void testLocck(int threadNum, int maxValue) {
int[] lock = new int[0];
Long begin = System.nanoTime();
Thread[] t = new Thread[threadNum];
for (int i = 0; i < threadNum; i++) {
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
synchronized(lock) {
++synValue;
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main线程等待前面开启的所有线程结束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用synchronized所花费的时间为:" + (System.nanoTime() - begin));
}
}
Разница в результатах тестов вполне очевидна, и производительность Lock значительно выше, чем у синхронизированных. Этот тест основан на JDK1.8.
使用lock所花费的时间为:436667997
使用synchronized所花费的时间为:616882878
В JDK1.5 синхронизация неэффективна с точки зрения производительности. Поскольку это тяжеловесная операция, ее наибольшее влияние на производительность оказывает реализация блокировки.Операции приостановки и возобновления потоков необходимо перевести в режим ядра для завершения, эти операции создают большую нагрузку на систему. По сравнению с использованием объекта Lock, предоставляемого Java, производительность выше. В многопоточной среде пропускная способность синхронизированных очень серьезно падает, в то время как ReentrankLock может в основном поддерживать относительно стабильный уровень.
В JDK1.6 произошли изменения: для синхронизации добавлено множество мер по оптимизации, в том числе адаптивный спин, устранение блокировки, огрубление блокировки, облегченная блокировка, предвзятая блокировка и так далее. В результате производительность synchronize на JDK1.6 не хуже, чем у Lock. Официальный также заявил, что они также больше поддерживают синхронизацию, и в будущих версиях еще есть возможности для оптимизации, поэтому рекомендуется отдавать приоритет использованию синхронизированного для синхронизации, когда синхронизированный может соответствовать требованиям.
4. Резюме
В этой статье в основном подробно объясняется синхронизированный механизм блокировки и блокировка в параллельном программировании. Синхронизация реализована на основе JVM, встроенной блокировки, и каждый объект в Java можно использовать в качестве блокировки. Для синхронизированных методов блокировкой является текущий объект экземпляра. Для статических синхронизированных методов блокировкой является объект класса текущего объекта. Для блоков синхронизированных методов блокировка — это объект, настроенный в скобках «Синхронизированный». Блокировка основана на блокировке, реализованной на уровне языка. Блокировку блокировки можно прерывать, а также поддерживаются временные блокировки. Блокировка может повысить эффективность операций чтения несколькими потоками. Для сравнения известно, что эффективность блокировки значительно выше, чем у ключевого слова synchronized.Как правило, блокировка, а не синхронизация предпочтительнее для проектирования структуры данных или разработки фреймворка.