CPL 2016, week 2 Inter-thread synchronization: locks and monitors Oleg Batrashev Institute of Computer Science, Tartu, Estonia February 15, 2016
Agenda Inter-thread synchronization Atomicity and consistency Unconditional locking Locking patterns and problems Simple patterns Deadlocks Refining locks Conditional locking Cost of synchronization
Atomicity It is sometimes needed for a block of code ◮ to run operations atomically , i.e. “all at once” ◮ other threads do not interfere: ◮ this thread sees all values as frozen at the start of the block ◮ other threads do not see changes until the whole block finishes ◮ as-if-serial execution
Atomicity for single variable Increment a variable from several threads volatile int c = 0 ... c = c+1; // or ++c; One possible order of actions (the variable is read into register) // Thread1 // Thread2 r1 = c r2 = c r2 = r2 + 1 c = r2 r = r1 + 1 c = r1 is incorrect, because it results in increment by one: c=c+1 . The solution: ◮ prohibit other threads from changing the variable during read/increment/write
Atomic variables Easiest way to solve the above problem is to use atomic variables AtomicInteger c = new AtomicInteger (0); ... c. incrementAndGet () ◮ atomically increments the variable Other atomic classes and methods: ◮ AtomicBoolean , AtomicLong , AtomicReference ◮ getAndSet(V) – no values are lost in updates, important for object de-initialization ◮ compareAndSet(expect, update) – do not update if something is different, important if the state has changed ◮ weakCompareAndSet(expect, update) – no ordering guarantees for other variables! ◮ lazySet() – eventually sets the value
Atomicity for multiple variables Buffer with internal array and size fields: void addElement (Element e) { size += 1; array[size -1] = e; } Someone may try to read the last element which is not yet set: Element getLastElement () { return array[size -1]; } ◮ reordering the two statements in addElement helps only if: ◮ size is volatile and you update it last in the addElement ! ◮ there is only single writer.
Consistency An object may have only limited set of consistent states: ◮ its fields must correspond to each other ◮ e.g. having matching array and size fields in the buffer An action could have ◮ precondition : must initially see the object in consistent state ◮ postcondition : must leave the object in consistent state There are extra conditions may be available as will be shown in Conditional Locks ◮ consistency is the responsibility of the application ◮ concurrency makes it more difficult to provide consistency
Optimistic vs Pessimistic locking Transition between consistent states may happen with two methods: ◮ pessimistic : make sure nobody interferes and then run the action ◮ optimistic : run the action locally and publish the results if nobody has interfered, otherwise restart This corresponds to: ◮ locks (mutexes) – the topic of this section ◮ transactions – the topic to be studied with Software Transaction Memory in Clojure We continue with pessimistic locking
Locks Locks are used to manage access to critical sections: ◮ lock is an entity that may be acquired and released ◮ only one thread may acquire the same lock at a time ( mutex ) ◮ a thread must wait if another thread is holding the lock the first thread tries to acquire ◮ lock variations: ◮ reentrant – may be acquired multiple times by the same thread, must be released as many times ◮ read-write – many readers may hold the lock in Java: ◮ every object has its own lock entity ◮ synchronized block is used to acquire/release object lock synchronized ( objectWithLock ) { // acquire the lock // execute code } // release the lock
Critical sections Protect code blocks that require atomic execution (Java): void addElement (Element e) { synchronized (theLock) { // enter critical section size += 1; array[size -1] = e; } // leave critical section: releasing theLock } ◮ change size and array at once, so no other thread interferes Protect read code too to see a consistent state: Element getElement () { synchronized (theLock) { return array[size -1]; } } ◮ do not enter if any other thread is already executing this or the above critical section ◮ forgetting to lock here makes it all meaningless: ◮ general rule: lock at every access !
Java synchronized Synchronized block is a structural way to locking: ◮ secure – protected against forgetting to unlock and exceptions, as in l.acquire (); try { compute (); } finally {l.release (); } ◮ not flexible – may not lock in one method and unlock in another Java specifics: ◮ Java locks are re-entrant but not interruptible ◮ synchronized void methodA() {} is equivalent to void methodA () { synchronized (this) { } } ◮ synchronized static void methodA() {} is equivalent to static void methodA () { synchronized (ThisC.class) { } }
Java ReentrantReadWriteLock It is useful: ◮ split readers and writers, so that readers may execute simultaneously ◮ only single writer is allowed and this implies no readers There are more to locks, e.g. Java read-write lock: ◮ non-fair or fully fair – acquisition order is not specified or by arrival ◮ no reader or writer preference is possible ◮ reentrant ◮ lock downgrading ◮ support interruption during lock acquisition https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadW
Fully synchronized objects ◮ every access to an object is takes the object lock – every method is synchronized ◮ java.util.Collections methods synchronizedMap , synchronizedList Problems, sometimes: ◮ need lock compound operations, e.g. the following may fail if (list.size () >0) v = list.get (0); ◮ do not need to synchronize every access but prefer to lock for a set of operations Solution for the first problem: ◮ client may use additional locking, because we know the object uses the same lock synchronized (list) { if (list.size () >0) v = list.get (0); }
Client side locking ◮ user of an object is required to lock before using the object ◮ more flexible but also error-prone ◮ may forget to take the lock or use different one Java synchronized collections ( Vector , Collections.synchronized* ): ◮ do not provide synchronized looping over the collection ◮ client must synchronize while iterating synchronized (coll) { for (Element e: coll) { // coll.iterator () is used implicitly .. } } ◮ versioned iterators throw ConcurrentModificationException if the collection is changed during iteration
Copy-on-iteration and Copy-on-write Copy-on-iteration: ◮ for each iteration copy the collection while holding the lock ◮ release the lock and iterate over the copy ◮ Advantage : lock is held for a short period ◮ Disadvantage : may be out of date and require lots of copying Copy-on-write: ◮ for each change copy the collection while holding the lock ◮ modify the copy and replace the reference ◮ Advantage : no need to lock or copy for iteration ◮ Disadvantage : may require lots of copying when changed Copy-on-write is often used for listener/observer lists, because they are seldom modified.
Deadlocks Pessimistic locking has one serious danger: ◮ thread 1 takes locks A and B in this order ◮ thread 2 takes locks A and B in the opposite order ◮ they may get into deadlock – no thread may proceed, because waiting for each other resources code A code B ◮ lines signify executing threads
Ordering locks One solution to deadlocks: ◮ order locks by any parameter, e.g. A,B,C ◮ never acquire higher priority locks if already holding lower priority locks ◮ i.e. never try to acquire A if already holding B ◮ may acquire C if already holding B For example: synchronized (list) { Element e = list.get (0); synchronized (e) { // change e } } ◮ never try to acquire list lock while holding an element lock!
Reducing and splitting locks Reducing critical area: ◮ do not hold the lock if not needed: take late, release early ◮ try not to hold the lock for long operations ◮ release and re-acquire if possible Splitting lock: ◮ use 2 or more locks instead of one if possible: ◮ locks protect independent data/logic ◮ just use Java object for a lock lock1 = new Object() ◮ use as in synchronized(lock1) ...
Collection access example This example presents reducing, splitting and ordering locks: ◮ maintain lock for list and separate locks for the elements ◮ order locks, so that list lock is always taken first ◮ for insertion/removal of element take the list lock ◮ when changing the element: 1. take the list lock and get the element 2. take the element lock and release the list lock 3. change the element 4. release the element lock list.lock (); elem = list.get(i); elem.lock (); list.unlock (); elem. someSeriousModification () elem.unlock (); ◮ efficient : each element may be modified in parallel ◮ error-prone : need to ensure correct behavior in case of exceptions
Recommend
More recommend