Как продемонстрировать условия гонки вокруг значений, которые не опубликованы должным образом?
Я читаю "Параллелизм Java на практике" и смотрю пример кода на странице 51.
Согласно книге, этот кусок кода может потерпеть неудачу, если он не был опубликован должным образом. Потому что я люблю кодировать примеры и ломать их, чтобы доказать, как они работают. Я пытался заставить его бросить AssertionError, но не удалось. (Ведет меня к моему предыдущему вопросу)
Может кто-нибудь опубликовать пример кода, чтобы выбрасывалось AssertionError? Правило: не изменяйте класс Holder.
public class Holder{
private int n;
public Holder(int n){
this.n = n;
}
public void assertSanity(){
if (n != n) {
throw new AssertionError("This statement is false");
}
}
}
Я изменил класс, чтобы сделать его более хрупким, но я все еще не могу получить брошенную ошибку AssertionError.
class Holder2 {
private int n;
private int n2;
public Holder2(int n) throws InterruptedException{
this.n = n;
Thread.sleep(200);
this.n2 = n;
}
public void assertSanity(){
if (n != n2) {
throw new AssertionError("This statement is false");
}
}
}
Можно ли заставить любой из вышеперечисленных классов генерировать ошибку AssertionError? Или мы должны признать, что они могут иногда это делать, и мы не можем написать код, чтобы доказать это?
5 ответов
Я бы запустил это на многопроцессорной машине в течение нескольких часов и посмотрел, что произойдет (снимите спящий режим, если вы используете свой Holder2). Такие условия гонки могут быть редкими или не существовать на вашей конкретной машине - но, по крайней мере, попытайтесь спровоцировать их на миллион случаев, пытаясь миллионы раз.
class Checker {
private Holder h;
public Checker() {
h = new Holder(42);
}
public void check() {
h.assertSanity();
}
public void create(int n) {
h = new Holder(n);
}
}
public class MyThread extends thread{
private bool check;
private final Checker c;
public MyThread(bool check,Checker c) {
this.check = check;
this.c = c;
}
public static void main(String[] args) {
Checker c = new Checker();
MyThread t1 = new MyThread(false,c);
MyThread t2 = new MyThread(true,c);
t1.start();
t2.start();
t1.join();
t2.join();
}
public void run() {
int n = 0;
while(true) {
if(check)
c.check();
else
c.create(n++);
}
}
}
}
В примере, который эта книга дает классу Холдера, не является непосредственной причиной проблемы, фактически говорится, что:
Проблема здесь не в самом классе Holder, а в том, что Holder не публикуется должным образом. Тем не менее, Holder может быть защищен от неправильной публикации, если объявить поле n окончательным, что сделает Holder неизменным; см. раздел 3.5.2.
Непосредственно перед этим упоминается следующий код, который является предметом проблемы:
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
Поэтому для его повторного создания вам нужно создать класс издателя и два потока: один, который вызывает инициализацию, и другой, который вызывает утверждение.
Сказав это, я попытался воссоздать его сам, но так и не смог:(
Ниже моя первая попытка, однако, есть лучшее объяснение проблемы на http://forums.oracle.com/forums/thread.jspa?threadID=1140814&tstart=195
public class HolderTest {
@Test
public void testHolder() throws Exception {
for (int i = 0; i < 1000000000; i++) {
final CountDownLatch finished = new CountDownLatch(2);
final HolderPublisher publisher = new HolderPublisher();
final Thread publisherThread = new Thread(new Publisher(publisher,
finished));
final Thread checkerThread = new Thread(new Checker(publisher,
finished));
publisher.holder = null;
publisherThread.start();
checkerThread.start();
finished.await();
}
}
static class Publisher implements Runnable {
private final CountDownLatch finished;
private final HolderPublisher publisher;
public Publisher(final HolderPublisher publisher,
final CountDownLatch finished) {
this.publisher = publisher;
this.finished = finished;
}
@Override
public void run() {
try {
publisher.initialize();
} finally {
finished.countDown();
}
}
}
static class Checker implements Runnable {
private final CountDownLatch finished;
private final HolderPublisher publisher;
public Checker(final HolderPublisher publisher,
final CountDownLatch finished) {
this.publisher = publisher;
this.finished = finished;
}
@Override
public void run() {
try {
publisher.holder.assertSanity();
} catch (final NullPointerException e) {
// This isnt the error we are interested in so swallow it
} finally {
finished.countDown();
}
}
}
static class HolderPublisher {
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
}
}
Как сказал BobbyShaftoe в другом потоке, нельзя полагаться на то, что достаточно просто запустить код достаточно, чтобы показать, что ошибка может или не может произойти. Если вы подумаете об этом на уровне сборки, это будет очень трудно для n!= N, так как это очень мало вызовов и зависит от процесса, который переключается в действительно точное время.
Если вы хотите показать, является ли параллельная система достоверно действительной, то лучше смоделировать ее, используя что-то вроде маркированной системы переходов. Попробуйте инструмент LTSA, если вы заинтересованы в проверке параллелизма или нахождении ошибок.
Я не думаю, что ошибка утверждения может произойти без изменения Holder
учебный класс. Я думаю, что книга не права.
Единственная причина, по которой возникает ошибка утверждения, - это когда assertSanity() вызывается для частично сконструированного объекта. Как может поток, кроме потока конструктора, ссылаться на частично созданный объект? AFAIK, это возможно только в следующих двух случаях:
- Публиковать
this
в конструкторе. Например, назначитьthis
к общей переменной. В нашем примере кода такого не может быть, потому что конструктор Holder этого не делает. - Нестатический внутренний класс класса может ссылаться на своего родителя, даже если его родитель частично создан. Этого также не может быть, потому что у Холдера нет внутреннего класса.
Обратите внимание, что следующий код в книге не публикует какой-либо частично созданный объект:
public class GoodCode {
public Holder holder;
public void initialize () {
holder = new Holder(42);
}
}
Если вы разберете initialize()
, вы получите следующее:
public void initialize();
Code:
0: aload_0
1: new #2 // class Holder
4: dup
5: bipush 42
7: invokespecial #3 // Method Holder."<init>":(I)V
10: putfield #4 // Field holder:LHolder;
13: return
Обратите внимание, что putfield holder
выполняется после invokespecial <init>
, Это означает назначение holder
происходит после завершения конструктора. Частично построенный объект хранится только в стеке потока. Это не опубликовано.
Если вы можете вызвать ошибку утверждения разумным способом (например, отражение не является разумным), укажите это здесь. Я буду голосовать за тебя.
Вы не можете изменить значение n
в любое время, используя:
Holder h = new Holder(5);
Field f = h.getClass().getDeclaredField("n");
f.setAccessible(true);
f.setInt(h, 10);
h.assertSanity();