What is the difference between Clojure STM (dosync) approach and Java synchronize Block?
I\'m reading the code below from \"The sleeping barber\" problem. (http://www.b
Also in addition to Michał's excellent answer, with STM transactions, reads always get you the frozen value at the beginning of the transaction and need not wait for any ongoing transaction to complete.
Just to give a full picture for those seeking, Clojure does have a synchronized
analog. It is useful when one has to work with Java non-threadsafe types for interop.
(locking x & body)
basic difference is following
Clojure STM supports optimistic concurrency whereas JAVA synchronized is pessimist
Clojure STM does not acquire lock until there are more than one thread. if mutable state is updated by another thread then the operation inside dosync is repeated. Also, dosync is mandatory for mutable states. Clojure throws illegalState exception when dosync is missing, unlike JAVA.
dosync
and synchronized
give access to completely different concurrency abstractions.
synchronized
is a way of acquiring and releasing locks. When a thread enters a synchronized
block, it attempts to acquire the appropriate lock; if the lock is currently held by a different thread, the current thread blocks and waits for it to be released. This leads to certain problems, such as the risk of deadlock. The lock is released when the thread leaves the synchronized
block.
dosync
marks a block of code which is to be run in a transaction. Transactions in Clojure are a way of coordinating changes to Refs (objects created with the ref
function); if you need some code to have a consistent view of some pieces of mutable state in Clojure -- and possibly change them -- you put those in Refs and execute your code in a transaction.
A transaction has the interesting property that it will restart if for some reason it cannot commit, up to a certain maximal number of retries (currently hard-coded to be 10000). Among the possible reasons for a transaction being unable to commit are an inability to obtain a consistent view of the world (actually, the relevant Refs -- there is an "adaptive history" facility which makes this less of a problem than it might seem at first glance); simultaneous changes made by other transactions; etc.
A transaction runs no risk of being deadlocked (unless the programmer goes out of their way to introduce a deadlock unrelated to the STM system through Java interop); livelock, on the other hand, is a certain possibility, though it is not very probable. In general, many -- although not all! -- of the intuitions programmers associate with database transactions are valid in the context of STM systems, including that of Clojure.
STM is a huge topic; one excellent resource for learning about Clojure's STM is Mark Volkmann's Software Transactional Memory article. It goes into great depth in discussing Clojure's STM in its final sections, but the beginning can serve as great introductory reading.
As for the snippet you quoted, it's actually not something you would normally want to emulate in production code, since dosync
blocks should almost always be side-effect free; the print
here can be useful for demonstrating the inner working of the STM, but if you wanted a transaction to cause side-effects in real code, you should have it spawn a Clojure Agent for the purpose (which would only execute its task if the transaction successfully commits).