Java. Синхронизация. Монитор. Общие понятия. Ключевое слово synchronized




Синхронизация. Монитор. Общие понятия. Ключевое слово synchronized


Содержание


Поиск на других ресурсах:

1. Понятие синхронизации между потоками. Необходимость применения синхронизации. Монитор

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

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

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

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

Монитором может обладать только один поток. Если поток (объект) обладает монитором, то он при необходимости может повторно войти в него.

В языке Java синхронизация применяется к целым методам или фрагментам кода. Исходя из этого существует два способа синхронизации программного кода:

  • за счет использования модификатора доступа synchronized;
  • за счет использования оператора synchronized () {}.

 

2. Модификатор доступа synchronized. Общая форма

Модификатор доступа synchronized применяется при объявлении синхронизированного метода и имеет следующую общую форму:

synchronized return_type MethodName(parameters) {
  ...
}

здесь

  • MethodName – имя метода, который есть общим ресурсом и его нужно синхронизировать;
  • return_type -тип, который возвращает метод;
  • parameters – параметры метода.

 

3. Оператор synchronized() { }. Общая форма

Оператор synchronized(), в отличие от модификатора доступа synchronized, используется для применения синхронизации к объектам (методам), которые изначально при их разработке не предназначались для многопоточного доступа. То есть, в классе не реализованы методы с модификатором доступа synchronized (классы сторонних разработчиков).

Общая форма оператора synchronized () следующая:

synchronized (reference) {
  // Операторы, которые нужно синхронизировать
  // ...
}

здесь

  • reference – ссылка на синхронизируемый объект;
  • фигурные скобки {}, которые определяют блок синхронизации. В этом блоке указываются операторы для синхронизации.

 

4. Пример, демонстрирующий синхронизированный доступ к общему методу из трех разных потоков. Применение модификатора доступа synchronized

В примере демонстрируется необходимость применения модификатора доступа synchronized с целью упорядочения доступа к ресурсу из разных потоков.

Общим ресурсом является метод Get() класса Array5. Метод Get() возвращает массив из 5 чисел, значения которых последовательно изменяются от 1 к 5. К этому методу будет осуществлена попытка одновременного доступа из разных потоков, поэтому метод обозначен как synchronized.

Инкапсуляция одного потока выполнения осуществляется в классе ArrayThread классическим (стандартным) способом с помощью реализации интерфейса Runnable. Класс ArrayThread содержит следующие общедоступные (public) элементы:

  • A — ссылку на экземпляр класса Array5. С помощью этой ссылки происходит доступ к методу Get();
  • AI — массив чисел, получаемый из метода Get();
  • t — ссылку на текущий поток;
  • конструктор ArrayThread(), который получает экземпляр класса Array5 и название потока. В конструкторе происходит создание потока выполнения и его запуск;
  • метод run(), вызывающий метод Get() с помощью ссылки A.

В классе Threads представлена функция main(), в которой создается три потока AT1, AT2, AT3 типа ArrayThread и один объект A5. Метод Get() объекта A5 является общим ресурсом для этих трех потоков.

// Тема. Синхронизация потоков.
// Класс, содержащий общий ресурс
class Array5 {
  // Синхронизированный метод, возвращающий массив з 5 чисел
  // и печатающий его на экране
  synchronized public int[] Get(String threadName) {
    // Вывести название выполняемого потока
    System.out.println("Begin => " + threadName);

    // Сформировать 5 целых чисел от 1 до 5
    int[] AI = new int[5];
    for (int i=0; i<AI.length; i++)
      AI[i] = i+1;

    // Вызвать задержку,чтоб другие потоки могли выполняться
    try {
      Thread.sleep(100);
    }
    catch (InterruptedException e) {
      System.out.println(e.getMessage());
    }

    // Вывести в методе массив AI для контроля
    System.out.print(threadName + " = [ ");
    for (int i=0; i<AI.length; i++)
      System.out.print(AI[i] + " ");
    System.out.println(" ]");

    // Оповестить об окончании выполнения метода
    System.out.println("End => " + threadName);
    return AI;
  }
}

// Класс, который является потоком читающим массив чисел
class ArrayThread implements Runnable {
  Array5 A; // экземпляр общего ресурса
  int[] AI; // массив из 5 чисел, который получается из общего ресурса
  Thread t; // ссылка на текущий поток

  // Конструктор - получает параметры:
  // - имя потока;
  // - экземпляр класса Array5.
  ArrayThread(String threadName, Array5 A) {
    // Сохранить массив
    this.A = A;

    // Создать поток
    t = new Thread(this, threadName);

    // Запустить поток на выполнение
    t.start(); // вызов метода run()
  }

  // Метод, в котором запускается поток, создающий массив из 5 чисел
  public void run() {
    AI = A.Get(t.getName()); // получить массив чисел от 1 до 5
  }
}

public class Threads {

  public static void main(String[] args) {
    // Продемонстрировать доступ к общему ресурсу из 3-х разных потоков
    // 1. Объявить экземпляр, который будет общим ресурсом в трех разных потоках
    Array5 A5 = new Array5();

    // 2. Объявить три разных потока и запустить их
    ArrayThread AT1 = new ArrayThread("AT1", A5);
    ArrayThread AT2 = new ArrayThread("AT2", A5);
    ArrayThread AT3 = new ArrayThread("AT3", A5);

    // 3. Дождаться завершения всех трех потоков
    try {
      AT1.t.join();
      AT2.t.join();
      AT3.t.join();
    }
    catch (InterruptedException e) {
      System.out.println(e.getMessage());
    }
  }
}

Результат выполнения программы

Begin => AT1
AT1 = [ 1 2 3 4 5 ]
End => AT1
Begin => AT3
AT3 = [ 1 2 3 4 5 ]
End => AT3
Begin => AT2
AT2 = [ 1 2 3 4 5 ]
End => AT2

Если в вышеприведенном примере перед методом Get() класса Array5 убрать ключевое слово synchronized

class Array5 {

  ...

  // здесь нет модификатора synchronized (тут вам не здесь:)
  public int[] Get(String threadName) {
    ...
  }
}

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

Begin => AT1
Begin => AT2
Begin => AT3
AT1 = [ AT2 = [ 1 1 2 2 3 3 4 4 5 ]
End => AT1
5 ]
End => AT2
AT3 = [ 1 2 3 4 5 ]
End => AT3

 

5. Пример использования оператора synchronized() {} для синхронизированного доступа к общему ресурсу

Код предыдущего примера (смотрите п. 4), синхронизирующего потоки, можно представить по-другому. В этом случае, вместо модификатора доступа synchronized перед именем метода Get() нужно использовать оператор synchronized () {}. Оператор synchronized должен быть использован в методе run() класса ArrayThread.

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

// Класс, который есть общим ресурсом
class Array5 {
  // Метод, возвращающий массив из 5 чисел и выводящий его на экран.
  // Уже не синхронизирован
  public int[] Get(String threadName) {
    ...
  }
}

// Класс, который есть потоком читающим массив чисел
class ArrayThread implements Runnable {
  Array5 A; // экземпляр общего ресурса
  int[] AI; // массив из 5 чисел, получающийся из общего ресурса
  Thread t; // ссылка на текущий поток

  // Конструктор - получает параметры:
  // - имя потока;
  // - экземпляр класса Array5.
  ArrayThread(String threadName, Array5 A) {
    ...
  }

  // Метод, в котором запускается поток, создающий массив из 5 чисел
  public void run() {
    // Оператор synchronized для общего ресурса A
    synchronized (A) {
      AI = A.Get(t.getName()); // получить массив чисел от 1 до 5
    }
  }
}

 


Связанные темы