В ранних статьях блога вы видели, как надо использовать объекты Lock и Condition. Прежде чем двинуться дальше, подведем итоги, перечислив ключевые моменты, касающиеся блокировок и условий.
- Блокировка защищает сегмент кода, позволяя только одному потоку в единицу времени выполнять этот код.
- Блокировка управляет потоками, которые пытаются войти в защищенный сегмент кода.
- Каждый объект условия управляет потоками, вошедшими в защищенный сегмент кода, но которые пока не в состоянии выполнять работу.
Интерфейсы Lock и Condition были добавлены в Java SE 5.0, чтобы предоставить программистам высокую степень контроля блокировок. Однако в большинстве ситуации вам не понадобится такой контроль, и вы можете использовать механизм, построенный на средствах языка Java.
Еще со времен версии 1.0 каждый объект Java обладает внутренней блокировкой. Если метод объявлен с ключевым словом synchronized, то блокировка объекта защищает весь этот метод. То есть для того, чтобы вызвать этот метод, поток должен захватить внутреннюю блокировку объекта.
Другими словами, следующее:
1 2 3 4 |
public synchronized void method() { тело метода } |
является эквивалентом этому:
1 2 3 4 5 6 7 8 9 |
public void method() { this.внутренняяБлокировка.lock(); try { тело метода } finally { this.внутренняяБлокировка.unlock(); } } |
Например, вместо использования явной блокировки, мы можем просто объявить метод transfer класса Bank, как synchronized.
Внутренний объект блокировки имеет единственное ассоциированное с ним условие. Метод wait() добавляет поток в набор ожидания, а методы notifyAll() и notify() разблокируют ожидающие потоки.
Другими словами, вызов wait() / notifyAll() — это эквиваленты следующего:
1 2 |
внутреннееУсловие.await(); внутреннееУсловие.signalAll(); |
Методы wait(), notifyAll() и notify() являются финальными(final) методами класса Object. Методы Condition должны именоваться await, signalAll и signal, так что они не конфликтуют с этими методами.
Например, вы можете реализовать класс Bank следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Bank { public synchronized void transfer(int from, int to, int amount) throws InterruptedException { while(accounts[from] < amount) wait(); // ожидать единственного условия внутреннего объекта блокировки accounts[from] -= amount; accounts[to] += amount; notifyAll(); // известить все потоки, ожидающие условия } public synchronized double getTotalBalance() { . . . } private double[] accounts; } |
Как видите, применение ключевого слова synchronized порождает намного более краткий код. Конечно, чтобы понять его, необходимо знать о том, что каждый объект обладает внутренней блокировкой, и что эта блокировка имеет внутреннее условие.
Блокировка управляет потоками, которые пытаются войти в метод synchronized. Условие управляет потоками, вызвавшими wait().
Синхронизированные методы относительно просты. Однако начинающие программисты часто испытают затруднения с условиями.
Также допустимо объявлять статические методы синхронизированными. Когда вызывается такой метод, он захватывает внутреннюю блокировку ассоциированного объекта класса.
Например, если класс Bank имеет статический синхронизированный метод, тогда блокировка объекта Bank.class блокируется при ее вызове. В результате ее не может вызвать никакой другой поток и никакой другой синхронизированный статический метод того же класса.
Внутренние блокировки и условия имеют некоторые ограничения:
- Вы не можете прервать поток, который пытается захватить блокировку.
- Вы не можете специфицировать таймаут, пытаясь захватить блокировку.
- Наличие единственного условия на блокировку может быть неэффективным.
Что же следует использовать в вашем коде — объекты Lock и Condition или синхронизированные методы? Вот наши рекомендации:
- Лучше не использовать ни Lock/Condition, ни ключевое слово synchronized. Во многих ситуациях вы можете применять один из механизмов пакета java.util.concurrent, который сделает всю работу по блокировке за вас.
- Если ключевое слово synchronized подходит для вашей ситуации, обязательно используйте его. Вы напишете меньше кода, и у вас будет меньше шансов допустить ошибку. В программе которая приведена ниже показан пример с банком, реализованный на основе синхронизированных методов.
- Используйте Lock/Condition, если вы действительно нуждаетесь в дополнительных возможностях этих конструкций.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
import java.util.concurrent.locks.*; /** * Банк с множеством счетов, использующий блокировки для сериализации доступа. * @version 1.50 * @author pro-java.ru */ public class Bank { /** * Конструирует банк. * @param n Количество счетов * @param initialBalance Начальный баланс каждого счета * * */ public Bank(int n, double initialBalance) { account = new double[n]; for(int i = 0; i < accounts.length; i++) accounts[i] = initialBalance; } /* * Переводит деньги с одного счета на другой. * @param from Счет, с которого выполняется перевод * @param to Счет, на который выполняется перевод * @param amount Сумма перевода */ public void transfer(int from, int to, double amount) throws InterruptedException { while(accounts[from] < amount) wait(); System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf("Total Balance: %10.2f%n", getTotalBalance()); sufficientFunds.signalAll(); notifyAll(); } /* * Получаем сумму всех счетов. * @return Общий баланс */ public double getTotalBalance() { double sum = 0; for(double a : accounts) sum += a; return sum; } /* * Получаем количество счетов в банке. * @return Количество счетов */ public int size() { return accounts.length; } private final double[] accounts; } |