Threads
1. Introduction
Threads are a java class which allows two or more pieces of a program to execute concurrently. That is to say, each thread is considered as a separate process (each however, depending on its parent) so that they can run simultaneously. The most common applications are in the traditional concurrent programming problems (for example, the producer/consumer problem) as well as multimedia.
The term thread is short for thread of control. Someone who can read a book and watch television at the same time is processing two threads. For a while the person concentrates on the book, perhaps during a commercial, but then devotes ones attention to a segment of the TV program. Since the TV program does not require ones undivided attention, one can read a few more lines every now and then. Each thread gets some of ones attention. Perhaps one can concentrate on both threads simultaneously, like musicians who are able to follow different parts of the harmony.
If we have only one thread of control, then when we have to wait whenever that thread gets delayed. For example, suppose our thread is downloading a picture from the Internet. It may have to wait while the system transfers all the pixels of the picture from some remote site. If our program can create a second thread to wait for the input from the remote site, it can go with other processing while the new thread is waiting. When some new data comes in from the remote site, the new thread can receive it while the first thread waits for a while. The two threads share the same processor, as shown below:
Starts to Load Image Delay on Network Load some more of the Image
thread1
Time for some word processing
thread2
In today’s environment, most processors have multiple CPU’s, so that the two threads can really run simultaneously on two different CPU’s. However, when one kills the parent thread, all of its children threads also die. It is not like the applets, whose threads survive after the parent dies.
Java allows us to use threads in our programs. Our program can define two or more threads; the processor or processors will divide their attention among the active threads. There are two ways to create a thread: (1) extend the Thread class via the usual inheritance or (2) implement the interface Runnable. One should look up the API’s of these two classes at http://java.sun.com/j2se/1.5.0/docs/api/ . Note that the Thread class is in the java.lang.Thread package; hence nothing has to be imported to use it. It implements the class Runnable.
One creates a thread in the born state, i.e. when it is created. The thread remains in this state until the program calls the thread’s start method. When the thread executes the start method, it goes into the ready state (also known as the runnable state). The highest priority thread in the ready state enters the running state (i.e., the thread begins executing), when the system assigns a processor to the thread. A thread enters the dead state when its run method completes or terminates for any reason—a dead thread will eventually be disposed of by the system.
One common way for a running thread to enter the blocked state is when the thread issues an input/output request. In this case, a blocked thread becomes ready when the I/O for which it is waiting completes. A blocked thread cannot use a processor even if one is available.
When the program calls method sleep of a running thread, the thread enters the sleeping state. A sleeping thread becomes ready after the designate sleep time expires. A sleeping thread cannot use a processor even if one is available. If the program calls method interrupt on a sleeping thread, the thread exits the sleeping state and becomes ready to execute.
When a running thread calls wait, the thread enters a waiting state for the particular object on which wait was called. One thread in the waiting state for a particular object becomes ready on a call to notify issued by another thread associated with that object. Every thread in the waiting state for a given object becomes ready on a call to notifyAll by another thread associated with that object.
A thread enters the dead state when its run method completes or throws an uncaught exception.
start
sleep interval expires
yield CPU dispatch (assign a CPU)
notify
or
notifyAll
wait sleep issue I/O
complete I/O complete
This is an example of a finite-state diagram. We have a “start” state, which is the state born, and a terminating state, which is the state dead. We move from one state to another depending on the actions.
We can also write these down via the Bauer-Naur normal forms. Here S will represent born and T will represent dead.
S: ready
ready: running
running: blocked | waiting | sleeping | T
waiting: ready
sleeping: ready
blocked: ready
Then a legal sentence is any path from S to T, e.g., S ready running waiting ready blocked ready running T.
2. First Example
To give an example, I will give two examples of a project which creates two threads, which will write their names five times, sleeping after each writing. Processors are so fast today that a thread could do a large amount of output while it had its turn. We sleep here to slow down the thread to human scale, so that we only have to read a few lines of output. The main method runs in a thread (the parent thread) different from the two we create, so we will actually have three threads sharing the processor(s) writing their names when they get their turns.
The class for creating the threads we call NameThread, which is passed two parameters to its constructor, the name of the thread, and the amount of time it is to sleep, in milliseconds. We will create two objects for it, Jack who sleeps for 2 seconds, and Jill, who sleeps for 1 millisecond.
public class NameThread extends Thread
{
private int time; //time in milliseconds to sleep
public NameThread(String str, int time)
{
super(str);
this.time = time;
}
public void run()
{
for(int i = 0; i <5;++i)
{
System.out.println(getName() + " " + i);
try
{
Thread.sleep(time);
}
catch(InterruptedException e)
{
return;
}
}
}
}
We have created a drive class, which will have the main method to run the program. In it, we will also have a thread, itself:
public static void main(String [] args)
{
NameThread jack = new NameThread("Jack",2000);
jack.start();
NameThread jill = new NameThread("Jill",1000);
jill.start();
for(int i = 0; i < 5; ++i)
{
System.out.println(Thread.currentThread().getName() +
" " +i);
try
{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
return;
}
}
System.out.println("We are done with the program");
}//end of main
Note: Neither of these classes has to import any package from the Java API.
In the main method, since we did not create this thread in the program, we get it by using the static currentThread method of the class Thread. This method references the current running thread, which in this case is itself.
The Thread class has a static method called sleep(int milliseconds), which will cause its caller to sleep (be blocked from using the processor) for the specified number of milliseconds. While one thread sleeps, other threads will get their turn at the processor(s). We use the try…catch for the InterruptedException, which could occur if anther thread interrupted this one. We will not discuss interruption or more advanced thread concepts here. We do note that there is another static method of sleep(int milliseconds, int nanoseconds) which allows one to have finer control of the time of sleeping. This is a concession to the faster clock rates on today’s microprocessors.
The output we generate for this program is:
main 0
Jill 0
Jack 0
main 1
Jill 1
main 2
Jill 2
Jack 1
main 3
Jill 3
main 4
Jack 2
Jill 4
We are done with the program
Jack 3
Jack 4
Because we have chosen such a large discrepancy for each of the times, each time we run the program, we will not quite get the same output:
main 0
Jack 0
Jill 0
main 1
Jill 1
main 2
Jack 1
Jill 2
main 3
Jill 3
main 4
Jack 2
Jill 4
We are done with the program
Jack 3
Jack 4
Note that if we run the programs twice once right after the first one, we can get the threads to be interspersed from the two programs:
We
start the program first running
of the program
main 0
Jill 0
Jack 0
main 1
Jill 1
main 2
Jack 1
Jill 2
main 3
Jill 3
main 4
Jill 4
Jack 2
We are done with the program
We
start the program second
running of the program
main 0
Jill 0
Jack 0
Jack
3 thread
from first running of the program
main 1
Jill 1
main 2
Jack 1
Jill 2
Jack thread
from first running of the program
main 3
Jill 3
main 4
Jack 2
Jill 4
We are done with the program
Jack 3
Jack 4
If we had chosen sleep times near each other, we would get different outputs more often. What would happen if the sleep methods were commented out? I.e., we ran the program without any sleeping whatsoever? You should run the above program to confirm your guess.
We could create the same type of program using an implementation of Runnable. Its code would look like:
public class NameUsingThread implements Runnable
{
private int time; //amount of time in milliseconds to sleep
private Thread thread; //the thread to execute the run method
public NameUsingThread(String str, int time)
{
this.time = time;
thread = new Thread(this,str);
thread.start();
}
public void run()
{
for(int i = 0; i <5;++i)
{
System.out.println(thread.getName() + " " + i);
try
{
Thread.sleep(time);
}
catch(InterruptedException e)
{
return;
}
}
}//end of run method
}
public class DriverNameUsingThread
{
public static void main(String [] args)
{
NameUsingThread jack = new NameUsingThread("Jack",2000);
NameUsingThread jill = new NameUsingThread("Jill",1000);
for(int i = 0; i < 5; ++i)
{
System.out.println(Thread.currentThread().getName() +
" " +i);
try
{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
return;
}
}
System.out.println("We are done with the program\n");
}
}
We should point out some of the uses of the threads above. In the first example, because we are extending the class Thread, we do start the thread in the class itself. It must be started outside the class. In the second version implementing Runnable directly, we must create the Thread inside the class, and hence start it there as well.
Which one is better, the extension or the implementation of Runnable? Actually, neither is better than the other. However, if you have a class which is already an extension of another class (i.e., it is inherited), then you can not have this class also extend Thread at the same time. This is because Java does not allow multiple inheritances. In this example, you must have it implement Runnable.
Note that this is why Java has the class of interface. Interfaces were invented to handle the problem of multiple inheritances, which Java does not allow. It has been known that multiple inheritances in Java can lead to problems:
The inventors of Java wanted to avoid this problem. They did so by not allowing multiple inheritances, so they needed someway to allow its functional equivalence. Interfaces solve this problem.
3. Concurrent Programming
Concurrent programming deals with programs that share data and need to cooperate with other to operate correctly.
To illustrate the problem, suppose we have two threads which are both acting on the same account. Suppose the balance is $100, and thread1 adds $100, while thread2 adds $200. After the deposit, each thread is to write to a log with the current balance after the transaction. So a deposit involves two steps: (1) computing the new balance and (2) recording the change in a log. We assume that each thread computes the balance separately, but shares the log to enter the result.
A thread runs for a certain time period, and then another thread gets its turn. If each thread completes both steps when it has its time, the balance and the log will be consistent. However, thread1 could lose the CPU after step (1), but before attempting step (2). In this case, one would have
thread1 thread2
deposit
balance = $200
deposit
balance = $300
enter $300 in log
enter $200 in log
The execution above shows thread1 making its deposit and computing the new balance, while it then loses its turn to thread2. Thread2 computes the new balance and records it in the log. After thread2 is finished, thread1 logs its balance of $200 into the log, which is not incorrect.
To create a program to illustrate this phenomenon, we use a buffer that contains an integer, number, which two threads share. The increment method adds 1 to number and reports its value, while the decrement method subtracts 1 and reports the new value. When the threads operate correctly, successive reports will display values that differ by one. For example, starting with number at 5, two increments give
thread1 increments to 6
thread1 reports 6
thread2 increments to 7
thread2 reports 7
Only when the threads operate incorrectly, do we get the same value on successive reports. For example, again starting at 5
thread1 increments to 6
thread2 decrements to 5
thread2 reports 5
thread1 reports 6
This does not happen very often. The increment and decrement are so short that they are rarely interrupted before completion. For larger methods, this error would occur more often.
We create 3 classes, Buffer, which has the increment and decrement function, as well as the report method. The class Plus, which is an independent thread, calls the increment, while the class Minus, which is also an independent thread, calls the decrement method of Buffer. Both Plus and Minus have an object buf of the class Buffer, but they are the same object. Thus these two separate threads are competing to use the same method of report. We need the method main to set up this situation.
When the previous and current values of number agree, then an error has occurred. It is output and with the total number of operations. The error could have occurred when a thread loses its turn after changing number, but before reporting. It could also lose its turn while executing the report method. The point is that errors can occur due to the interleaving of execution of various threads.
public class Buffer
{
private int number = 0; //the number to increase or decrease
private int previous = 0;
private int total = 0; //total number of operations performed
private int errors = 0; //number of errors
public void increment()
{
number++;
report(number);
}
public void decrement()
{
number--;
report(number);
}
public void report(int n)
{
total++;
if (n == previous)
System.out.println(++errors +"\t"+total);
previous = n;
}
}
public class Plus extends Thread
{
Buffer buf;
Plus( Buffer b)
{
buf = b;
}
public void run()
{
while(true)
buf.increment();
}
}
public class Minus extends Thread
{
Buffer buf;
Minus( Buffer b)
{
buf = b;
}
public void run()
{
while(true)
buf.decrement();
}
}
public static void main(String [] args)
{
Buffer b = new Buffer();
Plus p = new Plus(b);
Minus m = new Minus(b);
p.start();
m.start();
}
When we run it, we get the following output:
1 10568155
2 12528817
3 25226539
4 35470571
5 46824431
6 57523901
7 63008997
8 76699927
9 78068058
10 84250838
11 92213128
12 116649187
13 128247685
14 140686884
15 173220849
16 182584676
17 195515100
18 203224557
19 210805768
20 218237395
21 236702896
22 251900921
So we see we get some 22 errors, but they occur in some 250 million calls.
Note: The number of errors is computer dependent, especially concerning the CPU. One could easily get many errors.
We can fix this by having the increment and decrement methods execute completely once they have begun. Java provides a mechanism for this, called synchronized. We change the declaration of the method to
public synchronized void increment()
{
number++;
report (number);
}
A similar change is made to decrement.
This works because when a thread1 calls this method of the buf object,
buf.increment();
if no other thread is executing any method of buf, thread1 gets a lock for the object. Then no other thread can use buf until thread1 is finished executing the increment method. If another thread is executing a method of buf, then thread1 must wait until their operation completes. If several threads are waiting for a lock on an object, the thread scheduler determines who will get it when it becomes available.
We note that one can assign a priority to a thread using the method of setPriority(int). The value must be between 1 and 10, inclusive, with higher values having higher priority. When one creates a thread, the default value is 5. If two threads are waiting for the same lock, the thread with the higher priority will go first. If they both have the same priority, the choice will depend on their “inner” priorities decided by the operating system. That is to say, the operating system assigns a priority to each process, and each thread is a process.
Synchronization methods exact a performance penalty. If only a portion of the code of a large method is critical, we can improve performance by only synchronizing that portion of the code:
Public returnType someMethod(someParameters)
{
//some code not synchronized
synchronize(this)
{
// synchronize the critical code here
}
//more non-synchronized code
}
4. Classical Consumer-Producer Problem
The classic consumer-producer problem is one in which two threads, the producer and the consumer both access the same data buffer. Producers add data to the buffer, while consumers remove data.
Assuming a fixed size buffer, a producer cannot add more than the buffer can hold, while a consumer cannot retrieve data from an empty buffer. Each Java object has wait and notify methods, which are useful in this situation.
When a producer has a lock on the buffer, and cannot add data because the buffer is full, it executes a wait method causing it to release the lock and wait to be notified the state of the buffer has changed. When a consumer removes an item from a full buffer, it executes the notify method to notify a waiting thread that the buffer is no longer full.
Similarly, when a consumer has a lock on the buffer, and cannot remove data because the buffer is empty, it executes the wait method causing it to release the lock and wait to be notified that the state of the buffer has changed. When a producer puts an item into an empty buffer, it executes the notify method to notify a waiting thread that the buffer is no longer empty.
We implement the producer-consumer problem below. Note that the wait method can throw an InterruptedException, which we throw to the caller of the put or get method, who will handle it. Both the wait and notify method belong to the Object class of Java, so that any object can invoke them.
To store the incoming and outgoing data, we use a queue. A queue is a one dimensional list where inserts are done on one end, the front, and deletions are done at the other end, the rear. You can think of a queue as waiting in line. (The English call a line a queue, where the terminology comes from). We implement a queue by having a one dimensional array and two pointers to the array, front and rear. So the values of front and rear are indices into the list. In out example below, front is done by the getPos and rear is done by the putPos. The producer puts elements into the array, so it controls the rear of the queue where insertions are done. The consumer removes elements from the array, so it controls the front.
So if, for example, we start with an empty queue and start insertions at position 0, where additions would be at position 1, 2, etc., then suppose we had the following commands:
Insert A
Insert B
Remove (A is removed)
Insert C
We would end up with
(assuming a queue of size 3)
Front Rear
|
B |
C |
If we now we wanted to insert D, there appears no place to add it as we are at the end of the array, even though we have space in front.
So we solve this by using a circular buffer on size; here we have size to be 3, so we get:
0
1 2
This means that when we get to the last place (size -1, which is 2 in this example), we just go to the first one (position 0). We implement this in the program by using
negPos = (negPos + 1) % size
So if negPos is 1, then it becomes 2; but if negPos is 2, it becomes 0, as (2 + 1) modulo 3, is just 0.
public class Buffer
{
int size;
int [] buffer;
int putPos,getPos,number;
public Buffer(int size)
{
this.size = size;
buffer = new int[size];
putPos = getPos = number = 0;
}
public synchronized void put(int value) throws InterruptedException
{
if (number == size)
{
System.out.println("Cannot put --Buffer full");
wait();
}
number++;
buffer[putPos] = value;
System.out.println("Put "+value);
putPos = (putPos + 1) % size; //circular buffer
if (number == 1)
notify();
}
public synchronized int get() throws InterruptedException
{
if (number == 0)
{
System.out.println("Cannot get --Buffer empty");
wait();
}
number--;
int value = buffer[getPos];
System.out.println("Get "+value);
getPos = (getPos + 1) % size; //circular buffer
if (number == size - 1)
notify();
return value;
}
}
public class Producer extends Thread
{
Buffer buf;
int time;
public Producer(Buffer buf, int time)
{
this.buf = buf;
this.time = time;
}
public void run()
{
for(int i = 0; i < 10; ++i)
try
{
buf.put(i);
sleep(time);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
public class Consumer extends Thread
{
Buffer buf;
int time;
public Consumer(Buffer buf, int time)
{
this.buf = buf;
this.time = time;
}
public void run()
{
for(int i = 0; i < 10; ++i)
try
{
int n = buf.get();
sleep(time);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
public class TestProducerConsumer
{
public static void main(String [] args)
{
Buffer b = new Buffer(3);
Producer p = new Producer(b,Integer.parseInt(args[0]));
Consumer c = new Consumer(b,Integer.parseInt(args[1]));
p.start();
c.start();
}
}
The Producer class just produces 10 pieces of data, while the Consumer class just consumes the 10 pieces of data. Each sleeps a fixed amount after each act. The Buffer class holds the common data that both are using. In this example, we just have a buffer size of 3.
The TestProducerConsumer class just creates the Buffer, and the classes of Producer and Consumer (which are threads), and starts the two of them. It sets the sleep time by data given on the command line.
When we run this with producer at 300 and consumer at 500 (producer sleeps for .3 seconds, while the consumer sleeps for ˝ second), then we will get a buffer full, and the output we generate is:
Put 0
Get 0
Put 1
Get 1
Put 2
Put 3
Get 2
Put 4
Get 3
Put 5
Put 6
Get 4
Put 7
Cannot put --Buffer full
Get 5
Put 8
Cannot put --Buffer full
Get 6
Put 9
Get 7
Get 8
Get 9
While we run this with producer at 500 and consumer at 400, so we will get empty buffers, we get (note that the first “Buffer Empty” is just because the consumer started first.
Cannot get --Buffer empty
Put 1
Get 1
Cannot get --Buffer empty
Put 2
Get 2
Cannot get --Buffer empty
Put 3
Get 3
Cannot get --Buffer empty
Put 4
Get 4
Cannot get --Buffer empty
Put 5
Get 5
Cannot get --Buffer empty
Put 6
Get 6
Cannot get --Buffer empty
Put 7
Get 7
Cannot get --Buffer empty
Put 8
Get 8
Cannot get --Buffer empty
Put 9
Get 9
When threads wait for locks to be freed that cannot be freed, we have deadlock. We can easily modify the program above to have a deadlock. If we change the condition for the producer to put only when the buffer is empty and the consumer to get only when the buffer is full, we reach a deadlock almost immediately. The buffer starts out empty so the producer can put one item into it, but no more until the consumer removes that item, making the buffer empty again. The consumer cannot remove the one item in the buffer until the producer adds two more items to fill the buffer. Both the producer and consumer are stuck, each waiting an action by the other that will never come. The code to do this to change the put to be
if (number != 0)
and change the get to be
if (number != size)
5. Animation
We can use threads to control the movements of different objects in a program. Each object will be controlled by its own thread.
First of all, let us consider an interface which creates an oval object, which every time we click on a button it moves a certain distance. If it reaches an edge, it bounces off the edge. We have the following code to do this:
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class MoveInterface extends JFrame implements ActionListener
{
private int x,y;
private int direction_x,direction_y;
private JButton move;
private filledOval redOval;
private final int FRAME_WIDTH = 300;
private final int FRAME_HEIGHT = 300;
private final int FRAME_X_AXIS =100;
private final int FRAME_Y_AXIS =100;
public MoveInterface()
{
setSize(FRAME_WIDTH,FRAME_HEIGHT);
setLocation(FRAME_X_AXIS,FRAME_Y_AXIS);
setTitle("Move Ball");
setLayout(null);
direction_x = 1;direction_y = 1;
x = 20; y = 50;
move = new JButton("Move");
move.setBounds(100,30,100,30);
add(move);
redOval = new filledOval(x,y,20,20,0);
redOval.setForeground(Color.red);
add(redOval);
move.addActionListener(this);
}
public void actionPerformed(ActionEvent e)
{
run();
}
public void run()
{
int dx = 9, dy = 9;
x += dx * direction_x;
y += dy * direction_y;;
if (x >=FRAME_WIDTH – (redOval.getWidth() + dx) || x <= 0)
direction_x *= -1;
if (y >=FRAME_HEIGHT – (redOval.getHeight() + dy) || y <= 0)
direction_y *= -1;
redOval.setBounds(x,y,20,20);
}
}
We can replace the moving by the run method of the Thread. The actionPerformed will just create the thread and start it. Once started it will automatically call run. The constructor is exactly the same, so we do not reproduce it. Similarly, all of the members are the same, except for the Thread t, so we do not reproduce them either:
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class ThreadMoveInterface extends JFrame implements ActionListener,Runnable
{
//other members same as MoveInterface
Thread t;
//constructor same as MoveInterface
public void actionPerformed(ActionEvent e)
{
t = new Thread();
t.start();
}
public void run()
{
int dx = 9, dy = 9;
x += dx * direction_x;
y += dy * direction_y;;
if (x >=FRAME_WIDTH – (redOval.getWidth() + dx) || x <= 0)
direction_x *= -1;
if (y >=FRAME_HEIGHT – (redOval.getHeight() + dy) || y <= 0)
direction_y *= -1;
redOval.setBounds(x,y,20,20);
}
}
Then the code with the Thread will run the same as the above code without it.
We can modify run so that it moves indefinitely by the following:
public void run()
{
int dx = 9, dy = 9;
//add the while loop on second part
while(true)
{
x += dx * direction_x;
y += dy * direction_y;;
if (x >=FRAME_WIDTH – (redOval.getWidth() + dx) || x <= 0)
direction_x *= -1;
if (y >=FRAME_HEIGHT – (redOval.getHeight() + dy) || y <= 0)
direction_y *= -1;
redOval.setBounds(x,y,20,20);
//add this on second part
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
Here, we wait 100 milliseconds (1/10 of a second) and then let it move. This will give the illusion that the ball is continually moving.
Note that we could have had the ball moving using the Timer class. Here, we would have the Timer delay for 100 milliseconds. It would behave exactly the same way as the Thread. However, we could not have more than one ball behaving independendly.