ассистент каф. КСМ Ковалев М.П.

Криворожский технический университет, Украина

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

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

Для чего и когда она нужна?

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

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

В языке программирования Java предусмотрено несколько средств для синхронизации потоков, которые мы сейчас рассмотрим.

Синхронизация методов

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

Чтобы воспользоваться защелками, вы можете объявить соответствующий метод как synchronized, сделав его синхронизированным:

public synchronized void decrement()

{

  . . .

}

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

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

Заметим, что не обязательно синхронизовать весь метод - можно выполнить синхронизацию только критичного фрагмента кода.

. . .

synchronized(Account)

{

  if(Account.check(3000000))

     Account.decrement(3000000);

}

. . .

Здесь синхронизация выполняется для объекта Account.

 

Блокировка потока

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

Блокировка на заданный период времени

С помощью метода sleep можно заблокировать поток на заданный период времени:

try

{

  Thread.sleep(500);

}

catch (InterruptedException ee)

{

  . . .

}

В данном примере работа потока  Thread приостанавливается на 500 миллисекунд. Заметим, что во время ожидания приостановленный поток не отнимает ресурсы процессора.

Так как метод sleep может создавать исключение InterruptedException, необходимо предусмотреть его обработку. Для этого мы использовали операторы try и catch.

 

 

Временная приостановка и возобновление работы

Методы suspend и resume позволяют, соответственно, временно приостанавливать и возобновлять работу потока.

В следующем фрагменте кода поток m_Rectangles приостанавливает свою работу, когда курсор мыши оказывается над окном аплета:

public boolean mouseEnter(Event evt,

   int x, int y)

{

  if (m_Rectangles != null)

  {

    m_Rectangles.suspend();

  }

  return true;

}

Работа потока возобновляется, когда курсор мыши покидает окно аплета:

public boolean mouseExit(Event evt,

   int x, int y)

{

  if (m_Rectangles != null)

  {

    m_Rectangles.resume();

  }

  return true;

}

Ожидание извещения

Если вам нужно организовать взаимодействие потоков таким образом, чтобы один поток управлял работой другого или других потоков, вы можете воспользоваться методами wait, notify и notifyAll, определенными в классе Object.

Метод wait может использоваться либо с параметром, либо без параметра. Этот метод переводит поток в состояние ожидания, в котором он будет находиться до тех пор, пока для потока не будет вызван извещающий метод notify, notifyAll, или пока не истечет период времени, указанный в параметре метода wait.

Как пользоваться методами wait, notify и notifyAll?

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

public synchronized void run()

{

  while (true)

  {

    . . .

    try

    {

      this.wait();

    }

    catch (InterruptedException e)

    {

    }

  }

}

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

 

 

Ниже мы привели пример потока, вызывающией метод notify:

public void run()

{

  while (true)

  {

    try

    {

      Thread.sleep(30);

    }

    catch (InterruptedException e)

    {

    }

 

    synchronized(STask)

    {

      STask.notify();

    }

  }

}

Этот поток реализован в рамках отдельного класса, конструктору которого передается ссылка на поток, вызывающую метод wait. Эта ссылка хранится в поле STask.

Обратите внимание, что хотя сам метод run не синхронизированный, вызов метода notify выполняется в синхронизированном режиме. В качестве объекта синхронизации выступает поток, для которого вызывается метод notify.

Ожидание завершения потока

С помощью метода join вы можете выполнять ожидание завершения работы потока, для которой этот метод вызван.

 

Существует три определения метода join:

public final void join();

public final void join(long millis);

public final void join(long millis,  int nanos);

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

Литература:

1.     Кен Арнолд.   Язык программирования Java, 3-е издание.:–М.: Издательский дом «Вильямс», 2001.

2.     http://www.coordinator.dn.ua/lib/Java/Sun/index.html