Содержание
Ключевое слово synchronized
может быть использовано, чтобы гарантировать, что только один поток одновременно выполняет определенный раздел кода. Это простой способ предотвратить состояние гонки, которое возникает, когда несколько потоков изменяют общие данные одновременно, что приводит к неверным результатам. При synchronized
как целые методы, так и выбранные блоки.
Эта статья требует базовых знаний о потоках Java и условиях гонки .
Основы синхронизации
Давайте посмотрим, как использовать synchronized
методы и, более детально, блоки кода. Я также объясню, как это работает.
Использование synchronized
ключевого слова
Если мы добавим synchronized
модификатор к методу, JVM гарантирует, что только один поток может выполнить метод одновременно:
class MutableInteger { private int value; public synchronized void increment() { value++; } public synchronized int getValue() { return value; } }
Если несколько потоков пытаются выполнить синхронизированный метод одновременно, только один сможет это сделать. Другие будут приостановлены, пока первый не выйдет из метода. (В некотором смысле он работает во многом как светофор, следя за тем, чтобы параллельные выполнения не сталкивались.) Из-за этого потоки, увеличивающие счетчик value
, не перезаписывают результаты друг друга, и каждое увеличение будет записываться.
Чтобы убедиться, что synchronized
ключевое слово работает, я создаю десять потоков, каждый из которых увеличивает счетчик на десять тысяч раз. Я создал суть кода — он в основном такой же, как в статье, объясняющей условия гонки . Как и ожидалось, результат всегда одинаков и всегда корректен:
Result value: 100000
Ключевое слово synchronized
ограничивает доступ потока только к одному объекту. Если у вас есть два разных экземпляра, два разных потока могут использовать их одновременно, и они не будут блокировать друг друга:
MutableInteger integer1 = new MutableInteger(); MutableInteger integer2 = new MutableInteger(); // Threads are using different objects and don't // interact with each other Thread thread1 = new Thread(new IncrementingRunnable(integer1)); Thread thread2 = new Thread(new IncrementingRunnable(integer2));
Как synchronized
работает
Синхронизация имеет две формы: синхронизированные методы и синхронизированные операторы. Пока что мы столкнулись только с первым типом, вот второй:
class MutableInteger { private int value; private Object lock = new Object(); public void increment() { // Only one thread can execute this block of code at any time synchronized (lock) { value++; } } public int getValue() { // Only one thread can execute this block of code at any time synchronized (lock) { return value; } } }
Когда поток входит в синхронизированный блок, он пытается получить блокировку, связанную с объектом, переданным в оператор. (В Java блокировку объекта часто называют его монитором .) Если в данный момент блокировка удерживается другим потоком, текущий будет приостановлен до снятия блокировки. В противном случае поток завершается успешно и входит в синхронизированный блок.
Когда поток завершает синхронизированный блок, он снимает блокировку, и другой поток может получить его. Блокировка снимается, когда поток покидает синхронизированный блок, даже если выдается исключение.
Кстати, использование ключевого слова synchronized
в методе, а не в блоке — это просто сокращение для использования собственной блокировки объекта для синхронизации:
// this is equivalent to 'public synchronized void ...' public void increment() { synchronized (this) { value++; } }
Возможно, вам сейчас интересно, какая блокировка используется, если вы синхронизируете статический метод.
public static synchronized void foo() { ... }
В этом случае блокируется сам объект класса, поэтому он эквивалентен следующему:
public static void foo() { synchronized (TheClassContainingThisMethod.class) { ... } }
Несколько замков
До сих пор мы видели, что класс использует только один объект блокировки, но обычно используется несколько блокировок в одном классе. Это позволяет двум разным потокам параллельно выполнять два разных метода для одного объекта:
class TwoCounters { private Object lock1 = new Object(); private Object lock2 = new Object(); private int counter1; private int counter2; public void incrementFirst() { synchronized (lock1) { counter1++; } } public void incrementSecond() { synchronized (lock2) { counter2++; } } }
Подсчет в реальном Java-приложении
Синхронизация обычно используется в реальных Java-приложениях и является мощным инструментом, но я бы не советовал вам использовать ее для агрегирования значений из разных потоков, как мы это делали здесь. Java предоставляет специальный класс для этого, называемый AtomicInteger
, как часть своего богатого пакета параллелизма. Он имеет лучшую производительность, чем MutableInteger
который мы здесь реализовали, и гораздо более богатый интерфейс.
Выводы
В этом посте вы узнали, как избежать условий гонки с ключевым словом synchronized
, которое гарантирует, что только один поток может выполнить данный блок кода. Его можно использовать на уровне метода, в этом случае он использует сам объект в качестве блокировки или на уровне блока, и в этом случае должен быть указан объект блокировки. Любой объект Java может быть использован в качестве блокировки.
Аналогичным фундаментальным свойством параллелизма являются методы Object
wait
и notify
, которые можно использовать для реализации защищенных блоков для координации действий между различными потоками. Кроме того, Java имеет интерфейс Lock
который позволяет реализовать синхронизацию аналогично synchronized
ключевому слову, но обладает большей гибкостью. Если вы хотите использовать коллекции в нескольких потоках, обратите внимание на синхронизированные оболочки и особенно параллельные коллекции .