×
Traktatov.net » Философия Java » Читать онлайн
Страница 361 из 395 Настройки

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

*///:-

Однако программа находит нечетные значения и завершается. Хотя return i и является атомарной операцией, отсутствие synchronized позволит читать значение объекта, когда он находится в нестабильном промежуточном состоянии. Вдобавок переменная i не объявлена как volatile, а это приведет к проблемам с видимостью. Оба метода, getValue() и evenlncrement(), должны быть объявлены синхронизируемыми. Только эксперты в области параллельных вычислений могут пытаться применять оптимизацию в подобных случаях.

В качестве второго примера рассмотрим кое-что еще более простое: класс, производящий серийные номера>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.