S
Saxo
You'll still have the problem that threads T1 and T2 could get()
different values from the same Node, and both be working with those
conflicting values at the same time. An "instantaneous" change will
not avoid the issue -- so either (a) it better not be an issue, or
(b) you need The Mother Of All Locks around the whole shebang ...
Also, the problem ordinarily addressed by a "commit" mechanism
is somewhat different: You want to make changes to Nodes N1 and N2,
and you want to make sure an observer sees either the two old values
or the two new values, but never a mixture. Again, "instantaneous"
change won't solve the problem, not even in the simple case of just
one mutator and one observer:
Observer: Fetch the N1 value, then ...
Mutator: CHANGE THE WORLD INSTANTANEOUSLY
Observer: ... fetch the N2 value
For the observer's view of N1 and N2 to be consistent, it's not
enough that each get() be guarded against interference; you need
the pair of get()'s guarded as a unit. Very likely, you also need
the changes to N1 and N2 guarded as a unit.
Eric Sosman
All right, I see that the issue cannot be understood without
explaining mmore of the whole thing. What I want to develop is a
transactional concurrent map that is protected by an optimistic
locking mechanism. For every such map there is backing map that
carries the consistent snapshots from commit to commit. The user
interacts with the backing map through a locle cache:
BackingMap backingMap = new BackingMap();
LocalMap map1 = new LocalMap(backingMap);
map1.begin();
Node node1 = map1.get(key1); // line x - this is the get from my
initial post, augmented by a key for the map value lookup
Node node2 = map1.get(key1); // line y
node2.value = newValue;
Node node3 = map1.putAndGet(key1, node2);
map1.commit(); // or map1.mergeCommit();
The node objects hold a lock counter to determine whether an
optimistic locking conflict has occured. When map1.get(key) is done
the node object from the corresponding node object in the backing map
is copied and returned. It is then stored in the local cache with the
lock counter in the state when the original node from the backing map
was copied. When another time get is done as in line y, the same value
is returned as in line x even when it has meanwhile changed in the
backing map as on a repeated get for a node already in the local
cache, only the node in the local cache is returned. You can think of
adding a getRepeatableRead(key) method that refetches the current
value from the backing map with the current value of the lock counter.
But this is something for later considerations.
Until the commit is done all changes are held in a local change list.
When the commit is done, the >single< committing thread checks for
every change object in the change list whether the lock counter is
still the same as in the backing map. If so, the values of the change
list are transfered to the backing map as explained in my initial
post. If not, an optimistic locking exception is thrown and the local
change list is thrown away. In case of map1.commit() the commit fails
in case the backing map has changed in any way whether this concerns
the value for key1 or not. In case of map1.mergeCommit() the commit
only fails if the value for key1 has changed meanwhile from within a
different thread.
I now need to return to the get method of my initial post:
public Object get() {
synchronized(lock) {
if(useNewValue.get()) // 1
return newValue; // 2
return previousValue; // 3
}
}
Let's say the map1.commit() is done as in the example at the beginning
of this post. During the commit phase some other thread x calls
get(key1). When for the commit no optimistic locking conflict has been
detected and the new values have been transfered from the change list,
the atomic boolean useNewValue is set to true. Let's say at this time
thread x were in line 3 (see line numbers in the example above). Then
the meanwhile outdated value previousValue would be returned. But
strictly speaking, chronologically thread x has by passed line 1 at
the time useNewValue was changed to true. So seen from this
perspective there is no true race condition here. Now, if we assume
that what is returned from the synchronized block must be inline with
the value in the backing map, there is a race condition. True. On the
other hand, the purpose of the synchronized block is only to make sure
that get() and setNewValue() are not called at the same time. When a
commit is done with the situation as described in this block, an
optimistic locking conflict would occur and the data would return
consistent. Now you can argue whether the optimistic locking exception
in this case is correct or should not have happened. In my opinion, in
the situation just described, thread x was at the critical line 1
chronological order was retained correctly.before< the commit thread changed useNewValue. From that perspective
Thinking about it, I'm still not sure whether newValue or
previousValue need to volatile. They were changed from within a
synchronized block and are returned from with a synchronized block.
Hm ...
Hope this helped a bit.
Regards, Oliver