260 likes | 452 Views
Java threads: synchronization. Thread states. New : created with the new operator (not yet started ) Runnable : either running or ready to run Blocked : deactivated to wait for something Dead : has executed its run method to completion, or terminated by an uncaught exception
E N D
Thread states • New: • created with the new operator (not yet started) • Runnable: • either running or ready to run • Blocked: • deactivated to wait for something • Dead: • has executed its run method to completion, or terminated by an uncaught exception • a started thread is executed in its own run-time context consisting of: program counter, call stack, and some working memory (registers, cache)
Thread states (cont.) • don't confuse the interface java.lang.Runnable with the state Runnable • "running" is not a separate state within Runnable, butThread.currentThread () // static methodidentifies the current thread object (for itself) • thread.isAlive () tells if the thread has been started (is not New) and not yet Dead • you cannot directly ask whether an alive thread is Runnable or Blocked • you cannot directly ask whether a non-alive thread is still New or already Dead
Entering the Blocked state A thread enters the blocked state when: • the thread goes to sleep by calling the sleep method • the thread calls an operation that blocks until IO operations are complete • the thread tries to acquire a lock that is currently held by another thread (mutual exclusion) • the thread waits for a condition
Transition from Blocked to Runnable A blocked thread moves into the Runnable state when • if in sleep, the specified number of milliseconds have been expired • if waiting for the completion of an IO operation, the IO operation have finished • if waiting for a lock that was owned by another thread, the other thread have released its ownership of the lock • if waits for a condition, another thread signals that the condition (may) have changed • additionally, can wait for a lock or a condition with a timeout
Java Memory Model • computers can temporarily hold memory values in registers or local memory caches, and the compiler can reorder instructions to optimize throughput • without synchronization, threads (interleaved or parallel) may see out-of-date and/or out-of-order values for the same memory locations • locks always ensure that all calculated results are visible to other threads (flushing of caches, etc.) • alternatively, you can declare a flag field as volatile: private volatile boolean done; . . public boolean isDone () { return done; } • a write to a volatile variable synchronizes with all subsequent reads of that same variable (all reads and writes are atomic, even for long and double)
Mutual exclusion from critical code • basic way to protecting a shared code block: myLock.lock (); // a ReentrantLock object try { critical section } finally { myLock.unlock (); } • only one thread at a time can enter the critical section (identified and protected by this particular lock) • any other threads calling on this lock, are blocked until the first thread unlocks • the finally clause makes sure the lock is unlocked even if an exception is thrown
Fairness • you can specify that you want a fair locking policy: Lock fairLock = new ReentrantLock (true); • a fair lock favors the thread that has been waiting for the longest time • by default, locks are not required to be fair • for implementation reasons fair locks can be much slower than regular locks • anyway, you have no guarantee that the thread scheduler provided by the plarform is fair • so if the thread scheduler neglects a thread, it may not get the chance to be treated fairly by the lock
Condition objects • a lock protects sections of code, allowing only one thread to execute the code at a time • a lock manages threads that are trying to enter a protected code segment • a lock can have one or more associated condition objects • each condition object manages threads that have entered a protected code section but that cannot proceed for some reason (usually, lack of data / resource)
Condition objects (cont.) • use a condition object to manage threads that have acquired a lock but cannot proceed with useful work Condition cond = lock.newCondition (); . . lock.lock (); try { while (!(ok to proceed)) // try until succeeds cond.await (); // data available: do something useful } finally { lock.unlock (); } • must itself check that the condition is (still) valid
Condition objects (cont.) • some other thread must call the signalAll method to wake up any waiting threads change some item used in condition test . . cond.signalAll (); // or rarely: signal () • the condition wait must always be done in a loop while (! condition) cond.await (); • multiple threads may compete to acquire the lock • the first one can use up resources, and so others must then enter back to the blocked state to wait • additionally, a "spurious wakeup" (without signal) is permitted to occur (depending on the OS)
Keyword "synchronized" • since version 1.0, every object has an implicit lock to be used in so-called monitor methods: public synchronized void method () { body } this is the equivalent of the following pseudocode: public void method () { // use explicit lock this.implicitLock.lock (); // pseudocode try { body } finally { this.implicitLock.unlock(); } }
Keyword synchronized (cont.) • the monitor lock also has a single implicit associated condition with the following operations • wait adds a thread to its set of waiting threads • notifyAll unblocks all waiting threads to compete for access • these correspond to the following lock calls • implicitCondition.await (); // pseudocode • implicitCondition.signalAll (); • the wait and notifyAll methods are final methods in Object • so the Condition methods had to be renamed differently: await and signalAll
Synchronized codeblocks • an implicit lock can be acquired also by entering a code block synchronized by an object lock: synchronized (obj) { // any object critical section // implicit lock and unlock } • the lock is reentrant: if a thread has acquired the lock, it can acquire it again (increments a count) • similarly, a monitor method can call other monitor methods with the same implicit lock without waiting • you can declare a static monitor method: it uses the the associated class object ("X.class") as its lock
Limitations of implicit locks and conditions • you cannot interrupt a thread that is waiting to acquire a lock • you cannot specify a timeout when trying to acquire a lock • having only one single condition per lock can be inefficient • cannot separate buffer empty (cannot take) vs. buffer full (cannot put) conditions • the virtual machine locking primitives do not map well to the most efficient locking mechanisms available in modern hardware
Lock testing and timeouts • can be cautious about acquiring a lock: if (myLock.tryLock ()) { // now the thread owns the lock try { do something } finally { myLock.unlock (); } } else { do something else } • can call tryLock with a timeout parameter: if (myLock.tryLock(10,TimeUnit.MILLISECONDS)) . . . • TimeUnit can be: SECONDS, MILLISECONDS, MICROSECONDS, or NANOSECONDS.
Lock testing and timeouts (cont.) • if you call tryLock with a timeout, then it can throw InterruptedException when interrupted • can try to break up deadlocks and such • the lockInterruptibly method has the same meaning as tryLock with an infinite timeout • can also supply a timeout for a condition wait; myCondition.await (10, TimeUnit.MILLISECONDS)); • returns if another thread has called signalAll - or if the given timeout has elapsed • await methods throw an InterruptedException if the thread is interrupted (but awaitUninterruptibly)
Summary of synchronization operations • synchronized (lock) { . . use shared data } • synchronized void method () { . . use shared data } • lock.lock () { . . } finally { lock.unlock (); } // explicit • waiting for a condition to proceed: try { . . while (!(cannot proceed)) wait (); . . // means: this.wait (); } catch (InterruptedException e) { . . • either anyObject.wait (); or condition.await (); • either anyObject.notifyAll (); or condition.signalAll ();
Recommendations • it is best to use neither lock/condition nor the old monitor methods/code blocks ("synchronized") • in many situations, better to use mechanisms of the java.util.concurrent package that do all the locking and synchronization for you • for example, a blocking queue can synchronize threads that work on a common pipeline • if the monitor methods work for your situation, prefer using them: • less code to write and so less room for error • use explicit locks/conditions only if you need the additional power of these constructs
java.util.concurrent: thread management tools • ReentrantReadWriteLock provides shared access for readers, and exclusive access for a writer • BlockingQueue <E> can cause a thread to block when trying to add to a full queue or trying to remove from empty one • ConcurrentLinkedQueue <E> and ArrayBlockingQueue <E> provide optionally-bounded/bounded thread-safe queues • interfaceConcurrentMap <K,V> implemented efficiently by • ConcurrentHashMap and ConcurrentSkipListMap • CopyOnWriteArrayList<E> and CopyOnWriteArraySet<E> provide thread-safe collections (reads outnumber mutations) • Callable <T> plus Future <T> can return and hold the result of an asynchronous computation (can test, time-out, and cancel) • you can give a Runnable to a thread pool, and one of its idle threads executes it; afterwards, that thread can serve again • synchronizers: CyclicBarrier, CountDownLatch, Exchanger, SynchronousQueue, and Semaphore • AtomicInteger, AtomicLong, AtomicReference, etc.: lock-free thread-safe programming on single variables
Atomic variables • support lock-free thread-safe programming on single variables (objects) • extends the notion of volatile: provide atomic and synchronized access to single values (visibility!) • enable implementations to use efficient atomic machine instructions that available on processors (but no guarantees: might entail internal locking ..) AtomicLong seqNo = new AtomicLong (0); . .long next () { return seqNo.getAndAdd(1); } // atomic • may not work when state involves invariants.. • atomic variables do not replace Integer, Long, etc. • do not provide hashCode or compareTo (since expected to change, no good as Map keys)
A producer-consumer solution class IntProducer implements Runnable { private BlockingQueue <Integer> queue; public IntProducer (BlockingQueue <Integer> q) { queue = q; new Thread (this).start (); } public void run () { try { // "put" may block for (int x = 0; x < 100; x++) { Thread.sleep (delay); . . queue.put (x); } } catch (InterruptedException e) { } } }
A producer-consumer solution (cont.) class IntConsumer implements Runnable { private BlockingQueue <Integer> queue; public IntConsumer (BlockingQueue <Integer> q) { queue = q; new Thread (this).start (); } public void run () { try { // "take" may block while (true) { int value = queue.take (); . . } } catch (InterruptedException ex) { } } } . . BlockingQueue <Integer> q = new ArrayBlockingQueue <Integer> (10); // capacity