Синхронизация потоков
В многопоточной программе не всегда один поток работает независимо от другого. Бывает, они обмениваются данными или обрабатывают одни и те же объекты.
В таких случаях возникают проблемы правильной организации взаимодействия нитей так, чтобы они не мешали работе друг друга. Один поток должен знать об изменениях, внесенных другим потоком.
Синхронизация потоков – это настройка их взаимодействия. Рассмотрим пример, в котором два разных потока работают с одним и тем же объектом:
public class NoSynch { public static void main(String[] args) throws InterruptedException { Client client = new Client(1000); Thread operation = new Operation(client, 1000); Thread operation1 = new Operation(client, 500); operation.start(); operation1.start(); operation.join(); operation1.join(); System.out.println(client.getBill()); } } class Operation extends Thread { private Client mClient; private int mPay; Operation(Client client, int pay) { mClient = client; mPay = pay; } @Override public void run() { System.out.println(this.getName() + ": " + mClient.getBill()); if (mClient.getBill() - mPay >= 0) { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } mClient.changeBill(mPay); } System.out.println(this.getName() + ": " + mClient.getBill()); System.out.println(this.getName() + " stop"); } } class Client { private int mBill; Client(int bill) { this.mBill = bill; } int getBill() { return mBill; } void changeBill(int pay) { mBill -= pay; } }
Результат:
Thread-0: 1000 Thread-1: 1000 Thread-0: 0 Thread-0 stop Thread-1: -500 Thread-1 stop -500
Задержка sleep'ом искусственно надумана, чтобы разделить проверку условия и вычитание. В реальных программах между одним действием и другим могло бы быть множество промежуточных, на выполнение которых требуется время.
У клиента на счету оказалась отрицательная сумма, хотя по логике вещей одна из операций вычитания не должна была бы выполняться. Проблема возникла из-за того, что в каждом потоке на момент проверки (mClient.getBill() - mPay >= 0
) было по 1000 на счету. И каждый поток "решил", что денег достаточно. После этого поток-0 уменьшил сумму. Когда поток-1 приступил к вычитанию, денег на счету уже не было.
Чтобы исключить подобную логическую ошибку, надо как-то блокировать объект client, чтобы пока он обрабатывается в одном потоке, другие не могли его изменять. Как вариант делать это в методе в run():
class Operation extends Thread { private final Client mClient; private int mPay; Operation(Client client, int pay) { mClient = client; mPay = pay; } @Override public void run() { System.out.println(this.getName() + ": " + mClient.getBill()); synchronized (mClient) { if (mClient.getBill() - mPay >= 0) { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } mClient.changeBill(mPay); } } System.out.println(this.getName() + ": " + mClient.getBill()); System.out.println(this.getName() + " stop"); } }
Результат:
Thread-0: 1000 Thread-1: 1000 Thread-0: 0 Thread-0 stop Thread-1: 0 Thread-1 stop 0
И хотя оба потока сначала увидели 1000. Когда началась проверка и вычитание, действия выполнялись совместно, другой поток в это время доступ к объекту не имел.
Блок кода с ключевым словом synchronized может одновременно выполняться только одним потоком.
В примере синхронизация выполнена по объекту. В Java также возможна синхронизация по методу, т. е. пока методом пользуется один поток, другому он недоступен. Если в вышеприведенной программе мы по отдельности синхронизируем методы getBill() и changeBill() объекта, это ничего не даст. Решением может быть объединение проверки и вычитания в один метод:
class Operation1 extends Thread { private Client1 mClient; private int mPay; Operation1(Client1 client, int pay) { mClient = client; mPay = pay; } @Override public void run() { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } mClient.changeBill(mPay); } } class Client1 { private int mBill; Client1(int bill) { this.mBill = bill; } synchronized void changeBill(int pay) { System.out.println(mBill); if (mBill-pay >= 0) { mBill -= pay; System.out.println(mBill); } } }
У каждой нити есть собственный кэш, куда копируются данные, с которыми она работает. Таким образом, одна нить не имеет доступа к данным другой и не может прочитать произошедшие изменения, а читает устаревшие значения из обычной памяти. Если потоки работают с общими данными, можно запретить их помещение в кэш, используя ключевое слово volatile. Например:
private volatile int mBill;