Java: руководство для начинающих
Шрифт:
Рассмотрим подробнее эту программу. В ней определены три класса. Имя первого — SumArray. В нем содержится метод sumArray , вычисляющий сумму элементов целочисленного массива. Во втором классе MyThread используется статический объект sa типа SumArray для получения суммы элементов массива. А поскольку он статический, то все экземпляры класса MyThread используют одну его копию. И наконец, в классе Sync создаются два потока, в каждом из которых должна вычисляться сумма элементов массива.
В методе sumArray вызывается метод sleep . Он нужен лишь для того, чтобы обеспечить переключение задач. Метод sumArray синхронизирован, и поэтому в каждый момент времени он может использоваться только одним потоком. Следовательно, когда второй порожденный поток начинает свое исполнение, он не может вызвать метод sumArray до тех пор, пока этот метод не завершится в первом потоке. Благодаря этому обеспечивается правильность получаемого результата.
Для того чтобы лучше понять эффект от использования ключевого слова synchronized, попробуйте удалить его из объявления метода sumArray . В итоге
Нетрудно заметить, что вследствие одновременного вызова sa. sumArray из разных потоков результат искажается.
Прежде чем переходить к рассмотрению следующей темы, перечислим основные свойства синхронизированных методов.
Синхронизированный метод создается путем указания ключевого слова synchronized в его объявлении.
Как только синхронизированный метод любого объекта получает управление, объект блокируется и ни один синхронизированный метод этого объекта не может быть вызван другим потоком.
Потоки, которым требуется синхронизированный метод, используемый другим потоком, ожидают до тех пор, пока не будет разблокирован объект, для которого он вызывается.
Когда синхронизированный метод завершается, разблокируется объект, для которого он вызывается. Синхронизированные блоки
Несмотря на то что создание синхронизированных методов в классах — простой и эффективный способ управления потоками, такой способ оказывается пригодным далеко не всегда. Иногда возникает потребность синхронизировать доступ к методам, в объявлении которых отсутствует ключевое слово synchronized. Подобная ситуация часто возникает при использовании классов, которые были созданы независимыми разработчиками и исходный код которых недоступен. В таком случае ввести в объявление нужного метода ключевое слово synchronized вряд ли удастся. Как же тогда синхронизировать объект класса, содержащего этот метод? К счастью, данное затруднение разрешается очень просто. Достаточно ввести вызов метода в синхронизированный кодовый блок типа synchronized.
Синхронизированный блок определяется следующим образом: synchronized{ссылка_на_объект) { // синхронизируемые операторы }
где ссылканаобъект обозначает ссылку на конкретный объект, который должен быть синхронизирован. Как только содержимое синхронизированного блока получит управление, ни один другой поток не сможет вызвать метод для объекта, на который делается ссылканаобъект9 до тех пор, пока этот кодовый блок не завершится.
Следовательно, обращение к методу sumArray можно синхронизировать, вызвав его из синхронизированного блока. Такой способ демонстрируется в приведенной ниже переделанной версии предыдущей программы. // Применение синхронизированного блока // для управления доступом к методу sumArray. class SumArray { private int sum; // Здесь метод sumArray не синхронизирован. int sumArray(int nums[]) { sum =0; // обнулить сумму for(int i=0; icnums.length; i++) { sum += nums[i]; System.out.println("Running total for " + Thread.currentThread.getName + " is " + sum); try { Thread.sleep(10); // разрешить переключение задач } catch(InterruptedException exc) { System.out.println("Main thread interrupted."); } } return sum; } } class MyThread implements Runnable { Thread thrd; static SumArray sa = new SumArray; int a[]; int answer; // построить новый поток MyThread(String name, int nums[]) { thrd = new Thread(this, name); a = nums; thrd.start; // начать поток } // начать исполнение нового потока public void run { int sum; System.out.println(thrd.getName + " starting."); // Здесь вызовы метода sumArray для объекта sa синхронизированы. synchronized(sa) { answer = sa.sumArray(a); } System.out.println("Sum for " + thrd.getName + " is " + answer); System.out.println(thrd.getName + " terminating."); } } class Sync { public static void main(String args[]) { int a [] = {1, 2, 3, 4, 5}; MyThread mtl = new MyThread("Child #1", a); MyThread mt2 = new MyThread("Child #2", a); try { mtl.thrd.join; mt2.thrd.join; } catch (InterruptedException exc) { System.out.println("Main thread interrupted."); } } }
Выполнение этой версии программы дает такой же правильный результат, как и предыдущей ее версии, в которой использовался синхронизированный метод. Организация взаимодействия потоков с помощью методов notify , wait и notifyAll
Рассмотрим для примера следующую ситуацию. В потоке Т выполняется синхронизированный метод, которому необходим доступ к ресурсу R. Этот ресурс временно недоступен. Что должен предпринять поток т? Если он будет ожидать в цикле освобождения ресурса R, объект будет по-прежнему заблокирован и другие потоки не смогут обратиться к нему. Такое решение малопригодно, поскольку оно сводит на нет все преимущества программирования в многопоточной среде. Намного лучше, если поток Т временно разблокирует объект и позволит другим потокам воспользоваться его методами. Когда ресурс R станет доступным, поток т получит об этом уведомление и возобновит свое исполнение. Но для того чтобы такое решение можно было реализовать, необходимы средства взаимодействия потоков, с помощью которых один поток мог бы уведомить другой поток о том, что он приостановил свое исполнение, а также получить уведомление о том, что его исполнение может быть возобновлено. Для организации подобного взаимодействия потоков в Java предусмотрены методы wait , notify и notifyAll .
Эти методы реализованы в классе Object, поэтому они доступны для любого объекта. Но обратиться к ним можно только из синхронизированного контекста. А применяются они следующим образом. Когда поток временно приостанавливает свое исполнение, он вызывает метод wait . При этом поток переходит в состояние ожидания и монитор данного объекта освобождается, позволяя другим потокам использовать объект. Впоследствии ожидающий поток возобновит свое выполнение, когда другой поток войдет в тот же самый монитор и вызовет метод notify или notifyAll .
В классе Object определены различные формы объявления метода wait , как показано ниже. final void wait throws InterruptedException final void wait(long миллисекунд) throws InterruptedException final void wait(long миллисекунд, int наносекунд) throws InterruptedException
В первой своей форме метод wait переводит поток в режим ожидания до поступления уведомления. Во второй форме метода организуется ожидание уведомления или до тех пор, пока не истечет указанный период времени. А третья форма позволяет точнее задавать период времени в наносекундах.
Ниже приведены общие формы объявления методов notify и notifyAll . final void notifyO final void notifyAll
При вызове метода notify возобновляется исполнение одного ожидающего потока. А метод notifyAll уведомляет все потоки об освобождении объекта, и тот поток, который имеет наивысший приоритет, получает доступ к объекту.
Прежде чем рассматривать конкретный пример, демонстрирующий применение метода wait , необходимо сделать важное замечание. Несмотря на то что метод wait должен переводить поток в состояние ожидания до тех пор, пока не будет вызван метод notify или notifyAll , иногда поток выводится из состояния ожидания вследствие так называемой ложной активизации. Условия для ложной активизации сложны, возникают редко, а их обсуждение выходит за рамки этой книги. Но в компании Oracle рекомендуют учитывать вероятность проявления ложной активизации и помещать вызов метода wait в цикл. В этом цикле должно проверяться условие, по которому поток переводится в состояние ожидания. Именно такой подход и применяется в рассматриваемом ниже примере. Пример применения методов wait и notify
Для того чтобы стала понятнее потребность в применении методов wait и notify в многопоточном программировании, рассмотрим пример программы, имитирующей работу часов и выводящей на экран слова "Tick" (Тик) и "Тоск" (Так). Для этой цели создадим класс TickTock, который будет содержать два метода: tick и tock . Метод tick выводит слово "Tick", а метод tock — слово "Тоск". При запуске программы, имитирующей часы, создаются два потока: в одном из них вызывается метод tick , а в другом — метод tock . В результате взаимодействия двух потоков на экран будет выводиться набор повторяющихся сообщений "Tick Tock", т.е. после слова "Tick", обозначающего один такт, должно следовать слово "Тоск", обозначающее другой такт часов. // Применение методов wait и notifyO для имитации часов, class TickTock { String state; // содержит сведения о состоянии часов synchronized void tick(boolean running) { if (!running) { // остановить часы state = "ticked"; notifyO; // уведомить ожидающие потоки return; } System.out.print("Tick "); state = "ticked"; // установить текущее состояние после такта "тик" notify; // Метод tick уведомляет метод tock // о возможности продолжить выполнение. try { while(!state.equals("tocked") ) wait;// Метод tick ожидает завершения метода tock. } catch(InterruptedException exc) { System.out.println("Thread interrupted."); } } synchronized void tock(boolean running) { if(!running) { // остановить часы state = "tocked"; notifyO; // уведомить ожидающие потоки return; } System.out.println("Tock"); state = "tocked"; // установить текущее состояние после такта "так" notifyO; // Метод tock уведомляет метод tick // возможности продолжить выполнение. try { while(!state.equals("ticked") ) wait; // Метод tock ожидает завершения метода tick. } catch(InterruptedException exc) { System.out.println("Thread interrupted."); } } } class MyThread implements Runnable { Thread thrd; TickTock ttOb; // построить новый поток MyThread.(String name, TickTock tt) { thrd = new Thread(this, name); ttOb = tt; thrd.start; // начать поток } // начать исполнение нового потока public void run { if(thrd.getName.compareTo("Tick") == 0) { for(int i=0; i<5; i++) ttOb.tick(true); ttOb.tick(false); } else { for(int i=0; i<5; i++) ttOb.tock(true); ttOb.tock(false); } } } class ThreadCom { public static void main(String args[]) { TickTock tt = new TickTock; MyThread mtl = new MyThread("Tick", tt); MyThread mt2 = new MyThread("Tock", tt); try { mtl.thrd.join; mt2.thrd.join; } catch(InterruptedException exc) { System.out.println("Main thread interrupted."); } } }