Философия Java3
Шрифт:
//: concurrency/AtomicityTest.java
import java.util.concurrent.*;
public class AtomicityTest implements Runnable { private int i = 0; public int getValueO { return i; } private synchronized void evenIncrement О { i++, i++. } public void run { while(true)
evenlncrementO;
}
public static void mam(String[] args) {
ExecutorService exec = Executors newCachedThreadPoolО; AtomicityTest at = new AtomicityTest; exec execute(at). while(true) {
int val = at.getValueO; if(val % 2 != 0) {
System.out.println(val), System.exit(0);
} /* Output
191583767
*///:-
Однако
В качестве второго примера рассмотрим кое-что еще более простое: класс, производящий серийные номера25. Каждый раз при вызове метода nextSerialNum-ber он должен возвращать уникальное значение:
//: concurrency/SerialNumberGeneratorJava
public class SerialNumberGenerator {
private static volatile int serial Number = 0, public static int nextSerialNumberО {
return serialNumber++; // Операция не является потоково-безопасной
}
} ///.-
Представить себе класс тривиальнее SerialNumberGenerator вряд ли можно, и если вы ранее работали с языком С++ или имеете другие низкоуровневые навыки, то, видимо, ожидаете, что операция инкремента будет атомарной, так как инкремент обычно реализуется в виде одной инструкции микропроцессора. Однако в виртуальной машине Java инкремент не является атомарным и состоит из чтения и записи, соответственно, ниша для проблем с потоками найдется даже в такой простой программе.
Поле serialNumber объявлено как volatile потому, что каждый поток обладает локальным стеком и поддерживает в нем копии некоторых локальных переменных. Если вы объявляете переменную как volatile, то тем самым указываете компилятору не проводить оптимизацию, а это приведет к удалению чтения и записи, удерживающих поле в соответствии с локальными данными потока. Операции чтения и записи осуществляются непосредственно с памятью без кэширования. Кроме того, volatile не позволяет компилятору изменять порядок обращений с целью оптимизации. И все же присутствие volatile не влияет на тот факт, что инкремент не является атомарной операцией.
Для тестирования нам понадобится множество, которое не потребует переизбытка памяти в том случае, если обнаружение проблемы отнимет много времени. Приведенный далее класс CircularSet многократно использует память, в которой хранятся целые числа (int); предполагается, что к тому моменту, когда запись в множество начинается по новому кругу, вероятность конфликта
с перезаписанными значениями минимальна. Методы add и contains объявлены как synchronized,
//: concurrency/SerialNumberChecker java // Кажущиеся безопасными операции с появлением потоков // перестают быть таковыми. // {Args- 4}
import java util.concurrent *;
// Reuses storage so we don't run out of memory: class CircularSet {
private int[] array: private int len; private int index = 0; public CircularSet(int size) { array = new int[size], len = size.
// Инициализируем значением, которое не производится // классом SerialNumberGenerator for(int i =0; i < size; i++) array[i] = -1;
}
public synchronized void add(int i) { array[index] = i,
// Возврат индекса к началу с записью поверх старых значений: index = ++index % len.
}
public synchronized boolean contains(int val) { for(int i = 0; i < len; i++)
if(array[i] == val) return true; return false;
public class SerialNumberChecker {
private static final int SIZE = 10; private static CircularSet serials =
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 и т. д. Эти классы содержат атомарную операцию условного обновления в форме