Философия Java

         

Неправильный доступ к ресурсам


Рассмотрим изменение значения счетчиков, использованных в данной главе. В следующем примере каждый процесс имеет два счетчика, которые увеличивают свои значения и отображаются внутри вызова run(). Дополнительно существует другой процесс класса Watcher, который отслеживает равенство значений показаний счетчиков. Это выглядит как необязательное дополнение, поскольку посмотрев на исходный код можно предположить, что значения счетчиков всегда будут одинаковые. Однако нас ждут сюрпризы. Ниже приведена первая версия программы:

//: c14:Sharing1.java

// Problems with resource sharing while threading.

// <applet code=Sharing1 width=350 height=500>

// <param name=size value="12">

// <param name=watchers value="15">

// </applet>

import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;

public class Sharing1 extends JApplet { private static int accessCount = 0; private static JTextField aCount = new JTextField("0", 7); public static void incrementAccess() { accessCount++; aCount.setText(Integer.toString(accessCount)); } private JButton start = new JButton("Start"), watcher = new JButton("Watch"); private boolean isApplet = true; private int numCounters = 12; private int numWatchers = 15; private TwoCounter[] s; class TwoCounter extends Thread { private boolean started = false; private JTextField t1 = new JTextField(5), t2 = new JTextField(5); private JLabel l = new JLabel("count1 == count2"); private int count1 = 0, count2 = 0; // Add the display components as a panel:

public TwoCounter() { JPanel p = new JPanel(); p.add(t1); p.add(t2); p.add(l); getContentPane().add(p); } public void start() { if(!started) { started = true; super.start(); } } public void run() { while (true) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public void synchTest() { Sharing1.incrementAccess(); if(count1 != count2) l.setText("Unsynched"); } } class Watcher extends Thread { public Watcher() { start(); } public void run() { while(true) { for(int i = 0; i < s.length; i++) s[i].synchTest(); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < s.length; i++) s[i].start(); } } class WatcherL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < numWatchers; i++) new Watcher(); } } public void init() { if(isApplet) { String counters = getParameter("size"); if(counters != null) numCounters = Integer.parseInt(counters); String watchers = getParameter("watchers"); if(watchers != null) numWatchers = Integer.parseInt(watchers); } s = new TwoCounter[numCounters]; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new TwoCounter(); JPanel p = new JPanel(); start.addActionListener(new StartL()); p.add(start); watcher.addActionListener(new WatcherL()); p.add(watcher); p.add(new JLabel("Access Count")); p.add(aCount); cp.add(p); } public static void main(String[] args) { Sharing1 applet = new Sharing1(); // This isn't an applet, so set the flag and


// produce the parameter values from args:

applet.isApplet = false; applet.numCounters = (args.length == 0 ? 12 : Integer.parseInt(args[0])); applet.numWatchers = (args.length < 2 ? 15 : Integer.parseInt(args[1])); Console.run(applet, 350, applet.numCounters * 50); } } ///:~

Как и прежде, каждый счетчик содержит свой собственный компонент для отображения значения: два текстовых поля и надпись, первоначально показывающую что счетчики равны. Эти компоненты добавляются на панель родительского объекта в конструкторе TwoCounter. Так как два процесса начинают выполнение после нажатия пользователем кнопки, можно сделать так, чтобы start() мог быть вызван более одного раза. Так как Thread.start( ) не может быть вызван более одного раза для процесса (иначе генерируется исключение), то в приведенном алгоритме переопределен метод start() и используется флаг started.

В вызове run(), функции count1 и count2 увеличивают и отображают значение, так, что все кажется идентично. Затем вызываетсяsleep( ); без этого вызова программа "повиснет" поскольку CPU будет трудно переключаться между процессами.

Метод synchTest( ) выполняет очевидные функции по сравнению на равенство значения счетчиков count1 и count2; если они не равны то он установит значение надписи на панели в "Unsynched". Но в начале, он вызывает статический член класса Sharing1, который увеличит и отобразит значение счетчика доступа, чтобы показать сколько раз проверка закончилась успешно. (Причина использования данного счетчика будет понятна из следующих примеров.)

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

Sharing1 содержит массив объектов TwoCounter инициализируемый при init() и запускаемый как процесс когда нажимается кнопка "start". Позже, когда будет нажата кнопка "Watch", создаются два или более наблюдателя и уничтожают ничего неподозревающие процессы TwoCounter.



Запомните, чтобы запустить данный пример как апплет в броузере, в вызове апплета должны быть следующий строки:

<param name=size value="20">

<param name=watchers value="1">

Можете экспериментировать изменяя значение высоты и ширины и прочие параметры. Изменяя size и watchers вы изменяете поведение программы. Данная программа настроена на выполнение как одиночное приложение с передачей всех параметров через командную строку (или с использованием значений по умолчанию).

А вот и наиболее интересная часть. В вызове TwoCounter.run(), бесконечный цикле просто повторяет следующие строки:

t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++));

(так же как и sleep, но здесь это не важно). Однако, когда программа будет запущена, вы увидите, что значения count1 и count2 будут временами различны (что покажет Watcher)! Это связано с особенностями процесса, он может быть временно приостановлен  в любое время. Таким образом в то время, когда приостановка произошла при выполнение двух приведенных выше строк, а процесс Watcher произвел сравнение как раз в это время, то как раз два счетчика и будут различны.

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

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

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


Содержание раздела