Threads
From Suhrid.net Wiki
Jump to navigationJump to search- Think of Thread as the "worker" and Runnable as the job.
- Define work to be done in a class that implements Runnable.
- Instantiate the thread using the runnable object. (Thread is in the new state)
- Then start() it. (Thread moves to the runnable state, eligible to run, perhaps waiting for the scheduler to run it)
class Job implements Runnable {
public void run() {
//work to be performed in a separate thread.
}
}
Job j = new Job();
Thread t = new Thread(j);
t.start();
- When thread actually runs it is in the running state.
- The thread can also go into waiting/blocked/sleeping state. e.g. waiting for an IO Resource such as a packet to arrive. In other words it is NOT runnable.
- Once run() completes the Thread goes to the dead state. You cannot call start() again on it. Of course, the thread object itself can still be used.
SLEEP
- Be careful of the Thread classes static methods such as sleep() and yield(). They refer to the current executing thread! Do not be misled when they are invoked using a thread object.
- e.g. t1.sleep() will not cause Thread t1 to sleep(), it causes the current executing thread to sleep().
- sleep() specifies that the Thread must go to sleep for at least the specified duration.
- What does this mean, it means that when the sleep duration expires and thread wakes up, the thread immediately does not go to running - it goes to the runnable state.
- so sleep() gives the minimum duration that the thread will not run. You cannot use it as an accurate timer!
JOIN
- t.join() will take the current thread from which this code is called and join it to the end of t.
- This means the thread from which join() is called will wait until t finishes before it runs again.
Synchronization
- Control access to shared object by multiple threads.
- Only methods and code blocks can be synchronized.
- Synchronization happens through object locks.
- Suppose a synchronized method is called, then the lock of the object that invoked the method is used for synchronization.
- When a thread enters a synchronized method, since an object has only one lock - even other synchronized methods in the class cannot be entered for that object by other threads.
- The following program invokes three threads to each print a single different letter 100 times.
- If the code is not synchronized, different threads can print the same letter - because they would start printing before the letter has got a chance to change.
- Note that printLetter() is not synchronized. Why ? Because printLetter is invoked using a LetterPrintJob object and each thread uses a separate LetterPrintJob object.
- So you have to synchronize on the lock of the shared object which in this case is the StringBuffer.
class LetterPrintJob implements Runnable {
private StringBuffer strBuf;
LetterPrintJob(StringBuffer strBuf) {
this.strBuf = strBuf;
}
@Override
public void run() {
printLetter();
}
void printLetter() {
synchronized(strBuf) {
char letter = strBuf.charAt(0);
for(int i=0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + letter);
}
strBuf.setCharAt(0, ++letter);
}
}
}
public class SynchroTest {
public static void main(String[] args) {
StringBuffer strBuf = new StringBuffer("A");
new Thread(new LetterPrintJob(strBuf),"T1").start();
new Thread(new LetterPrintJob(strBuf),"T2").start();
new Thread(new LetterPrintJob(strBuf),"T3").start();
}
}
- Since each thread has its own stack, two threads executing the same method concurrently will use different copies of local variables.
- Shared data needs to be synchronized. For non-static members - usually accessed by non-static methods, we need to prevent threads that use the same object instance to invoke the method. Usually by marking
such methods as synchronized.
- Threads using different instances is not a problem - since the non-static data is not shared.
- Similarly for static fields that need to be synchronized, mark the static accessor methods as synchronized.
- However problems arise when non-static methods access static fields and static methods access non-static members through instances !
- Careful coding is necessary to make the class thread-safe.
- See example below on how threads can cause even a Thread-safe class like a Vector to fail.
- Individual methods like add() and remove() are synchronized - which prevents multiple threads using these methods concurrently on a single vector instance.
- However, this will not prevent situations where a different thread can manipulate the vector in between two invocations of the synchronized methods.
class VectorJob implements Runnable {
Vector<String> v;
VectorJob(Vector<String> v) {
this.v = v;
}
public void run() {
try {
if(v.size() > 0) {
Thread.sleep(10);
v.remove(0);
}
} catch (Exception e) {
System.out.println("Oops ");
e.printStackTrace();
}
}
public class SynchroTest1 {
public static void main(String[] args) {
Vector<String> v = new Vector<String>();
v.add("Troll");
new Thread(new VectorJob(v), "T1").start();
new Thread(new VectorJob(v), "T2").start();
}
}
- What happens here is T1 checks Vector size is non-zero before removing the element and then goes to sleep
- T2 gets a chance to run, checks size is non-zero and goes to sleep
- T1 wakes up - removes the first element
- T2 wakes up - proceeds to remove the first element ( T2 was sure that the size > 0 before sleeping) and an exception occurs.
- Solution is to externally synchronize the code block on the vector element.
Deadlock
- Example-1 using synchronized blocks. Straightforward to see how threads can get deadlocked.
public class Deadlock {
String resourceA = "resourceA";
String resourceB = "resourceB";
public static void main(String[] args) {
final Deadlock dl = new Deadlock();
new Thread("Reader") {
public void run() {
dl.read();
}
}.start();
new Thread("Writer") {
public void run() {
dl.write("AA", "BB");
}
}.start();
}
String read() {
synchronized(resourceA) {
synchronized(resourceB) {
return resourceA + resourceB;
}
}
}
void write(String a, String b) {
synchronized(resourceB) {
synchronized(resourceA) {
resourceA = a;
resourceB = b;
}
}
}
}
- Example 2 - Same example using synchronized methods(). Notice, it is more difficult to spot the deadlock condition here.
- How can a deadlock occur here ? A possible scenario:
- Thread-A gets the lock for the thing1 object by calling thing1.foo(thing2)
- Before Thread-A can call the lock for thing2 object by calling thing2.bar(), Thread-B has acquired the lock for thing2 by calling thing2.foo(thing1)
- Since Thread-A has to call thing2.bar(), it waits for Thing2's lock being held by Thread-B.
- Since Thread-B has to call thing1.bar(), it waits for Thing1's lock being held by Thread-A.
- Deadlock !
class Thing {
private String str;
public Thing(String str) {
this.str = str;
}
synchronized void foo(Thing t) {
t.bar();
}
synchronized void bar() {
}
public String toString() {
return str;
}
}
public class Deadlock1 {
public static void main(String[] args) {
final Thing thing1 = new Thing("Thing1");
final Thing thing2 = new Thing("Thing2");
new Thread("Thread-A") {
public void run() {
thing1.foo(thing2);
}
}.start();
new Thread("Thread-B") {
public void run() {
thing2.foo(thing1);
}
}.start();
}
}
Thread Communication
- Threads communicate with each other through their locking status on the objects.
- The Object class has three methods wait(), notify() and notifyAll() which are used by Threads to communicate to each other.
- wait(), notify() must be called from a synchronized context - the thread must own the object's lock before calling it.
- wait() means the thread says - I am going to wait when this object's lock is release till another thread notify()'s me.
- notify() means the thread says - I am done running the job after acquiring the object's lock, I will now release the lock, so that threads wait()'ing for this object can run.