Можно представить однопоточное приложение как автономную сущность, способную перемещаться по пространству задач и выполнять одну операцию за раз. Поскольку существует только одна сущность, никогда не возникает ситуации, когда две сущности пытаются одновременно использовать одинаковый ресурс, как если бы два человека пытались парковаться в одном месте, проходить через одну дверь или говорить одновременно.
Переходя к многопоточной среде, эти сущности больше не являются автономными. Возможна ситуация, когда два или более потока пытаются одновременно получить доступ к одному и тому же ограниченному ресурсу. Такие потенциальные конфликты ресурсов должны быть предотвращены, чтобы избежать ситуаций, таких как одновременный доступ двух потоков к одному банковскому счёту, печать на одной и той же машине или одновременное изменение одного и того же значения.
run()
. Кроме того, мы используем ещё один поток класса Watcher
, который наблюдает за счетчиками и проверяет, равны ли они. Это может показаться бессмысленным действием, поскольку анализируя код можно заключить, что счетчики всегда будут одинаковыми. Однако это не всегда так. Вот первый вариант программы:```java//: Sharing1.java // Проблемы с разделением ресурсов при использовании потоков import java.awt.; import java.awt.event.; import java.applet.*;
class TwoCounter extends Thread { private boolean started = false; private TextField t1 = new TextField(5), t2 = new TextField(5); private Label l = new Label("count1 == count2"); private int count1 = 0, count2 = 0; // Добавляем компоненты отображения в панель: public TwoCounter(Container c) { Panel p = new Panel(); p.add(t1); p.add(t2); p.add(l); c.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) {} } } public void synchTest() { Sharing1.incrementAccess(); if (count1 != count2) l.setText("Unsynced"); } }
```markdown
class Observer extends Thread {
private Share1 p;
public Observer(Share1 p) {
this.p = p;
start();
}
public void execute() {
while(true) {
for(int i = 0; i < p.s.length; i++)
p.s[i].synchronizationTest();
try {
sleep(500);
} catch (InterruptedException e){}
}
}
}
``````java
public class Sharing1 extends Applet {
TwoCounter[] s;
private static int accessCount = 0;
private static TextField aCount = new TextField("0", 10);
public static void incrementAccess() {
accessCount++;
aCount.setText(Integer.toString(accessCount));
}
private Button start = new Button("Start"), observer = new Button("Observe");
private boolean isApplet = true;
private int numCounters = 0;
private int numObservers = 0;
public void init() {
if (isApplet) {
numCounters = Integer.parseInt(getParameter("size"));
numObservers = Integer.parseInt(getParameter("observers"));
}
s = new TwoCounter[numCounters];
for (int i = 0; i < s.length; i++)
s[i] = new TwoCounter(this);
Panel p = new Panel();
start.addActionListener(new StartL());
p.add(start);
observer.addActionListener(new ObserverL());
p.add(observer);
p.add(new Label("Access Count"));
p.add(aCount);
add(p);
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for (int i = 0; i < s.length; i++)
s[i].start();
}
}
class ObserverL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for (int i = 0; i < numObservers; i++)
new Watcher(Sharing1.this);
}
}
public static void main(String[] args) {
Sharing1 applet = new Sharing1();
// Это не апплет, поэтому установите флаг и
Здесь исправлены все ошибки в синтаксисе Java, а также переведены все строки, которые были на китайском языке.```markdown // Производим значения параметров из args: applet.isApplet = false; applet.numCounters = (args.length == 0 ? 5 : Integer.parseInt(args[0])); applet.numObservers = (args.length < 2 ? 5 : Integer.parseInt(args[1])); Frame aFrame = new Frame("Sharing1"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(350, applet.numCounters * 100); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
Как обычно, каждый счетчик содержит свои компоненты отображения: два текстовых поля и одну метку. По их начальным значениям можно понять, что счетчики идентичны. Эти компоненты добавляются в контейнер через конструктор `TwoCounter`. Поскольку этот поток запускается действием пользователя — нажатием кнопки, метод `start()` может быть вызван несколько раз. Однако повторный вызов `Thread.start()` для одного и того же потока является недопустимым (вызовет ошибку). В маркере `started` и переопределенном методе `start()`, можно заметить меры предосторожности против этой ситуации. В методе `run()`, увеличение и отображение значений `count1` и `count2` внешне кажутся одинаковыми, что может создать впечатление их полной синхронизации. Однако затем вызывается метод `sleep()`. Без этого вызова программа выдает ошибку, так как это затрудняет переключение задач между процессорами.
```Метод `synchTest()` выполняет действия, которые могут показаться бессмысленными. Он проверяет, равны ли значения `count1` и `count2`; если они не равны, то метка устанавливается в значение `"Несинхронизировано"`. Вначале он вызывает статический член класса `Sharing1`, чтобы увеличивать и выводить счетчик доступа, указывающий, сколько раз была выполнена эта проверка (причины этого станут очевидны при рассмотрении других версий данного примера).
Класс `Watcher` представляет собой поток, который вызывает метод `synchTest()` для всех активных объектов типа `TwoCounter`. При этом он проходит по массиву, содержащемуся в объектах класса `Sharing1`. Можно представить себе `Watcher` как постоянно "шпионящего" за объектами `TwoCounter`.
Класс `Sharing1` содержит массив объектов типа `TwoCounter`, который инициализируется методом `init()` и запускается как поток после нажатия кнопки `"start"`. После того как нажата кнопка `"Observe"` (наблюдение), создаются один или несколько наблюдателей, которые исследуют неохраняемые объекты `TwoCounter`.
Обратите внимание, что для запуска программы в виде апплета в браузере страница должна содержать следующие строки:
```html
<applet code=Sharing1 width=650 height=500>
<param name=size value="20">
<param name=observers value="1">
</applet>
```Вы можете изменять ширину, высоту и параметры по своему усмотрению для проведения экспериментов. Изменение размера (`size`) и количества наблюдателей (`observers`) влияют на поведение программы. Также стоит отметить, что она спроектирована таким образом, чтобы работать как самостоятельное приложение, принимая параметры командной строки (или используя значения по умолчанию).И вот самое удивительное. В бесконечном цикле метода `TwoCounter.run()` повторяются соседние строки:
```java
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
(как и "сон", но здесь это не важно). Однако, когда программа работает, вы заметите, что количество раз, когда count1
и count2
были "наблюдаемы" (проверены) классом Watcher
, не совпадает! Это происходит из-за сути потока — они могут быть приостановлены в любое время. Поэтому между моментами выполнения этих двух строк иногда происходит приостановка выполнения. В этот момент также может войти поток Watcher
и произвести сравнение, что приводит к тому, что счетчики оказываются несовпадающими. Этот пример демонстрирует очень базовую проблему при использовании потоков. Мы не знаем, когда конкретный поток будет выполняться. Представьте себе, что вы сидите за столом, перед вами ложка, готовая взять последний кусок еды. Когда ложка почти касается еды, она внезапно исчезает (потому что этот поток был приостановлен, а другой поток "украл" еду). Вот какова наша задача.
Иногда нам всё равно, используется ли ресурс другим потоком (еда есть ещё где-то). Но чтобы многопоточная система работала правильно, требуется принять меры для предотвращения доступа двух потоков к одному и тому же ресурсу — по крайней мере, в критический момент.Чтобы избежать таких конфликтов, достаточно заблокировать ресурс во время его использования. Первый поток, который использует ресурс, заблокирует его, и другие потоки не смогут использовать этот ресурс до тех пор, пока он не будет разблокирован. Если передние сиденья автомобиля являются ограниченным ресурсом, то ребёнок, кричащий "это мое!", может заявить права на блокировку этого ресурса.
Для одного специального типа ресурса — памяти объекта — Java предоставляет встроенные механизмы для предотвращения конфликтов. Поскольку мы обычно делаем данные элементы приватными (private
) внутри класса и затем обращаемся к ним через методы, достаточно сделать один определенный метод synchronized
(синхронизированным), чтобы эффективно предотвратить конфликты. В любой момент времени только один поток может вызвать синхронизированный метод определенного объекта (хотя тот же поток может вызывать синхронизированные методы различных объектов). Ниже представлены простые синхронизированные методы:
synchronized void f() { /* ... */ }
synchronized void g() { /* ... */ }
```Каждый объект содержит монитор (или "замок"), который автоматически становится частью объекта (не требуются специальные строки кода для этого). При вызове любого синхронизированного метода объект блокируется, и другие синхронизированные методы того же объекта становятся недоступными, пока первый метод не завершится и не будет разблокирован. В приведённом выше примере, если для объекта вызывается `f()`, то `g()` для того же объекта не может быть вызван, пока `f()` не завершится и не будет разблокирован. Таким образом, все синхронизированные методы одного объекта используют одну общую блокировку, которая предотвращает одновременную запись нескольких потоков в общий участок памяти. Каждый класс также имеет свою собственную локаль (как часть объекта типа `Class` этого класса), поэтому синхронизированные статические методы могут быть взаимно заблокированы в рамках одного класса, чтобы предотвратить доступ к статическим данным.Обратите внимание, что если вы хотите защитить другие ресурсы от одновременного доступа несколькими потоками, можно заставить использовать ключевое слово `synchronized`, чтобы получить доступ к этим ресурсам.
### Синхронизация счетчика
С использованием нового ключевого слова мы можем применять более гибкие подходы: мы можем просто использовать ключевое слово `synchronized` для методов внутри `TwoCounter`. В этом примере представлено изменение предыдущего примера с использованием нового ключевого слова:
```java
//: Sharing2.java
// Использование ключевого слова synchronized для предотвращения
// множественного доступа к определенному ресурсу.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class TwoCounter2 extends Thread {
private boolean started = false;
private TextField
t1 = new TextField(5),
t2 = new TextField(5);
private Label l =
new Label("count1 == count2");
private int count1 = 0, count2 = 0;
public TwoCounter2(Container c) {
Panel p = new Panel();
p.add(t1);
p.add(t2);
p.add(l);
c.add(p);
}
public void start() {
if (!started) {
started = true;
super.start();
}
}
public synchronized void run() {
while (true) {
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
try {
sleep(500);
} catch (InterruptedException e) {}
}
}
public synchronized void synchTest() {
Sharing2.incrementAccess();
if (count1 != count2)
l.setText("Unsynced");
}
}
``````java
class Watcher2 extends Thread {
private Sharing2 p;
public Watcher2(Sharing2 p) {
this.p = p;
start();
}
public void run() {
while (true) {
for (int i = 0; i < p.s.length; i++)
p.s[i].synchTest();
try {
sleep(500);
} catch (InterruptedException e) {}
}
}
}
public class Sharing2 extends Applet {
TwoCounter2[] s;
private static int accessCount = 0;
private static TextField aCount =
new TextField("0", 10);
public static void incrementAccess() {
accessCount++;
aCount.setText(Integer.toString(accessCount));
}
private Button
start = new Button("Start"),
observer = new Button("Observe");
private boolean isApplet = true;
private int numCounters = 0;
private int numObservers = 0;
public void init() {
if(isApplet) {
numCounters =
Integer.parseInt(getParameter("size"));
numObservers =
Integer.parseInt(getParameter("observers"));
}
s = new TwoCounter2[numCounters];
for(int i = 0; i < s.length; i++)
s[i] = new TwoCounter2(this);
Panel p = new Panel();
start.addActionListener(new StartL());
p.add(start);
observer.addActionListener(new ObserverL());
p.add(observer);
p.add(new Label("Access count"));
p.add(aCount);
add(p);
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < s.length; i++)
s[i].start();
}
}
class ObserverL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < numObservers; i++)
new Watcher2(Sharing2.this);
}
}
public static void main(String[] args) {
Sharing2 applet = new Sharing2();
// This is not an applet, so set the flag and
// generate parameter values from arguments:
applet.isApplet = false;
applet.numCounters =
(args.length == 0 ? 5 : Integer.parseInt(args[0]));
applet.numObservers =
(args.length < 2 ? 5 : Integer.parseInt(args[1]));
Frame aFrame = new Frame("Sharing2");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
}
``````java
exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(350, applet.numCounters * 100);
applet.init();
applet.start();
aFrame.setVisible(true);
}
Мы заметили, что как `run()`, так и `synchTest()` являются "синхронизированными". Если синхронизировать только один метод, то другой может игнорировать блокировку объекта и вызываться свободно. Поэтому важно помнить одно важное правило: все методы, доступные к одному общедоступному ресурсу, должны быть объявлены как `synchronized`, в противном случае они могут работать некорректно.
```Сейчас мы столкнулись с новой проблемой. `Watcher2` никогда не сможет видеть текущие изменения, поскольку весь метод `run()` объявлен как "синхронизированный". Поскольку каждый объект будет использовать метод `run()`, блокировка всегда будет заблокирована, а `synchTest()` никогда не будет вызван. Это можно понять по тому факту, что значение `accessCount` вообще не меняется.
Чтобы решить эту проблему, одним из подходов является выделение части кода внутри метода `run()`. Эта часть называется "критической секцией", и она должна использоваться с помощью ключевого слова `synchronized` для установки этой критической секции. Java предоставляет поддержку критических секций через "синхронизированные блоки"; в данном случае ключевое слово `synchronized` используется для указания блокировки объекта для синхронизации заключенного в нем кода:
```java
synchronized(syncObject) {
// Этот код может быть доступен только одной нитью за раз,
// при условии, что все нити уважают блокировку syncObject
}
Для того чтобы войти в синхронизированный блок, необходимо получить блокировку на объекте syncObject
. Если другая нить уже получила эту блокировку, вход в блок невозможен до тех пор, пока эта блокировка не будет освобождена.
Можно удалить ключевое слово synchronized
из всего метода run()
и заменить его синхронизированным блоком, окружающим две критические строки. Таким образом, пример Sharing2
будет модифицирован следующим образом:```java
public void run() {
while (true) {
synchronized(this) {
t1.setText(String.valueOf(count1++));
t2.setText(String.valueOf(count2++));
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
}
}
Это единственная необходимая модификация файла `Sharing2.java`. Мы увидим, что хотя два счетчика никогда не будут выходить из синхронизации (в зависимости от времени проверки `Watcher`), во время выполнения метода `run()` предоставляется достаточно доступа для `Watcher`.
Таким образом, это единственное изменение, которое требуется сделать для файла `Sharing2.java`. Теперь мы можем наблюдать, что хотя два счетчика никогда не будут выходить из синхронизации (в зависимости от времени проверки `Watcher`), во время выполнения метода `run()` предоставляется достаточно доступа для `Watcher`. Конечно, все синхронизации зависят от того, насколько старательны программисты: каждая часть кода, которая должна получить доступ к общему ресурсу, должна быть заключена в подходящий синхронизированный блок.
### 14.2.3 Обзор Java Beans
Теперь, когда мы понимаем синхронизацию, можно рассмотреть Java Beans с другой точки зрения. Всякий раз, когда создается Bean, следует предположить, что он будет работать в многопоточной среде. Это означает:(1) По возможности все публичные методы Bean должны быть синхронизированы. Конечно, это также привносит "надбавку" по времени выполнения синхронизации. Если вы особенно заботитесь об этом вопросе, методы, которые не вызывают проблем конкуренции, могут быть сделаны "несинхронизированными", но учтите, что это обычно не так легко определить. Подходящие методы часто имеют небольшой размер (например, `getCircleSize()`) и/или являются "микроскопическими". То есть, этот метод выполняется таким малым количеством кода, что объект не может измениться во время его выполнения. Если такой метод сделать "несинхронизированным", это может не оказывать заметного влияния на скорость выполнения программы. Также можно сделать все публичные методы Bean синхронизированными и удалить ключевое слово `synchronized`, только если это действительно необходимо и обеспечивает значительное различие.(2) Если несколько событий передаются последовательно группе слушателей, интересующихся этим событием, следует предположить, что при перемещении по списку слушателей новые слушатели могут быть добавлены или существующие удалены.
Первый пункт легко реализовать, но второй требует более глубокого анализа. Давайте возьмём за пример `BangBean.java` из прошлой главы. В этом примере мы игнорировали ключевое слово `synchronized` (которое тогда ещё не было введено) и установили преобразование как одиночное, чтобы избежать проблем многопоточности. В следующей модифицированной версии мы делаем его способным работать в многопоточной среде и используем технику множественного преобразования для событий:
```markdown
//: BangBean2.java
// Вы должны писать свои Beans таким образом, чтобы они
// могли работать в многопоточной среде.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
public class BangBean2 extends Canvas
implements Serializable {
private int xm, ym;
private int cSize = 20; // Размер окружности
private String text = "Bang! ";
private int fontSize = 48;
private Color tColor = Color.red;
private Vector actionListeners = new Vector();
public BangBean2() {
addMouseListener(new ML());
addMouseMotionListener(new MM());
}
public synchronized int getCircleSize() {
return cSize;
}
public synchronized void setCircleSize(int newSize) {
cSize = newSize;
}
public synchronized String getBangText() {
return text;
}
public synchronized void setBangText(String newText) {
text = newText;
}
}
``` public synchronized int getFontSize() {
return fontSize;
}
public synchronized void setFontSize(int newSize) {
fontSize = newSize;
}
public synchronized Color getTextColor() {
return tColor;
}
public synchronized void setTextColor(Color newColor) {
tColor = newColor;
}
public void paint(Graphics g) {
g.setColor(Color.BLACK);
g.drawOval(xm - cSize / 2, ym - cSize / 2, cSize, cSize);
}
// Этот многопоточный слушатель используется чаще, чем однопоточный подход в BangBean.java:
public synchronized void addActionListener(ActionListener l) {
actionListeners.addElement(l);
}
public synchronized void removeActionListener(ActionListener l) {
actionListeners.removeElement(l);
}
// Обратите внимание, что этот метод не синхронизирован:
public void notifyListeners() {
ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null);
Vector lv = null;
// Создаем копию вектора, так как кто-то может добавить слушатель во время вызова слушателей:
synchronized(this) {
lv = (Vector)actionListeners.clone();
}
// Вызываем все методы слушателя:
for(int i = 0; i < lv.size(); i++) {
ActionListener al = (ActionListener)lv.elementAt(i);
al.actionPerformed(a);
}
} class ML extends MouseAdapter {
public void mousePressed(MouseEvent e) {
Graphics g = getGraphics();
g.setColor(tColor);
g.setFont(new Font("TimesRoman", Font.BOLD, fontSize));
int width = g.getFontMetrics().stringWidth(text);
g.drawString(text, (getSize().width - width) / 2,
getSize().height / 2);
g.dispose();
notifyListeners();
}
}
class MM extends MouseMotionAdapter {
public void mouseMoved(MouseEvent e) {
xm = e.getX();
ym = e.getY();
repaint();
}
}
// Testing the BangBean2:
public static void main(String[] args) {
BangBean2 bb = new BangBean2();
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("ActionEvent" + e);
}
});
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("BangBean2 action");
}
});
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("More action");
}
});
Frame aFrame = new Frame("BangBean2 Test");
aFrame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(bb, BorderLayout.CENTER);
aFrame.setSize(300, 300);
aFrame.setVisible(true);
}
} ///:~
Добавление synchronized
к методам может быть легко выполнено. Однако обратите внимание, что в методах addActionListener()
и removeActionListener()
, где теперь добавляется ActionListener
, а также удаляется из Vector
, можно использовать любое количество слушателей по своему желанию.Обратите внимание, что метод notifyListeners()
не был объявлен как synchronized
. Этот метод может вызываться из нескольких потоков одновременно. Кроме того, во время выполнения notifyListeners()
могут быть вызваны методы addActionListener()
и removeActionListener()
. Это создаёт проблемы, поскольку это нарушает целостность Vector actionListeners
. Чтобы решить эту проблему, мы "клонируем" Vector
внутри synchronized
блока и выполняем операцию над клоном. Таким образом, можно манипулировать Vector
без влияния на notifyListeners()
.
Метод paint()
также не был объявлен как synchronized
. В отличие от простого добавления своего метода, решение о том, следует ли сделать синхронизированным переопределённый метод, является более сложной задачей. В этом примере, независимо от того, является ли paint()
синхронизированным или нет, он кажется работать корректно. Тем не менее, существуют вопросы, которые следует учитывать:
(1) Метод изменяет состояние "критических" переменных внутри объекта? Для определения того, является ли переменная "критической", необходимо знать, используется ли она другими потоками программы для чтения или записи (в данном случае, чтение или запись почти всегда происходит через синхронизированные методы, поэтому проверка требуется только для них). В случае paint()
, никаких изменений не происходит.(2) Метод зависит от состояния этих "критических" переменных? Если синхронизированный метод изменяет переменную, которую наш метод использует, то обычно лучше сделать свой метод тоже синхронизированным. На основе этого можно заметить, что cSize
изменяется с помощью синхронизированного метода, поэтому paint()
должен быть синхронизированным. Но здесь можно спросить: "Какой будет худший случай, если cSize
изменится во время выполнения paint()
?" Если выяснится, что последствия не слишком серьёзны и являются временным эффектом, то лучше сохранить paint()
в неконтролируемом состоянии, чтобы избежать дополнительных затрат на вызовы синхронизированных методов. Третьей заметной нитью является то, синхронизирован ли базовый класс paint()
. В данном случае он не синхронизирован. Это не строгое требование, а просто "нить". Например, в текущей ситуации поле, изменённое с помощью синхронизированного метода (например, cSize
), было включено в формулу paint()
, что могло бы повлиять на ситуацию. Однако обратите внимание, что synchronized
не наследуется — другими словами, если метод синхронизирован в базовом классе, его переопределение в производном классе не будет автоматически синхронизировано. Тестовый код в TestBangBean2
был модифицирован на основе предыдущей главы, добавив дополнительных "слушателей", что демонстрирует многомерные возможности преобразования BangBean2
.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )