CS 4773 Object Oriented Systems
Threads and Synchronization
Start of class on Wednesday
Previous Topic: Threads
About Threads
Terminology
Creating Threads
Thread States
Thread Priorities and Scheduling
Importance of Understanding Scheduling
Synchronization in Java
A Bounded Buffer Example
CRC Cards
Next Topic: Vectors and Synchronization
Note: Much of this material comes from Concurrent Programming, The Java
Programming Language by Stephen Hartley, Oxford Press, 1998.
and The Java Language Specification by Gosling, Joy, and Steele, Addison Wesley, 1996.
About Threads
-
If two or more threads act on a shared variable, there is a possibility that
the actions on that variable will produce timing-dependent results.
- Each thread has a working memory in which it keeps copies of values of
variables from main memory that is shared by all threads.
- To access a shared variable, a thread usually obtains a lock and
flushes its working memory.
- This guarantees that shared values will be thereafter loaded from the
shared memory to the thread's working memory.
- When a thread unlocks a lock it guarantees the values it holds in its
working memory will be written back to main memory.
- In the absence of explicit synchronization, a Java implementation is free
to update main memory in an order that may be surprising.
Example 1. Suppose that y = 1.
One thread executes:
It is possible for another thread to execute the following code:
b1 = (y==3);
b2 = (x==2);
and have both b1 and b2 to be true.
Example 2:
P is null and available is false.
One thread executes:
P = new Point(2,3);
available = true;
The other thread executes:
while (!available) ;
x = P.x;
This can generate a null pointer exception.
Terminology
- A variable is any location in a Java program that can be stored into,
including class variables, instance variables, and array components.
- Variables are kept in main memory that is shared by all threads.
- Every thread has a working memory in which it keeps its own working
copy of variables it may use or assign.
- The main memory contains the master copy of each variable.
- Main memory also contains locks, one for each object.
Threads may compete to acquire a lock.
- Use, assign, load, store, lock. and unlock are
actions that can be performed by a thread.
- Read, write, lock, and unlock are actions that the main
memory system can perform.
- The above actions are assumed to be atomic
(but see below for doubles and longs).
- Use and assign are tightly coupled interactions between a thread's execution
engine and its working memory.
- Lock and unlock are tightly coupled interactions between a thread's
execution engine and main memory.
- A transfer of data between a thread's working memory and main memory is
loosely coupled.
- This means that these operations may occur with some delay, and the delay
may be different for different occurrences.
- Another thread may see the results of these transfers in a different order.
- Actions such as load, store, read, and write on 64-bit quantities
double and long are not treated as atomic unless these variables are
declared volatile.
It is possible for stores into one of these variables by two different
threads to result in a value which comes half from one and half
from the other!
The Java language specification has a complete list of how these actions
are allowed to interact with each other.
You can think about this as having each thread run on a processor with
local memory (working memory) and a distributed main memory.
The assignment a = b; might result in the following operations:
- read b from main memory
- load b into the working memory
- use b (move from working memory into a register)
- assign a (put value into a register)
- store a in working memory
- write a to main memory
The read and write operations may require moving the data through an
interconnection network and may take an unpredictable length of time.
Actions performed by a single thread are totally ordered.
However, actions performed by different threads may not be.
The main result is that shared variables should be manipulated only
within locked code.
A lock is not released until all pending writes have been completed.
Creating Threads (a review)
There are two ways to create a thread:
- Create an object with a class which extends the class Thread
- The constructor initializes the thread.
- When the thread's start method is called, the thread's
run method executes concurrently with other threads.
- Create an object of type Thread.
- Pass an object from a class which implements Runnable.
- When the thread is started, the run method of that
Runnable object is executed.
- Often, the Runnable is the same object that creates the thread.
Thread States
- new: The thread has been created but not started, for example with
- runnable: The thread has been started and is in the ready queue.
- running: The thread is actually running on a CPU.
- suspended: The thread's suspend method has been called
while the thread was runnable or running.
It makes no further progress until its resume method is called.
- blocked: The thread is blocked from continuing because it requested
I/O, called sleep, called wait, or called join.
- suspended-blocked: A blocked thread was suspended by another thread
calling its suspend method.
- dead: The thread has terminated because it has completed its run
method or its stop method has been called.
Thread Priorities and Scheduling
Each thread has a priority which can affect its scheduling.
- The priority is an integer between MIN_PRIORITY and
MAX_PRIORITY.
- You can set (maybe) a thread's priority with set_priority().
- You can get a thread's priority with get_priority().
The scheduling algorithm is not specified by Java and is left up to the
implementation.
- The only thing that is guaranteed, is that the highest priority runnable
thread will be able to get some CPU time.
- On some systems low priority threads may be starved out while on others
they will get some of the CPU time.
- On some systems when there are two equally highest priority threads
one can starve out the other while on other systems they will each
get part of the CPU time.
Importance of Understanding Scheduling
Consider a system which uses preemptive scheduling with the
following four threads:
- MD runs infrequently for a short time with a low priority.
It writes to a shared memory buffer as follows:
lock buffer
write to buffer
unlock buffer
- IB runs frequently for a short time at a high priority.
It removes items from the shared buffer as follows:
lock buffer
read from buffer
unlock buffer
- CT runs at a medium priority, is long running and does not
used the shared buffer.
- WD runs at the highest priority infrequently and checks to see
if any I/O has been done. If not it performs a system reset.
Suppose MD has the shared buffer locked and IB preempts it.
IB blocks when it attempts to access the shared buffer and MD can get back
in to finish and unlock the buffer.
This is OK.
Suppose that MD has the shared buffer locked and IB preempts it and is blocked.
Before MD can get in, CT gets the CPU and prevents MD from running, thus
blocking IB.
The effect is that a medium priority process, CT has blocked a high priority
process, MD.
This is called priority inversion.
Now WD gets the CPU and sees that no I/O has occurred for a long time
(which is an error condition because IB should be doing I/O frequently).
WD thinks an error has occurred and resets the system.
Remember the Pathfinder Mars mission? This is exactly what was causing
the system resets on the Pathfinder.
MD was a meteorological data gathering task.
IB was the information bus task.
CT was a communication task.
WD was the watchdog timer
The problem was fixed by changing the system (from earth) to
use what is called priority inheritance.
When a higher priority process blocks because a lower priority process
holds a lock, the process holding the lock inherits the priority of
the other process.
This may hold the record for longest distance remote programming.
You can read more about this
here.
Synchronization in Java
Synchronization is done in Java using monitors.
Each instance of a class has its own monitor with one
(anonymous) condition variable.
Recall that a monitor is a compiler construct which allows only one
process to be active (own the monitor)
within the protected methods of the monitor.
In Java, by default, no methods of the object are protected.
You can protect methods by using the synchronized key word.
How to own a monitor of an object
- Execute code in a synchronized method of the object. This is the usual way.
- Execute code which is contained in a synchronized block as follows:
synchronized (obj) {
code goes here
}
Here are some of the important methods related to thread synchronization:
- public static native void sleep(long millis) throws InterruptedException
This method causes the calling thread to sleep for the given number of
milliseconds (more or less, according to the specification)
or until the thread's interrupt method is called.
- public final void join() throws InterruptedException
Waits until the given thread dies or the current thread is interrupted.
- public final void wait() throws InterruptedException
Waits to be notified by another thread or interrupted.
The current thread must own the object's monitor, that is be executing from
inside a synchronized method.
The thread releases ownership of the monitor.
When it is notified, it must regain ownership of the monitor before it
continues executing.
- public final native void notify()
Wakes up a single thread that is waiting on this object's monitor.
The current thread must own the object's monitor, that is be executing from
inside a synchronized method.
The current thread does not lose ownership of the monitor.
- public final native void notifyAll()
Wakes up all threads that are waiting on this object's monitor.
The current thread must own the object's monitor, that is be executing from
inside a synchronized method.
The current thread does not lose ownership of the monitor.
- public void interrupt()
Interrupts a thread.
This sets a flag in the thread which can be tested.
If the thread was block on sleep, wait or join,
an InterruptedException is thrown by that thread.
Note: this was not implemented in Java 1.0.
Examples:
This thread sleeps for about 100 milliseconds:
try {
sleep(100)
}
catch (InterruptedException e) {}
If this is executed in a method of an object that does not extend Thread,
then it might have to be written:
try {
Thread.sleep(100);
}
catch (InterruptedException e) {}
Wait for the thread T to die:
try {
T.join();
}
catch (InterruptedException e) {}
Wait and notify:
public synchronized void test1() {
while (count == 0)
try {
wait();
catch (InterruptedException e) {}
...
}
public synchronized void test2() {
...
count++;
if (count == 1)
notify();
}
Note that the two methods must be in the same object (same monitor)
and executed by different threads.
A Bounded Buffer Example
In the bounded buffer problem a single finite buffer is controlled
by an arbitrary number of producer and consumer threads.
The bounded buffer might be implemented as a circular queue using
and array, buffer with integer variables putIm,
takeOut, and count:
putIn: index of the entry to next receive an item (next empty entry)
takeOut: index of the entry that will be tken out next (oldest entry)
count: number of filled entries in the buffer
The following code does not work even if there are no exceptions:
Producer code:
public synchronized void deposit(double value) {
if (count == numSlots)
try {
wait();
} catch (InterruptedException e) {}
buffer[putIn] = value;
putIn = (putIn + 1) % numSlots;
count++;
if (count == 1)
notify();
}
Consumer code:
public synchronized double fetch() {
double value;
if (count == 0)
try {
wait();
} catch (InterruptedException e) {}
value = buffer[takeOut];
takeOut = (takeOut + 1) % numSlots;
count--;
if (count == numSlots-1)
notify();
return value;
}
}
Why does it not work?
Suppose the buffer is empty and there are three threads in the ready queue:
C1 blocks and P notifies it.
C1 is put back in the ready queue.
C2 gets the CPU before C1.
C2 consumes the item.
C1 consumes an item which is not there!
One way to fix this problem is to put the wait in a while loop:
public synchronized void deposit(double value) {
while (count == numSlots)
try {
wait();
} catch (InterruptedException e) {}
buffer[putIn] = value;
putIn = (putIn + 1) % numSlots;
count++;
if (count == 1)
notify();
}
public synchronized double fetch() {
double value;
while (count == 0)
try {
wait();
} catch (InterruptedException e) {}
value = buffer[takeOut];
takeOut = (takeOut + 1) % numSlots;
count--;
if (count == numSlots-1)
notify();
return value;
}
}
This still has a problem.
Suppose the buffer is empty and the ready queue looks like this:
After the two consumers get in and are blocked, P1 inserts and item and
wakes up one of the consumers, say C1.
Suppose that C1 is put in the ready queue after P2.
P2 produces a second item and does not notify since the count is now 2.
C1 consumes its item but C2 never wakes up.
We can fix this problem by waking up all waiting processes.
public synchronized void deposit(double value) {
while (count == numSlots)
try {
wait();
} catch (InterruptedException e) {}
buffer[putIn] = value;
putIn = (putIn + 1) % numSlots;
count++;
if (count == 1)
notifyAll();
}
public synchronized double fetch() {
double value;
while (count == 0)
try {
wait();
} catch (InterruptedException e) {}
value = buffer[takeOut];
takeOut = (takeOut + 1) % numSlots;
count--;
if (count == numSlots-1)
notifyAll();
return value;
}
}
CRC Cards
CRC cards is a technique for object oriented design. The leters stand for
Class, Responsibilities, Collaborations.
A CRC card is a 3 by 5 index card for a given class.
The first line contains the name of the class.
Next is a list of responsibilities of the class, that is the public methods
of that class.
Finally, the collaborations are listed. These are the classes that this class
needs to know about in order to implement its public methods.
Design steps:
- List the classes
- List the responsibilities for each class
- List the collaborations for each class
- Simulate a part of the program
- Iterate the design
Chutes and Ladders Example
CRC Cards Designed in Class, Wednesday, February 24, 2000