Reading “Programming Concurrency on the JVM” I found an example (which I’ve modified below) using Clojure to solve a classic concurrency dilemma by using the STM to help keep things sane.
; we need to have 1000 (or more) across both accounts
; if we have less than a 1000 then there is a violation
(def current-account (ref 500))
(def savings-account (ref 600))
; the `alter` function allows us to modify a reference (`@from` in this case
; which points to either the reference `current-account` or `savings-account`)
(defn unsafe-withdraw [from constraint amount]
(dosync
(let [total (+ @from @constraint)]
(Thread/sleep 1000) ; blocks & so allows context switch to other future(thread) to start
(if (>= (- total amount) 1000)
(alter from - amount)
(println "Sorry, can't withdraw due to constraint violation")))))
(defn safe-withdraw [from constraint amount]
(dosync
(let [total (+ @from (ensure constraint))] ; the `ensure` function is the secret sauce!
; also notice we don't deference the value (@constraint)
(Thread/sleep 1000)
(if (>= (- total amount) 1000)
(alter from - amount)
(println "Sorry, can't withdraw due to constraint violation")))))
; `ensure` let's us tell a transaction (remember `dosync` is using the STM)
; to watch a variable that is read and not modified.
; STM will ensure writes are comitted only if the values we've read have not
; changed outside the transaction. It'll then retry the transaction otherwise.
; UNSAFE EXAMPLE EXECUTION
(println "UNSAFE: BEFORE")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))
(future (unsafe-withdraw current-account savings-account 100))
(future (unsafe-withdraw savings-account current-account 100))
(Thread/sleep 2000) ; allowing enough time for both futures(threads) to complete
(println "UNSAFE: AFTER")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))
; OUTPUT (notice our code is not thread-safe as we've violated the account requirements)...
; UNSAFE: BEFORE
; Current Account balance is 500
; Savings Account balance is 600
; Total balance is 1100
; UNSAFE: AFTER
; Current Account balance is 400
; Savings Account balance is 500
; Total balance is 900
; ...WE'RE UNDER A 1000 ACROSS BOTH ACCOUNTS :-(
; SAFE EXAMPLE EXECUTION
(dosync (ref-set current-account 500)) ; reset value
(dosync (ref-set savings-account 600)) ; reset value
(println "SAFE: BEFORE")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))
(future (safe-withdraw current-account savings-account 100))
(future (safe-withdraw savings-account current-account 100))
(Thread/sleep 2000) ; allowing enough time for both futures(threads) to complete
(println "SAFE: AFTER")
(println "Current Account balance is" @current-account)
(println "Savings Account balance is" @savings-account)
(println "Total balance is" (+ @current-account @savings-account))
; OUTPUT (notice our code is now thread-safe)...
; SAFE: BEFORE
; Current Account balance is 500
; Savings Account balance is 600
; Total balance is 1100
; SAFE: AFTER
; Current Account balance is 400
; Savings Account balance is 600
; Total balance is 1000
; ...ONLY ISSUE IS THAT IT SEEMS LIKE THE STM HASN'T RETRIED THE TRANSACTION?
; BECAUSE THERE IS NO WARNING MESSAGE PRINTED (AS PER THE `ELSE` STATEMENT)
; ALSO! MOST OF THE TIME THE BALANCE STAYS AS 1100 (NOTHING DEDUCTED FROM EITHER ACCOUNT)
; AND ON THE RARE OCCASION THE SAVINGS ACCOUNT WILL BE DEDUCTED FROM RATHER THAN THE CURRENT ACCOUNT