How to demonstrate race conditions around values that aren't published properly?
I am reading "Java Concurrency in practice" and looking at the example code on page 51.
According to the book this piece of code is at risk of of failure if it has not been published properly. Because I like to code examples and break them to prove how they work. I have tried to make it throw an AssertionError but have failed. (Leading me to my previous question)
Can anyone post sample code so that an AssertionError is thrown? Rule: Do not modify the Holder class.
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");
}
}
}
I have modified the class to make it more fragile but I still can not get an AssertionError thrown.
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");
}
}
}
Is it possible to make either of the above classes throw an AssertionError? Or do we have to accept that they may occasionally do so and we can't write code to prove it?
I'd run this on a multiprocessor machine for a few hours and see what happens(remove the sleep if you use your Holder2). Such race conditions might be rare, or not existant on your particular machine - but atleast try to provoke these one on a million cases , by trying millions of times.
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++);
}
}
}
}
As BobbyShaftoe said in the other thread, you can't rely on just running the code enough times to show that the error can or cannot happen. If you think about this from an Assembly level, it will be very hard for n != n as it is so few calls and relies on the process being switched out at a really precise time.
If you want to be able to show whether a concurrent system is provably valid it would be better to model it using something like Labelled Transition Systems. Try the LTSA tool if you're interested in proving concurrency or finding errors.
http://www.doc.ic.ac.uk/ltsa/
In the example the that book is giving the Holder class is not directly the cause of the problem, in fact it states that:
The problem here is not the Holder class itself, but that the Holder is not properly published. However, Holder can be made immune to improper publication by declaring the n field to be final, which would make Holder immutable; see Section 3.5.2.
Just prior to this it mentions the following code, which it the subject of the problem:
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
So to re-create it you will need to create a publisher class and two threads, one that calls initialize and one that calls the assert.
Having said that, I tried to re-create it myself and still failed to do so :(
Below is my first attempt, however there is a better explanation of the problem at 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);
}
}
}
I don't think the assertion error can occur without modifying the Holder
class. I think the book is wrong.
The only reason to cause the assertion error is when assertSanity() is called on a partially constructed object. How can a thread, other than the constructor thread, reference a partially constructed object? AFAIK, it's only possible in the following two cases:
- Publish
this
in the constructor. E.g. assignthis
to a shared variable. This can't happen in our sample code because Holder's constructor doesn't do that. - A class's non-static inner class can refer to its parent even when its parent is partially constructed. This can't happen either because Holder doesn't have any inner class.
Note that the following code in the book doesn't publish any partially constructed object:
public class GoodCode {
public Holder holder;
public void initialize () {
holder = new Holder(42);
}
}
If you disassemble initialize()
, you get the following:
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
Note that putfield holder
executes after invokespecial <init>
. This means the assignment of holder
happens after the constructor is completed. The partially constructed object is only stored in the thread's stack. It's not published.
If you can trigger the assertion error in a reasonable way (e.g. reflection is not reasonable), put it here. I will up vote you.
You cant change value of n
at any time by using:
Holder h = new Holder(5);
Field f = h.getClass().getDeclaredField("n");
f.setAccessible(true);
f.setInt(h, 10);
h.assertSanity();
精彩评论