состояние гонки
В книге «Operating System Essence and Design Principles» состояние гонки определяется следующим образом: несколько процессов или потоков читают и записывают определенные элементы данных одновременно, в результате чего конечный результат зависит от порядка выполнения инструкций в этих процессах. .
Иными словами, в условиях гонки результат вычисления изменяется с детерминированного состояния при работе с одним потоком на неопределенное состояние при многопоточности. В чем причина этой неопределенности? Давайте обсудим это ниже.
При использовании только одного потока весь код выполняется последовательно в заданном порядке. Например, мы оперируем определенной областью памяти и модифицируем определенную переменную, и полученный результат согласуется с логикой кода.
Но когда несколько потоков выполняются одновременно, ситуация становится намного более хлопотной. Так называемое одновременное выполнение, то есть параллелизм, реализуется путем квантования времени процессора, то есть каждый поток выполняет квант времени по очереди. В этом случае поток может не выполнить все операции, когда квант времени закончится, и процессор переключится на другие потоки для выполнения в середине.
Хуже всего то, что если прерванный поток является рабочим ресурсом А, а коммутируемый поток также является рабочим ресурсом А, то при повторном выполнении процессором прерванного потока ресурс А может быть модифицирован до неузнаваемости, и получить результат естественно проблематично .
Давайте разберемся с условиями гонки на примере.
/**
* @author: Wray Zheng
* @date: 2018-02-01
* @description: An example of multi-thread
*/
public class MyThread implements Runnable {
private static String globalBuffer = "";
private String m_msg;
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread("A"), "Thread-A");
Thread t2 = new Thread(new MyThread("B"), "Thread-B");
t1.start();
t2.start();
}
public MyThread(String msg) {
m_msg = msg;
}
public static void print(String msg) {
globalBuffer = msg;
System.out.println(Thread.currentThread().getName() + ": " + globalBuffer);
}
@Override
public void run() {
try {
while (true) {
print(m_msg);
Thread.sleep(500);
}
} catch(Exception e) {}
}
}
Ключевыми элементами примера программы являются глобальная переменная globalBuffer и функция печати print. Функция печати очень проста, она сначала присваивает параметр globalBuffer, а затем распечатывает его.
Согласно логике кода, результатом вывода должно быть то, что поток A печатает символ A, а поток B печатает символ B. Но результат таков:
Thread-B: A
Thread-A: A
Thread-B: B
Thread-A: A
Thread-A: B
Thread-B: B
Thread-A: A
Thread-B: A
Причина ошибки вывода в том, что обоим потокам A и B нужно читать и писать в общий ресурс globalBuffer, а процесс чтения и записи, то есть функция печати, не является атомарной операцией.
Из-за характеристик параллелизма поток A может быть прерван после выполнения первого оператора присваивания функции печати, и вместо этого процессор выполнит функцию печати потока B. При возврате к потоку A для повторного выполнения функции печати globalBuffer в это время был изменен потоком B, поэтому выводится неверный результат.
взаимоисключающий
Чтобы решить проблемы, вызванные условиями гонки, мы можем заблокировать ресурсы. Ресурс, который несколько потоков читают и записывают вместе, называется общим ресурсом, также называемым общим ресурсом.критический ресурс. Область кода, задействованная в манипулировании критическими ресурсами, называетсякритическая секция(Критическая секция). При этом в критическую секцию может войти только один поток. Мы называем эту ситуацию взаимным исключением, то есть нескольким потокам не разрешается работать с общим ресурсом одновременно.
Как критические секции достигают взаимного исключения? Продолжаем анализ.
Прежде чем войти в критическую зону, нам нужно получить мьютекс. Если поток уже использует ресурсы, нам нужно подождать, пока другой поток не вернет мьютекс.
После работы с общим ресурсом, то есть при выходе из критической секции, мьютекс необходимо вернуть, чтобы другие потоки, ожидающие использования ресурса, могли войти в критическую секцию.
Пример псевдокода:
wait(lock); //获得互斥锁
{
临界区,操作共享资源
}
signal(lock); //归还互斥锁
В Java вы можете использовать ReentrantLock для блокировки критической секции, чтобы предотвратить одновременный вход нескольких потоков в критическую секцию:
private static Lock bufferLock = new ReentrantLock();
public static void print(String msg) {
bufferLock.lock();
//临界区,操作临界资源 globalBuffer
bufferLock.unlock();
}
Здесь нам нужно только использовать lock() для блокировки перед критическим разделом и использовать unlock() для разблокировки после критического раздела.java.util.concurrent помогает нам реализовать работу по оценке состояния блокировки перед критическим разделом и будет решите сами заблокировать или войти в критическую секцию.
синхронизированное ключевое слово
Java предоставляет нам более простой способ добиться взаимного исключения критических секций.
Например, мы можем добавить синхронизированные ключевые слова для функций общих ресурсов:
public synchronized void myFunction() {
//操作共享资源 A
}
Таким образом, можно гарантировать, что в большинстве один поток одновременно выполняется функция. Если ресурс a прочитан и записывается только в этой функции, может быть гарантировано, что ресурс A не будет читаться и записан несколькими потоками одновременно.
Однако, если общий ресурс А также используется в других функциях, то этот метод не может быть использован для достижения взаимного исключения ресурсов. Потому что, даже если эти функции объявлены как синхронизированные, это означает только то, что несколько потоков не могут выполнять одну и ту же функцию одновременно, но нескольким потокам разрешено одновременно выполнять разные функции, и все эти функции работают с одним и тем же ресурсом. А.
Ниже мы даем еще один способ добиться взаимного исключения использования ресурсов.
синхронизированный блок
Объявив функцию синхронизированной, можно добиться только взаимного исключения тела функции. Чтобы обеспечить взаимное исключение использования ресурсов, то есть только один поток может использовать ресурс одновременно, вы можете поместить оператор, который работает с ресурсом A, в блок синхронизированного кода:
public void function1() {
......
synchronized (A) {
//操作资源 A
}
......
}
public void function2() {
......
synchronized (A) {
//操作资源 A
}
......
}
Таким образом, для ресурса А одновременно может выполняться только один соответствующий блок синхронизированного кода. Следовательно, независимо от того, где используется ресурс A, никогда не будет множества потоков, конкурирующих за этот ресурс.
Синхронизировать
Несколько потоков совместно работают с одними и теми же ресурсами, такое поведение называется синхронизацией. Синхронизация на самом деле представляет собой взаимодействие между потоками, но при работе необходимо работать с одними и теми же ресурсами.
Хорошо известным примером является проблема производителя-потребителя.
Теперь есть производитель и потребитель.Производитель отвечает за производство ресурсов и помещение их в ящик.Емкость ящика бесконечна,потребитель берет ресурсы из ящика,и если в ящике нет ресурсов,то они нужно подождать.
Эта проблема включает в себя два аспекта: взаимное исключение, синхронизацию (кооперацию). Взаимное исключение означает, что ящиком одновременно может пользоваться только одна сторона. Синхронизация означает, что когда потребители потребляют, они должны выполнять условие: ресурсы в ящике не равны нулю, и это условие требует выполнения сотрудничества обеих сторон, то есть потребители не могут потреблять ресурсы без ограничений, и должно быть избыточные ресурсы, произведенные производителями, время на потребление.
В этом примере потребитель не только ограничен мьютексом box, но также должен ждать, пока ресурс не станет ненулевым, прежде чем потреблять.
В псевдокоде операция обеих сторон выражены следующим образом:
private static Box box = new Box();
private static int boxSize = 0;
public static void producer() {
wait(box);
//往 box 中放入资源,boxSize++
signal(box);
}
public static void consumer() {
while (boxSize == 0); //资源为零时阻塞
wait(box);
//从 box 中取出资源,boxSize--
signal(box);
}
public static void main(String[] args) {
parbegin(producer, consumer); //两个函数由两个线程并发执行
}
Перед получением блокировки мьютекса необходимо судить о состоянии "ресурсы не равны нулю", иначе, если сначала получить блокировку мьютекса ящика, а потом судить об условии, может возникнуть взаимоблокировка, то есть оба потока навсегда заблокированы.
Это связано с тем, что после того, как потребитель получит мьютекс, если он обнаружит, что блок не соответствует условию «ресурсы не равны нулю», он заблокируется и будет ждать, пока производитель создаст ресурсы. В это время, поскольку ящик используется потребителем, производитель также будет блокироваться, ожидая, пока потребитель не израсходует ящик. Таким образом, оба потока блокируются и больше не могут быть разблокированы.
Объект условия
Как мы упоминали выше, оценка условий использования ресурсов должна предотвратить возникновение взаимоблокировок до получения блокировки мьютекса Это действительно должно иметь место для нормальной логики кода, но реализация блокировок в Java довольно специфична. Блокировки мьютекса должны быть получены сначала, а затем Определите, выполнены ли условия использования.Если условия не выполнены, используйте метод await() объекта Condition, чтобы отказаться от полученного мьютекса и блокировать до тех пор, пока условия не будут выполнены.
То есть обычная логика такова: сначала определить условия использования ресурсов, а затем получить мьютекс, если они выполняются. Логика Java такова: сначала захватить мьютекс, а если условия использования ресурса не выполняются, то отказаться от захваченного мьютекса.
Создание объекта Condition в Java путем вызова метода NewCondition() объекта Lock, что означает, что Condition связан с Lock. После получения мьютекса, если условия не выполняются, мы используем метод AWAIT() объекта Condition для отказа от полученной блокировки, чтобы другие потоки использовали общие ресурсы. При этом текущий поток начнет ждать, как только другие потоки вызовут метод Signalall() объекта Condition, текущий поток получит уведомление, снова проверит условие, если оно выполняется, дождется блокировки мьютекса.
Специфические примеры приведены ниже:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: Wray Zheng
* @date: 2018-02-02
* @description: An example of synchronization between multiple threads
*/
public class Synchronization {
private static int resourceCount = 3;
private static Lock boxLock = new ReentrantLock();
private static Condition resourceAvailable = boxLock.newCondition();
public static void main(String[] args) {
Thread producer = new Thread(() -> {
try {
while (true) producer();
} catch (InterruptedException e) {}
});
Thread consumer = new Thread(() -> {
try {
while (true) consumer();
} catch (InterruptedException e) {}
});
producer.start();
consumer.start();
}
public static void producer() throws InterruptedException {
boxLock.lock();
resourceCount++;
resourceAvailable.signalAll();
System.out.println("Producer: boxSize + 1 = " + resourceCount);
boxLock.unlock();
Thread.sleep(1000);
}
public static void consumer() throws InterruptedException {
boxLock.lock();
try {
while (resourceCount == 0)
resourceAvailable.await();
resourceCount--;
System.out.println("Consumer: boxSize - 1 = " + resourceCount);
}
finally {
boxLock.unlock();
}
Thread.sleep(500);
}
}
В примере создаются два потока: поток-потребитель и поток-производитель. Производители продолжают производить ресурсы, в то время как потребители продолжают потреблять ресурсы. Чтобы убедиться, что потребители будут блокироваться, когда ресурсы равны нулю, пока производители снова не будут генерировать ресурсы, скорость потребления потребителя больше, чем скорость производства производителя, а начальное количество ресурсы resourceCount имеет значение 3.
Запустите программу один раз и получите следующие результаты:
Producer: resourceCount + 1 = 4
Consumer: resourceCount - 1 = 3
Consumer: resourceCount - 1 = 2
Producer: resourceCount + 1 = 3
Consumer: resourceCount - 1 = 2
Consumer: resourceCount - 1 = 1
Consumer: resourceCount - 1 = 0
Producer: resourceCount + 1 = 1
Consumer: resourceCount - 1 = 0
Producer: resourceCount + 1 = 1
Consumer: resourceCount - 1 = 0
Producer: resourceCount + 1 = 1
Consumer: resourceCount - 1 = 0
Видно, что скорость потребления ресурсов намного выше скорости производства в начале, но после потребления потребитель каждый раз должен ждать, пока производитель произведет ресурсы, поэтому каждый раз он вынужден блокироваться на период время, и, наконец, скорость потребления такая же, как и скорость производства.
ждать(), уведомить всех()
На самом деле объект Object в Java поставляется с методами, соответствующими функциям await() и signalAll() объекта Condition: wait() и notifyAll().
Поскольку все объекты наследуются от Object, мы можем использовать ключевое слово synchronized, чтобы использовать блокировку внутри объекта общего ресурса, и использовать функции wait() и notifyAll() объекта для достижения синхронизации между потоками.
Предыдущую программу производителя-потребителя можно переписать следующим образом:
/**
* @author: Wray Zheng
* @date: 2018-02-02
* @description: An example of synchronization between multiple threads
*/
public class Synchronization {
private static int resourceCount = 3;
private static Object box = new Object();
public static void main(String[] args) {
Thread producer = new Thread(() -> {
try {
while (true) producer();
} catch (InterruptedException e) {}
});
Thread consumer = new Thread(() -> {
try {
while (true) consumer();
} catch (InterruptedException e) {}
});
producer.start();
consumer.start();
}
public static void producer() throws InterruptedException {
synchronized (box) {
resourceCount++;
box.notifyAll();
System.out.println("Producer: resourceCount + 1 = " + resourceCount);
}
Thread.sleep(1000);
}
public static void consumer() throws InterruptedException {
synchronized (box) {
while (resourceCount == 0)
box.wait();
resourceCount--;
System.out.println("Consumer: resourceCount - 1 = " + resourceCount);
}
Thread.sleep(500);
}
}
По сравнению с ручным созданием объекта Lock, объект Condition реализует синхронизацию между потоком, напрямую использует блок синхронизированного кода и объект(), NotifyAll() не удобно?
Пополнить
Для функций, объявленных синхронизированными, синхронизация может быть достигнута следующими способами:
public synchronized void function() {
while (!condition)
wait();
//操作共享资源
notifyAll();
}
Суммировать
Принципы взаимного исключения и синхронизации многопоточности в Java аналогичны принципам взаимного исключения и синхронизации процессов в операционной системе.
В Java, когда несколько потоков управляют одним и тем же ресурсом, вы можете использовать lock() и unlock() объекта ReentrantLock, чтобы добиться взаимного исключения ресурсов, или вы можете использовать для этого синхронизированные блоки кода. Добавьте ключевое слово synchronized при объявлении функции, чтобы добиться взаимного исключения тел функций.
Многопоточная синхронизация требует не только взаимного исключения ресурсов, но и выполнения определенных условий, прежде чем их можно будет использовать.
Объект Condition можно получить с помощью метода newCondition() объекта Lock.При получении мьютекса, если условия использования ресурсов не выполняются, можно вызвать метод await() объекта Condition, чтобы отказаться от мьютекса и блокировать до тех пор, пока не будут выполнены условия, только снова получить блокировку мьютекса.
Вы также можете использовать методы wait() и notify(), поставляемые с объектами Java, вместе с ключевым словом synchronized, чтобы упростить многопоточную синхронизацию.
Статьи по Теме
- Общие сценарии применения лямбда-выражений Java
- Графический интерфейс Java: Awt/Swing реализует масштабирование и прокрутку изображений.
- Разница между созданием объектов в Java и C++ с помощью new
- Eclipse импортирует веб-проект Java, созданный Maven
- Java Web: три картинки для понимания Servlet, Filter, Listener
- Лучшие практики управления многомодульными проектами с помощью Maven