Thursday, October 12, 2006

Conflating Atomicity with Thread-Safety

I've been adding a thread pool to Qwicap (in the unreleased version 1.4a40), and, with it, a lot of machinery for gathering information about the pool, the threads, the rate at which they're processing hits, etc. The thread pool implementation, of course, depends on careful use of synchronization for its correctness. Because synchronization can be costly, I tried to minimize its use in the information gathering machinery. I thought I had a lot of scope for such minimization due to the fact that operations on Java primitives that occupy 32 bits, or less, are atomic. So I did things like declaring various counters as "int" types and using the increment operator to change them, e.g. "ThreadCount++;", without bothering to execute them inside an appropriately synchronized block.

Very quick, but, during testing I soon realized that it was also very wrong.

A run of javap made the problem obvious. The "ThreadCount++;" statement, where "ThreadCount" is an instance variable, compiles into the following Java instructions:

   2:   getfield        #2; //Field ThreadCount:I
   5:   iconst_1
   6:   iadd
   7:   putfield        #2; //Field ThreadCount:I

Each of those instructions is operating on an integer primitive, and is therefore operating atomically, but, of course, there are four instructions, and if other threads are executing any of the same instructions at the same time, you can loose the effect of one or more of the increment operations. So, operations on 32-bit, and smaller, primitives are atomic in the JVM, but this fact does nothing to help you avoid synchronization.

My dim recollections of 68000 assembly language programming seem to have steered me in the wrong direction in this case, by leaving me convinced that an operation as simple and common as "ThreadCount++;" would probably be represented by a single instruction. Wrong. And returning to my 68000 documentation, I see that I must have been thinking of facilities like the "address register indirect post-increment" addressing mode, which was nifty, but also wouldn't have allowed that increment operation to be represented as a single 68000 instruction. Wrong again. That'll teach me.

By the way, am I the only old C programmer who wishes from time to time that the "asm" statement had been carried over into Java? I mean, once you have a platform independent assembly language, why not? I admit that the practical value would be low, and code readability would suffer, [OK, that's two good "why nots" right there] but I'd have found it very educational. Having actually used the JVM instruction set (which I wouldn't have been able to resist), rather than having merely read about it, I certainly would have known better than to believe that my increment operation would execute atomically.

1 comment:

  1. [Chris: My apologies if this is a duplicate. Blogger gave me no response when I originally posted it.]

    Inline assembly can be problematic because it can run afoul of compiler optimizations. And in a stack-based virtual CPU like the JVM, leaving the operand stack in the wrong state can have surprising consequences. Better to have separate assembly-only classes.

    That's been possible for some time with the Jasmin assembler (note: no "e" at the end of "Jasmin"). There have been other bytecode assemblers such as Jasml and the more stylized Jamaica, but as far as I know Jasmin is still the gold standard.

    ReplyDelete