Tuesday, November 10, 2009

Threads Updating 64-bit Variables

I've been reading "Java Concurrency in Practice" by Brian Goetz lately, and he described something that I've heard about, but never really been a witness to. In the chapter 3, when describing synchronisation, and atomic operations there is a mention of 64-bit variables. He describes:

"The Java Memory Model requires fetch and store operations to be atomic, but for nonvolatile long and double variables, the JVM is permitted to treat a 64-bit read or write as two separate 32-bit operations. If the reads and writes occur in different threads, it is therefore possible to read a nonvolatile long and get back the high 32-bitsof one value and the low 32 bits of another."

Sounds fair, but I have never really seen this in the flesh. So, I threw together a little program to demonstrate this behaviour.

package com.codewax.threadtests.sixtyfourbit;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.*;

public class Example {

public static void main(String[] args) throws Exception {
displayReport(new Example().runFor(100000));
}

private static void displayReport(Set<Long> results) {
displaySystemProperties("java.version", "java.runtime.version",
"java.vm.version", "java.vm.name");
System.out.println("\nResults");
System.out.println("-------");
System.out.println(results.size() + " alien numbers found");
}

private static void displaySystemProperties(String... propertyNames) {
System.out.println("System Configuration");
System.out.println("--------------------");
for (String propertyName : propertyNames) {
System.out.println(propertyName + "=" +
System.getProperties().getProperty(propertyName));
}
}

private static final long STARTING_NUMBER = 0;
private long source = STARTING_NUMBER;

private Set<Long> runFor(int numberOfOperations) throws Exception {
ExecutorService service = Executors.newCachedThreadPool();
Future<Set<Long>> numbersWritten =
service.submit(writing(numberOfOperations));
Future<Set<Long>> numbersRead =
service.submit(reading(numberOfOperations));
service.shutdown();
service.awaitTermination(10, TimeUnit.SECONDS); //plenty!
return findAlienNumbers(numbersRead.get(), numbersWritten.get());
}

private Set<Long> findAlienNumbers(Set<Long> numbersRead,
Set<Long> numbersWritten) {
numbersRead.remove(STARTING_NUMBER);
numbersRead.removeAll(numbersWritten);
return numbersRead;
}

private Callable<Set<Long>> reading(final int totalOperations) {
return new Callable<Set<Long>>() {
public Set<Long> call() throws Exception {
return new HashSet<Long>() {{
int operationsSoFar = 0;
while (operationsSoFar++ < totalOperations) {
add(source);
}
}};
}
};
}

private Callable<Set<Long>> writing(final int totalOperations) {
return new Callable<Set<Long>>() {
public Set<Long> call() throws Exception {
final Random random = new Random();
return new HashSet<Long>() {{
int operationsSoFar = 0;
while (operationsSoFar++ < totalOperations) {
long next = random.nextLong();
source = next;
add(next);
}
}};
}
};
}

}


This example should be fairly self-explanatory, but for those of you who would like a description, read on.

The main parts of this class are
  1. The writer

  2. The reader

  3. The alien number finder.

(hmm, sounds like a poem).

The writer (1) writes 100000 random numbers to a source 64-bit variable, recording in a set which numbers it wrote along the way.

The reader (2) reads 100000 times from the source variable.

The alien number finder (3) basically removes from the list of read numbers any numbers that were written (and the starting number).

There are a few caveats to this code. One of which being that when the callable's are submitted to the thread pool, they start immediately. The writer thread has some time to write a number of values before the reader thread has time to start reading. On average, by the time the writer thread has completed and written 100000 values, the reader thread will have only read around 4000 values.

Still, I feel that even with these numbers, this represents enough data to see this behaviour.

On to running the code!

First attempt...
agentdh-2:ThreadTests rbarlow$ java -cp out/production/ThreadTests \
com.codewax.threadtests.sixtyfourbit.Example
System Configuration
--------------------
java.version=1.6.0_15
java.runtime.version=1.6.0_15-b03-219
java.vm.version=14.1-b02-90
java.vm.name=Java HotSpot(TM) 64-Bit Server VM

Results
-------
0 alien numbers found


What has happened here? At first glance, it seems that the code must be incorrect-0 alien numbers fonud (alien being numbers that were not written, but were read). But this is not correct. The problem is not with the code, but with the platform! I'm running a MacBook with Snow Leopard installed. Snow Leopard defaults to running a 64-bit version of Java 1.6 (as can be seen by the report output before the results).

Lets swap over to a 32-bit java version, and try again.

Second attempt...
System Configuration
--------------------
java.version=1.6.0_15
java.runtime.version=1.6.0_15-b03-219
java.vm.version=14.1-b02-90
java.vm.name=Java HotSpot(TM) Client VM

Results
-------
319 alien numbers found


Yes! There it is. 319 alien numbers were read during the operation.

This obviously shows that the way that the JVM reads and writes 64 bit values is dependant on the implementation. In the earlier case, the registers used were 64 bit, so there is no need to read or write values in more than one operation.

Of course, none of this would be a problem if you marked the variable as volatile, or used proper synchronisation!

3 comments: