Solving a race condition
Concurrency Strategy

In general, using volatile is complicated, partially because the meaning of volatile changed in Java 1.4, and also because it is hard to spot. I recommend against using it at all. Instead, you should use a more visible concurrency mechanism like synchronization, java.util.concurrent.* classes, java.util.concurrent.locks.*, or java.util.concurrent.atomics.*

So, while I recommend against volatile, what's a real problem is using multiple different locking schemes in the same code, and you use both volatile and synchronized.

The use of synchronization as your concurrency strategy should be fine all on its own in this case.

General


The private final Object lock = new Object() is great. Good to be final, and I prefer that to locking on the instance itself (like synchronized(this)). Makes it more clear.
Println in a synchronized block is almost never a good idea:


synchronized(lock) {
  System.out.println("Received ack Event: " + state);
  state = State.READY;
}



A better solution would be:

State copy = null;
synchronized(lock) {
  copy = state;
  state = State.READY;
}
System.out.println("Received ack Event: " + copy);

Timed sleeps while waiting for a lock are a horrible solution:


Thread.sleep(1);




Better algorithm

The right way to solve this problem, though is to used timed-waits on the lock:

private void handleAck(Event event) {
    State copy = null;
    synchronized(lock) {
      copy = state;
      state = State.READY;
      // tell any waiting threads the state has changed.
      lock.notifyAll();
    }
    System.out.println("Received ack Event: " + copy);
}

/* THIS IS THE IMPORTANT BIT */
private void ensureAck() {
  // how long to wait... 10 milliseconds
  try {
      synchronized(lock) {
          if (state == State.NEW) {
              long waited = 0; // milliseconds
              long start = System.currentTimeMillis();
              while (state == State.NEW &amp;&amp; waited &lt; 10) {
                  lock.wait(10 - waited);
                  waited = System.currentTimeMillis() - start;
              }
          }
      }
  } catch (InterruptedException e) {
      // do more than nothing.....
  }
}


The above strategy does not need anything to be volatile, and it does not 'poll' the state, it returns immediately when the state changes....