Доказательство следующего кода небезопасно
Как я могу быстро доказать, что следующий класс не является потокобезопасным (поскольку он использует Lazy Initialization и не использует синхронизацию) путем написания кода?
Другими словами, если я тестирую следующий класс для безопасности потоков, как я могу его терпеть?
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
Ответы
Ответ 1
Ну... Результат этого кода будет ложным, где вы ожидаете истинного.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class LazyInitRace {
public class ExpensiveObject {
public ExpensiveObject() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
public static void main(String[] args) {
final LazyInitRace lazyInitRace = new LazyInitRace();
FutureTask<ExpensiveObject> target1 = new FutureTask<ExpensiveObject>(
new Callable<ExpensiveObject>() {
@Override
public ExpensiveObject call() throws Exception {
return lazyInitRace.getInstance();
}
});
new Thread(target1).start();
FutureTask<ExpensiveObject> target2 = new FutureTask<ExpensiveObject>(
new Callable<ExpensiveObject>() {
@Override
public ExpensiveObject call() throws Exception {
return lazyInitRace.getInstance();
}
});
new Thread(target2).start();
try {
System.out.println(target1.get() == target2.get());
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}
}
Ответ 2
По определению условия гонки не могут быть проверены детерминистически, если только вы не контролируете планировщик потоков (чего у вас нет). Самое близкое, что вы можете сделать, это либо добавить настраиваемую задержку в методе getInstance()
, либо написать код, в котором проблема может проявляться и запускаться тысячами раз в цикле.
Кстати, ничто из этого действительно не является "доказательством". Формальная проверка, но очень и очень сложно сделать даже для относительно небольших количеств кода.
Ответ 3
Можете ли вы заставить ExpensiveObject
занять много времени, чтобы построить в своем тесте? Если это так, просто вызовите getInstance()
дважды из двух разных потоков, за короткое время, когда первый конструктор не будет выполнен до того, как будет сделан второй вызов. В итоге вы создадите два разных экземпляра, в которых вы должны потерпеть неудачу.
Выполнение наивной двойной проверки блокировки будет сложнее, заметьте... (хотя это не безопасно без указания volatile
для переменной).
Ответ 4
Это не использует код, но вот пример того, как я это докажу. Я забываю стандартный формат для диаграмм выполнения, подобных этому, но значение должно быть достаточно очевидным.
| Thread 1 | Thread 2 |
|-----------------------|-----------------------|
| **start** | |
| getInstance() | |
| if(instance == null) | |
| new ExpensiveObject() | |
| **context switch ->** | **start** |
| | getInstance() |
| | if(instance == null) | //instance hasn't been assigned, so this check doesn't do what you want
| | new ExpensiveObject() |
| **start** | **<- context switch** |
| instance = result | |
| **context switch ->** | **start** |
| | instance = result |
| | return instance |
| **start** | **<- context switch** |
| return instance | |
Ответ 5
Так как это Java, вы можете использовать библиотеку thread-weaver для ввода пауз или разрывов в ваш код и управления несколькими потоками выполнения, Таким образом, вы можете получить медленный конструктор ExpensiveObject
без необходимости изменять код конструктора, как и другие (правильно).
Ответ 6
Положите очень длинный расчет в конструкторе:
public ExpensiveObject()
{
for(double i = 0.0; i < Double.MAX_VALUE; ++i)
{
Math.pow(2.0,i);
}
}
Возможно, вы захотите уменьшить условие завершения до Double.MAX_VALUE/2.0
или делить на большее число, если MAX_VALUE
занимает слишком много времени для вашей симпатии.
Ответ 7
Вы можете легко доказать это с помощью отладчика.
- Напишите программу, которая вызывает
getInstance() на двух отдельных
потоки.
- Установить точку останова на конструкции
ExpensiveObject. Убедиться
отладчик только приостанавливает
thread, а не VM.
- Затем, когда первый поток останавливается
точку останова, оставьте ее приостановленной.
- Когда второй поток останавливается, вы просто продолжаете.
- Если вы проверите результат
getInstance() для обоих потоков,
они будут ссылаться на разные
экземпляров.
Преимущество такого способа заключается в том, что на самом деле вам не нужен объект ExpensiveObject, любой объект фактически даст те же результаты. Вы просто используете отладчик, чтобы запланировать выполнение этой конкретной строки кода и тем самым создать детерминированный результат.
Ответ 8
Ну, это не потокобезопасно.
Доказательство безопасности потока является случайным, но довольно простым:
-
Сделать конструктор ExpensiveObject полностью безопасным:
synchronized ExpensiveObject() {...
-
Поместите в код конструктора, который проверяет, существует ли другая копия объекта - затем создайте исключение.
-
Создайте потокобезопасный метод очистки переменной "instance"
-
Поместите последовательный код getInstance/clearInstance в цикл для выполнения несколькими потоками и ожидайте исключение из (2)