Рейтинговые книги
Читем онлайн Философия Java3 - Брюс Эккель

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 118 119 120 121 122 123 124 125 126 ... 132

new CircularSet(lOOO); private static ExecutorService exec =

Executors.newCachedThreadPool(), static class SerialChecker implements Runnable { public void run() {

while(true) {

int serial =

Seri alNumberGenerator.nextSeri alNumber(); if(serials.contains(serial)) {

System, out. pri ntl nCDuplicate: " + serial); System.exit(O);

}

serials.add(serial);

}

}

}

public static void main(String[] args) throws Exception { for(int i = 0; i < SIZE, i++)

exec, execute (new SerialCheckerO); // Остановиться после n секунд при наличии аргумента:

if(args length > 0) {

TimeUnit SECONDS sleep(new lnteger(args[0])). System out printin("No duplicates detected"), System exit(0).

}

}

} /* Output Duplicate 8468656 *///•-

В классе SerialNumberChecker содержится статическое поле CircuLarSet, хранящее все серийные номера, и вложенный поток Thread, который получает эти номера и удостоверяется в их уникальности. Создав несколько потоков, претендующих на серийные номера, вы обнаружите, что какой-нибудь из них довольно быстро получит уже имеющийся номер (заметьте, что на вашей машине программа может и не обнаружить конфликт, но на многопроцессорной системе она успешно их нашла). Для решения проблемы добавьте к методу nextSe-rialNumber() слово synchronized.

Предполагается, что безопасными атомарными операциями являются чтение и присвоение примитивов. Однако, как мы увидели в программе Atomi-cityTest.java, все так же просто использовать атомарную операцию для объекта, который находится в нестабильном промежуточном состоянии, так что ожидать, что какие-то предположения оправдаются, опасно и ненадежно.

Атомарные классы

В Java SE5 появились специальные классы для выполнения атомарных операций с переменными — Atomiclnteger, AtomicLong, AtomicReference и т. д. Эти классы содержат атомарную операцию условного обновления в форме

boolean compareAndSer(expectedValue, updateValue),

Эти классы предназначены для оптимизации с целью использования атомарности на машинном уровне на некоторых современных процессорах, поэтому в общем случае вам они не понадобятся. Иногда они применяются и в повседневном программировании, но только при оптимизации производительности. Например, версия AtomicityTest.java, переписанная для использования Atomic-Integer, выглядит так:

// concurrency/AtomicIntegerTest java import java.util concurrent *. import java util concurrent atomic *; import java.util.*.

public class AtomicIntegerTest implements Runnable { private Atomiclnteger i = new AtomicInteger(O), public int getValueO { return i getO. } private void evenIncrement() { i addAndGet(2), } public void runО { while(true)

evenlncrement();

}

public static void main(String[] args) { продолжение &

new TimerO.schedule(new TimerTaskO { public void run() {

System.err println("Aborting"). System exit(O).

}

}, 5000). // Завершение через 5 секунд ExecutorService exec = Executors newCachedThreadPoolO. Atomic I ntegerTest ait = new AtomicIntegerTestO; exec.execute(ait); while(true) {

int val = ait getValueO. if(val % 2 != 0) {

System out.println(val); System.exit(0);

}

}

}

} ///:-

Здесь вместо ключевого слова synchronized используется Atomiclnteger. Так как сбой в программе не происходит, в программу включается таймер, автоматически завершающий ее через 5 секунд.

Вот как выглядит пример MutexEvenGeneratorjava, переписанный для использования класса Atomiclnteger:

//: concurrency/AtomicEvenGenerator.java

// Атомарные классы иногда используются в обычном коде.

// {RunByHand}

import java.util.concurrent.atomic.*;

public class AtomicEvenGenerator extends IntGenerator { private Atomiclnteger currentEvenValue =

new AtomiсInteger(0); public int nextO {

return currentEvenValue.addAndGet(2);

}

public static void main(String[] args) {

EvenChecker.test(new AtomicEvenGeneratorO);

}

} ///.-

Стоит еще раз подчеркнуть, что классы Atomic проектировались для построения классов из java.util.concurrent. Используйте их в своих программах только в особых случаях и только тогда, когда вы твердо уверены, что это не создаст новых проблем. В общем случае безопаснее использовать блокировки (с ключевым словом synchronized или явным созданием объектов Lock).

Критические секции

Иногда необходимо предотвратить доступ нескольких потоков только к части кода, а не к методу в целом. Фрагмент кода, который изолируется таким способом, называется критической секцией (critical section), для его создания также применяется ключевое слово synchronized. На этот раз слово synchronized определяет объект, блокировка которого должна использоваться для синхронизации последующего фрагмента кода:

5упсИгоп12ес1(синхронизируемый0бъект) {

//К такому коду доступ может получить // одновременно только один поток

}

Такая конструкция иначе называется синхронизированной блокировкой (synchronized block); перед входом в нее необходимо получить блокировку для syncObject. Если блокировка уже предоставлена другому потоку, вход в последующий фрагмент кода запрещается до тех пор, пока блокировка не будет снята.

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

//: concurrency/CriticalSection.java

// Синхронизация блоков вместо целых методов. Также демонстрирует защиту

// неприспособленного к многопоточности класса другим классом

package concurrency;

import java.util.concurrent.*:

import java.util.concurrent.atomic.*;

import java.util.*;

class Pair { // Not thread-safe private int x, y; public Pair(int x. int y) { this.x = x; this.у = у;

}

public PairO { this(0, 0); } public int getXO { return x; } public int getYO { return y; } public void incrementXO { x++; } public void incrementYO { y++; } public String toStringO {

return "x; " + x + ", y; " + y;

}

public class PairValuesNotEqualException extends RuntimeException {

public Pai rValuesNotEqual Excepti onO {

superC'Pair values not equal; " + Pair.this);

}

}

// Произвольный инвариант - обе переменные должны быть равны; public void checkStateO { if(x != у)

throw new PairValuesNotEqualException();

}

}

// Защита класса Pair внутри приспособленного к потокам класса; abstract class PairManager {

Atomiclnteger checkCounter = new AtomicInteger(O). protected Pair p = new PairO. private List<Pair> storage =

Collections synchronizedList(new ArrayList<Pair>0). public synchronized Pair getPairO {

// Создаем копию, чтобы сохранить оригинал в безопасност return new Pair(p getXO, p getYO).

}

// Предполагается, что операция занимает некоторое время protected void store(Pair р) { storage add(p), try {

TimeUnit MILLISECONDS sleep(50); } catch(InterruptedException ignore) {}

}

public abstract void incrementO.

}

// Синхронизация всего метода.

class PairManagerl extends PairManager { public synchronized void incrementO { p.incrementXO. p incrementYO. store(getPairO).

// Использование критической секции-class PairManager2 extends PairManager { public void incrementO { Pair temp.

synchronized(this) {

p incrementXO; p. incrementYO; temp = getPairO,

}

store(temp).

class PairManipulator implements Runnable { private PairManager pm; public PairManipulator(PairManager pm) { this pm = pm,

}

public void run О { while(true)

pm. increment);

}

public String toStringO {

return "Pair: " + pm.getPairO +

" checkCounter = " + pm checkCounter.get О;

}

}

class PairChecker implements Runnable { private PairManager pm; public PairChecker(PairManager pm) {

}

public class CriticalSection { // Сравнение двух подходов-static void

testApproaches(PairManager pmanl. PairManager pman2) {

ExecutorService exec = Executors newCachedThreadPool(). PairManipulator

pml = new PairManipulator(pmanl), pm2 = new PairManipulator(pman2), PairChecker

pcheckl = new PairChecker(pmanl), pcheck2 = new PairChecker(pman2), exec execute(pml), exec execute(pm2), exec execute(pcheckl); exec execute(pcheck2), try {

TimeUnit MILLISECONDS sleep(500); } catchdnterruptedException e) {

System out.printlnC'Sleep interrupted"),

}

System.out printin("pml " + pml + "npm2: " + pm2). System exit(O),

}

public static void main(String[] args) { PairManager

pmanl = new PairManagerlO, pman2 = new PairManager2(); testApproaches(pmanl. pman2);

}

} /* Output-

pml- Pair. x. 15, у 15 checkCounter = 272565 pm2- Pair- x. 16, y: 16 checkCounter = 3956974 */// ~

Как было отмечено, класс Pair не приспособлен к работе с потоками, поскольку его инвариант (предположительно произвольный) требует равенства обоих переменных. Вдобавок, как мы уже видели в этой главе, операции инкремента небезопасны в отношении к потокам, и, так как ни один из методов не был объявлен как synchronized, мы не можем считать, что объект Pair останется неповрежденным в многопоточной программе.

Представьте, что вы получили готовый класс Pair, который должен работать в многопоточных условиях. Класс PairManager хранит объекты Pair и управляет любым доступом к ним. Заметьте, что единственными открытыми (public) методами являются getPair(), объявленный как synchronized, и абстрактный метод doTask(). Синхронизация этого метода будет осуществлена при его реализации.

this pm = pm.

}

public void run() {

while(true) {

pm checkCounter.i ncrementAndGet(); pm getPa>r() checkState(),

Структура класса PairManager, в котором часть функциональности базового класса реализуется одним или несколькими абстрактными методами, определенными производными классами, называется на языке паттернов проектирования «шаблонным методом». Паттерны проектирования позволяют инкапсулировать изменения в коде — здесь изменяющаяся часть представлена методом increment(). В классе PairManagerl метод increment() полностью синхронизирован, в то время как в классе PairManager2 только часть его была синхронизирована посредством синхронизируемой блокировки. Обратите внимание еще раз, что ключевые слова synchronized не являются частью сигнатуры метода и могут быть добавлены во время переопределения.

Метод store() добавляет объект Pair в синхронизированный контейнер Array-List, поэтому операция является потоково-безопасной. Следовательно, в защите он не нуждается, поэтому его вызов размещен за пределами синхронизируемого блока.

Класс PairManipulator создается для тестирования двух разновидностей Pair-Manager: метод increment() вызывается в задаче в то время, как в другой задаче работает PairChecker. Метод main() создает два объекта PairManipulator и дает им поработать в течение некоторого времени, после чего выводятся результаты по каждому PairManipulator.

1 ... 118 119 120 121 122 123 124 125 126 ... 132
На этой странице вы можете бесплатно читать книгу Философия Java3 - Брюс Эккель бесплатно.
Похожие на Философия Java3 - Брюс Эккель книги

Оставить комментарий