Синхронизация потоков

1. Что такое синхронизация?

Все потоки, принадлежащие одному процессу, разделяют некоторые общие ресурсы (адресное пространство, открытые файлы). Что произойдет, если один поток еще не закончил работать с каким-либо общим ресурсом, а система переключилась на другой поток, использующий тот же ресурс?

Когда два или более потоков имеют доступ к одному разделенному ресурсу, они нуждаются в обеспечении того, что ресурс будет использован только одним потоком одновременно. Процесс, с помощью которого это достигается, называется синхронизацией.

Пример 1. Одновременный доступ к ресурсу

public class Account implements Runnable {
    private int balance = 50;

    public int getBalance() {
        return balance;
    }

    public void withdraw(int amount) {
        balance -= amount;
    }

    public void run() {
        for (int x = 0; x < 5; x++) {
            makeWithdrawal(10);
            if (getBalance() < 0) {
                System.out.println("account is overdrawn!");
            }
        }
    }

    private void makeWithdrawal(int amount) {
        if (getBalance() >= amount) {
            System.out.println(Thread.currentThread().getName()
                    + " is going to withdraw");
            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            withdraw(amount);
            System.out.println(Thread.currentThread().getName()
                    + " completes the withdrawal. The balance is "
                    + getBalance());
        } else {
            System.out.println("Not enough in account for "
                    + Thread.currentThread().getName()
                    + " to withdraw " + getBalance());
        }
    }
}
public class AccountDanger{
    public static void main(String[] args) {
        Account account = new Account();
        Thread one = new Thread(account);
        Thread two = new Thread(account);
        one.setName("Fred");
        two.setName("Lucy");
        one.start();
        two.start();
    }
}

Два потока в предыдущем примере находятся в состоянии гонок. Состояние гонок – это одновременный вызов в потоках исполнения одного и того же метода для того же самого объекта.

Чтобы защитить данные, нам необходимо выполнить два действия:

  1. Объявить переменные как private.
  2. Синхронизировать код.

2. Способы синхронизации кода

Синхронизировать прикладной код можно двумя способами:

  1. С помощью синхронизированных методов. Метод объявляется с использованием ключевого слова synchronized:
    public synchronized void someMethod(){}
  2. Заключить вызовы методов в блок оператора synchronized
    sуnсhrоnizеd(объект) { 
      // операторы, подлежащие синхронизации 
    }​

Только методы и блоки могут быть синхронизированы, но не переменные и классы.

Не все методы в классе должны быть синхронизированы.

3. Модификатор volatile

Поток создается с чистой рабочей памятью, и должен перед использованием загрузить все необходимые переменные из основного хранилища (можно сказать что он имеет некий кэш).

Любая переменная сначала создается в основном хранилище и лишь затем копируется в рабочую память потоков, которые будут ее применять.

Если переменная объявлена, как volatile, то ее чтение и запись будет производиться из\в основное хранилище.

Чтение volatile переменных синхронизировано и запись в volatile переменные синхронизирована, а неатомарные операции – нет.

4. Монитор

Каждый объект в Java имеет ассоциированный с ним монитор. Монитор - это объект, используемый в качестве взаимоисключающей блокировки. Когда поток исполнения запрашивает блокировку, то говорят, что он входит в монитор.

Только один поток исполнения может в одно, и то же время владеть монитором. Все другие потоки исполнения, пытающиеся войти в заблокированный монитор, будут приостановлены до тех пор, пока первый поток не выйдет из монитора. Говорят, что они ожидают монитор.

Поток, владеющий монитором, может, если пожелает, повторно войти в него.

Если поток засыпает, то он удерживает монитор.

Поток может захватить сразу несколько мониторов.

Рассмотрим разницу между доступом к объекту без синхронизации и из синхронизированного кода. Доступ к банковскому счету без синхронизации:

Доступ к объекту без синхронизации фото

И с синхронизацией:

Доступ к объекту с синхронизацией фото

Когда выполнение кода доходит до оператора synchronized, монитор объекта счет блокируется, и на время его блокировки монопольный доступ к блоку кода имеет только один поток, который и произвел блокировку (Люси).

После окончания работы блока кода, монитор объекта счет освобождается и становится доступным для других потоков.

После освобождения монитора его захватывает другой поток, а все остальные потоки продолжают ожидать его освобождения.

Синхронизируем методы из предыдущего примера для корректного совместного доступа потоков:

Пример 2. Синхронизация доступа к ресурсу

public class AccountDanger implements Runnable {
    private Account account = new Account();

    public static void main(String[] args) {
        AccountDanger accountDanger = new AccountDanger();
        Thread one = new Thread(accountDanger);
        Thread two = new Thread(accountDanger);
        one.setName("Fred");
        two.setName("Lucy");
        one.start();
        two.start();
    }

    public void run() {
        for (int x = 0; x < 5; x++) {
            makeWithdrawal(10);
            if (account.getBalance() < 0) {
                System.out.println("account is overdrawn!");
            }
        }
    }

    private synchronized void makeWithdrawal(int amt) {
        if (account.getBalance() >= amt) {
            System.out.println(Thread.currentThread().getName()
                    + " is going to withdraw");
            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            account.withdraw(amt);
            System.out.println(Thread.currentThread().getName()
                    + " completes the withdrawal. The balance is "
                    + account.getBalance());
        } else {
            System.out.println("Not enough in account for "
                    + Thread.currentThread().getName()
                    + " to withdraw " + account.getBalance());
        }
    }
}

5. Синхронизация статических методов

Статические методы тоже могут быть синхронизированы с помощью ключевого слова synchronized.

Для синхронизации статических методов используется один монитор для одного класса. Каждый загруженный в Java класс имеет соответствующий объект класса Class, представляющий этот класс. Монитор именно этого объекта используется для синхронизации статических методов (если они синхронизированы).

Пример 3. Синхронизация статических методов

public static synchronized int getCount() {
    return count;
}

И эквивалентный код:

public static int getCount() {
    synchronized(MyClass.class) {
        return count;
    }
}

6. Блокировка

Если поток пытается зайти в синхронизированный метод, а монитор уже захвачен, то поток блокируется по монитору объекта.

Поток попадает в специальный пул для этого конкретного объекта и должен находиться там пока монитор не будет освобожден. После этого поток возвращается в состояние runnable.

Варианты блокировки:

  1. Потоки, вызывающие нестатические синхронизированные методы одного и того же класса, будут блокировать друг друга только если они вызваны для одного объекта.
  2. Потоки, вызывающие статические синхронизированные методы одного класса, будут всегда блокировать друг друга. Они блокируются по монитору Class объекта. Статические синхронизированные и нестатические синхронизированные методы не будут блокировать друг друга никогда.
  3. Для синхронизированных блоков нужно смотреть какой объект используется для синхронизации.
  4. Блоки синхронизированные по одному объекту будут блокировать друг друга.

7. Методы и состояние блокировки

Освобождают монитор

Удерживают монитор

Класс определяющий метод

wait()

notify()

java.lang.Object

 

join()

java.lang.Thread

 

sleep()

java.lang.Thread

 

yield()

java.lang.Thread

Read also:
Trustpilot
Trustpilot
Comments