C SC 225 Lecture 13: Multi-threading and Concurrent Programming
[ previous
| schedule
| next ]
Basic Definitions
- Thread : single sequential flow of program execution control
- Concurrent : multiple threads that appear to execute "simultaneously".
- On a uniprocessor system, concurrent executions are interleaved.
- Invoker of a thread does not suspend for its completion (unlike invoker of a method)
- Each thread progresses on its own but that does not mean they are independent
- Multiple threads have access to shared data/objects
Threads versus Processes
- Process is “heavyweight”, has its own ID, memory and other fields maintained by the OS in a process control block (PCB)
- Thread is “lightweight”, each thread in a process shares that process’ memory
- ADV: can share memory, thus quickly communicate. Also requires fewer system resources
- DIS: can share memory, thus potentially interact in unpredictable and incorrect ways
- To see process and thread information in Windows, do the following:
- Give the three finger salute, ctrl-alt-del
- Click Task Manager
- Click the Processes tab. This will display basic process information.
- In the View menu, click Select Columns...
- Check the box next to Thread Count
- Click OK
- If you are reading this through
a browser, find its process (executable file) and see how many threads it has.
- A process has at least one thread (e.g. the thread executes, not the program)
- Why use threads?
- system can be more responsive : for instance one web browser thread can display page text while another is receiving an image.
- In multiprocessor system, can assign different threads to different processors and achieve true parallelism.
- Can perform autonomous background processing (e.g. monitoring a gauge) while an app is running
- Client can randomly interrupt a thread without bringing down the whole app.
- All Java GUI applications are multithreaded.
- The main thread is created by JVM for main() (this is true for all Java apps)
- When a JFrame (or any descendent of java.awt.Window) is created, several housekeeping threads are created in the background
- The Swing Event Dispatch Thread is the most significant. It is run when the GUI frame is displayed (frame.setVisible(true))
- The main thread terminates upon return from main() but the event thread lives on
- The app terminates only when the event thread terminates with System.exit()
- You can print the name of the current thread with
System.out.println(Thread.currentThread().getName());
- The event thread is named "AWT-EventQueue-0"
- All GUI events are handled by this single thread. Swing is not multi-threaded, and not thread-safe.
- Debugging threads is a pain. If fault or incorrect results occur, you may not be able to reproduce the runtime scenario that led to it.
Creating and using threads in Java
Two techniques for creating a thread class
- Extend the Thread class
- Implement the Runnable interface
Extend the Thread class |
class MyThread extends Thread {
. . .
public void run() {
// thread executes this code
}
}
class Client {
. . .
public Whatever anyMethod() {
. . .
Thread doIt = new MyThread();
doIt.start();
// continue on without waiting for thread to complete
}
}
|
Implement the Runnable interface |
class MyThread implements Runnable {
. . .
public void run() {
// thread executes this code
}
}
class Client {
. . .
public Whatever anyMethod() {
. . .
Thread doIt = new Thread(new MyThread());
doIt.start();
// continue on without waiting for thread to complete
}
}
|
Both will do the same thing. From a design standpoint, which is preferable and why?
Special things a thread can do
static Thread currentThread() method
- Call it to get a reference to the currently-running thread object
- The thread equivalent of "this" for thread self-reference
void start() method
- Call this to schedule a new thread to run
- When the thread runs, its run() method is called
- Do not call run() directly because this will not work correctly – it will in effect do nothing.
- Call to start() immediately returns so you can continue running
void run() method
- The thread's algorithm
- Do not call it directly, call start() which will in turn call it
- The thread terminates when this method does (end, return, uncaught exception)
static void sleep() method
- A thread can suspend itself for a fixed period of time, in milliseconds.
- Sleep can throw checked exception
- try {
Thread.sleep(200); // 200 milliseconds
} catch (InterrupedException ie) { }
- Your program runs as a thread, so you can put this anywhere
- If there are multiple threads, another thread may take over while you’re sleeping
void join() method
- Calling thread suspends until given thread has completed
- You call this on a different thread, not on yourself!
- Use this to synchronize threads – suspend calling thread until specified other thread is finished
static void yield() method
- Give some other thread a chance to execute for awhile
- This is voluntary
- Threads have priorities, by default inherited from thread that creates it
Thread States
- All this talk about sleeping, dying, runnable!
- The life of a thread can be shown as a state diagram
-
- Thread goes from New -> Alive -> Dead
- While Alive, it alternates between being Runnable and Blocked
- Runnable means it either is currently executing or is eligible to do so.
- Blocked means it is not eligible to execute until some future event occurs
Thread Scheduling
- Thread scheduling is handled by the JVM
- What is scheduling? Selecting a runnable thread to run
- When does scheduling occur? When the currently running thread blocks itself, runs out
of time, or when a higher priority thread becomes runnable
- Which thread is selected by scheduler? A thread selected from highest priority runnable
threads. JVM specification does not address which, if there is more than one.
Thread Blocking
The current thread can block itself by:
- calling Thread.sleep(time); It becomes runnable when the time period expires
- calling join() on a different thread; it becomes runnable again when that thread dies.
- calling wait() on an object (this, or some other object).
- Once this occurs, the thread can only become runnable again when another thread calls notify() or notifyAll() on the same object!
- So don’t call wait() on an object unless someone else out there has a reference to that object!
The current thread may be blocked by JVM when:
- it issues a time-consuming operation (e.g. read from keyboard)
- it needs to use a synchronized resource that is not available
The current thread may be removed from execution by JVM but remain runnable when
- A higher priority thread becomes runnable
- It has been executing for awhile and other runnables are waiting
Thread Interruption
- The static Thread.sleep() method can throw a checked InterruptedException
- This will occur when a different thread calls sleepingThread.interrupt();.
- If the receiver of the interrupt is not sleeping, a status variable is set but no exception occurs
- A thread can check this status with Thread.currentThread().isInterrupted()
Thread Synchronization
Bad things can happen when two different threads share the same memory!
Classic example is Producer-Consumer a.k.a. Bounded Buffer problem
- Featured in Horstmann text
- Such buffers are featured throughout operating systems and in many apps
- Buffers are necessary to coordinate data exchange between two activities that run at different speeds
- Think of buffer for reading disk bytes into memory
- The buffer is a finite queue: Consumer removes items from the front, Producer adds items to the rear
Another Example: two threads call acct.withdraw() at nearly the same time:
1 public boolean withdraw(long amount) {
2 if (amount <= balance) {
3 long newBalance = balance – amount;
4 balance = newBalance;
5 return true;
6 } else {
7 return false;
8 }
9 }
Consider this execution sequence involving 2 threads A and B:
- balance is 1500
- Thread A calls acct.withdraw(1000)
- Thread A does line 2, condition is true
- Thread A does line 3, computes newBalance as 500
- JVM Scheduler yanks thread A and replaces it with thread B
- Thread B calls acct.withdraw(1000) on the same account (acct)
- Thread B does line 2, condition is true (balance is still 1500)
- Thread B does line 3, newBalance as 500
- Thread B does line 4, assigns balance 500
- Thread B does line 5, returns true
- JVM Scheduler yanks thread B and replaces it with thread A
- Thread A does line 4, assigns balance 500!
- Thread A does line 5, returns true!
Not such a good deal for the bank!
- This is called a race condition, and the withdraw() method is not thread-safe.
- Race condition : outcome depends on how thread execution is interleaved.
- Section of code in which race condition can occur is called critical section.
- Only one thread should be allowed inside the critical section at a time.
- This is known as mutual exclusion, one to the exclusion of all others.
Critical Sections and Concurrent Programming in Java
Java has two major mechanisms for implementing critical sections and concurrent programming
- A monitor associated with every Java object
- The java.util.Concurrent package, its classes and subpackages. Added with Java 1.5 (5.0)
Java Monitors (a.k.a. locks)
- Every Java object has an associated monitor (sometimes called lock)
- Design based on Hansen and Hoare's monitor mechanism developed in the 1970s
- See my
Operating Systems: Process Synchronization notes for details
- See java.lang.Object documentation, particularly, wait(), notify()
and notifyAll() methods
- The monitor has both a lock and a condition variable
- The monitor's lock (a.k.a. monitor):
- Only one thread can possess the monitor at a time
- A thread requests the monitor by attempting to enter a synchronized method or
block
- If the monitor is available, the thread gets possession and retains it until leaving the
method or block
- If the monitor is not available, the thread is put in the Blocked state and added to the monitor's
Entry Set
- When thread surrenders the monitor, some thread from Entry Set is selected by JVM
and that thread is put in the Running state and gets monitor possession.
- Java specifications do not dictate thread selection policy
- The monitor's condition variable:
- Is used for thread coordination
- An unnamed variable accessed through the object's wait(), notify()
and notifyAll() methods (see java.lang.Object)
- A thread must possess the monitor (see above) to call these methods
- When thread calls wait() on an object, it is put in the Blocked state
and added to the condition's Wait Set. It also surrenders its lock on the monitor.
- When a different thread calls notifyAll() (or notify()) on that object,
all (or one) of the threads in the Wait Set are woken up and put in the Runnable state.
- In the case of notify(), Java specifications do not dictate thread selection policy
- An awoken thread cannot proceed beyond its wait() until the notifier has surrendered
the monitor (at which time one of the awoken gets it and the others have to keep waiting)
- See below for examples of synchronized methods and blocks
- Under certain conditions, variables can be safely shared by declaring them volatile (example below)
Returning to the previous example: In Java, you can declare the method to be synchronized:
1 public synchronized boolean withdraw(long amount) {
2 if (amount <= balance) {
3 long newBalance = balance – amount;
4 balance = newBalance;
5 return true;
6 } else {
7 return false;
8 }
9 }
Now, when Thread A calls acct.withdraw(1000), B cannot enter it until A has returned.
Thread A gets the lock on object acct. When Thread A is finished, the lock is
released and B gets it. Thread A will return true, B will return false.
Can also synchronize individual statements or blocks of statements using synchronize block
public Whatever anyMethod() {
// non-critical stuff
synchronized(this) {
// critical operations
}
// non-critical stuff
}
Primitive variables declared volatile, can be thread-safe in limited situations without using synchronized.
- Useful because synchronization mechanism takes time.
- Tells compiler to generate code that writes to memory each time variable is written to, instead
of caching value in register until next use.
- Tells compiler to generate code that reads from memory each time variable is used, instead
of using previously-read value cached in register.
- Here's an example:
1 int count;
2 int accum;
3 void getCount() {
4 return count;
5 }
6 void update(int newCount) {
7 count = newCount;
8 accum = accum + newCount;
9 }
- In update(), the assignment in line 7 occurs in registers and the receiving register
may not be written out to memory until after line 8 (due to optimization). If a different
thread calls getCount() in the meantime, it will get the wrong value of count
- If lines 7 and 8 were converted to assembly code, it would look like this:
1M load $r0, newCount # contents(newCount) loaded into register $r0
2M move $r1, $r0 # $r1 = $r0
3M load $r2, accum # contents(accum) loaded into register $r2
4M add $r3, $r2, $r0 # $r3 = $r2 + $r0
5M store $r1, count # store contents($r1) into count
6M store $r3, accum # store contents($r3) into accum
- Note that the new computed value of count is not stored immediately to
memory after line 2M. Instead it is kept in register $r1 and saved only
at the end of the method.
- If the thread running this method was suspended by the JVM Scheduler after line 2M, then
the field count would not be up-to-date and a different thread calling getCount()
would get the wrong value.
- Change the declaration to volatile:
1 volatile int count;
2 int accum;
3 void getCount() {
4 return count;
5 }
6 void update(int newCount) {
7 count = newCount;
8 accum = accum + newCount;
9 }
- If lines 7 and 8 were converted to assembly code, it would look like this:
1M load $r0, newCount # contents(newCount) loaded into register $r0
2M move $r1, $r0 # $r1 = $r0
3M store $r1, count # store contents($r1) into count
4M load $r2, accum # contents(accum) loaded into register $r2
5M add $r3, $r2, $r0 # $r3 = $r2 + $r0
6M store $r3, accum # store contents($r3) into accum
- Now the new computed value of count is stored immediately to
memory after line 2M.
- The use of count is now considered thread-safe.
The java.util.Concurrent Package
- This package and its subpackages were introduced in Java 1.5 (5.0)
- They were designed to support concurrent programming
- Focus on subpackage java.util.concurrent.locks and selected members
- Address limitations of built-in monitors (described above)
- public interface Lock
- Supports more extensive locking than synchronized
- Supports multiple condition variables
- Call lock() to acquire lock
- Call boolean trylock() to acquire but return immediately if not available
- Call unlock() to release lock
- Call newCondition() to get a new condition variable
- public interface Condition
- Get one of these from Lock implementing class factory (newCondition())
- Can create as many as you need
- Works similarly to implicit Object condition (above)
- Call await() instead of wait()
- Call signal() instead of notify()
- Call signalAll() instead of notifyAll()
- Does caller have to possess associated Lock to call these methods? The
Condition documentation says only:
"The current thread is assumed to hold the lock associated with this Condition when this method is called.
It is up to the implementation to determine if this is the case and if not, how to respond. "
- A thread that calls await() on a condition will block until another thread
calls signalAll() (or signal()) on the same Condition object
- public class ReentrantLock implements Lock
- Implements a mutual exclusion lock, as described in Lock interface above
- Provides a number of methods beyond those required by Lock interface
- Horstmann favors these over use of the built-in monitors
Deadlock (a.k.a. "The Deadly Embrace")
Just because you carefully synchronize methods, that doesn't mean bad things cannot happen in
a multi-threaded environment! Consider this sequence:
- Thread A gets lock on object 1
- Thread B gets lock on object 2
- Thread A requests lock on object 2 and is blocked because B has it
- Thread B requests lock on object 1 and is blocked because A has it!
Neither thread can continue, locked in a "deadly embrace"! Each is waiting for a resource that
the other has an exclusive lock on.
A related condition is livelock, in which the threads are technically still alive
because they are written to periodically wake up and re-test a condition that will never
become true:
while (!condition) {
sleep(awhile);
}
Deadlocks are so insidious because the conditions that lead to them are timing-related, possibly
based on external events, and therefore may be very difficult to reproduce.
The same is true of any thread synchronization problem.
[ C
SC 225 | Peter
Sanderson | Math Sciences server
| Math Sciences home page
| Otterbein ]
Last updated:
Peter Sanderson (PSanderson@otterbein.edu)