When a program is executing ( or running ) it is called a process. Every process runs in sequential order. That means that one command is executed after the other. However, sometimes we may want to have two alternate lines of execution. For example, we may want to download a movie, and simultaneously play it. Normally we would have to wait for the download to finish before we could attempt any other command , like playing it. In such cases we can use a special Java Class called a Thread.
We can create an object of type thread in our main( ) method. This object has a built in method called run( ) which like the main method is the entry point of code execution. That means that if for example we create two threads, then we have two points of code execution (in addition to the code exectuting in the main( ) method ) from which we can run any commands we desire. These commands will be run simultaneously.
Lets see an example:
file: MyThreadProgram public class MyThreadProgram{ public static void main( ) { //declare the threads MyThread thread1 = new MyThread( "thread1" ); MyThread thread2 = new MyThread( "thread2" ); thread1.start();//actually start the threads running thread2.start(); System.err.println( "Started two Threads, main ends\n" ); //System.exit(0) ; this will kill the parent process and the two thread //children, therefore not use System.exit(0) in the parent process. } // end main() }//end class file: MyThread class MyThread extends Thread{ private int time; public MyThread (String name) { super( name );//call superclass constructor to assign name //initialize time time = ( int ) ( Math.random() * 8000 );//time will be 0-8000 miliseconds } public void run()//this is like main( ) in the parent class { try { System.err.println( getName() + " is going to sleep for " + time + "miliseconds" ); Thread.sleep( time ); } // if thread is interrupted during its sleep, print stack trace catch ( InterruptedException exception ) { exception.printStackTrace(); } System.err.println( getName() + " finished sleeping" ); } }
Both threads will execute the code as defined in the file: MyThread. Both threads will start sleeping at (almost) the exact same moment. Their sleeping will be done in parallel. We don't know which thread will finish first since each thread sleeps a different amount of time.
A note about the concept of simultaneous execution. In reality, if my computer has only one CPU then I cannot execute two seperate commans at once. So what is really going in the computer is that the operating system is creating tiny slices of time, or time slices , of about 5 miliseconds and giving one slice to each process alternatively. In this way both processes will advance together, although not strictly simultaneously, although from our perspective it will appear to be simultaneous. In this way we can avoid what is called blocking. Blocking is when one activity cannot be done because another activity is taking up all the CPU time. By alternating the time each process gets the CPU's time we can ensure that all processes get a share and no process is blocked out. When we create several threads in one program, the Java compiler asks the operating system to implement a time sharing scheme to simulate parallel processing. True parallel processing can only occur on machine with more than one CPU.
In the above example, both threads executed the same code. Alternatively, we could have totally different executions for the two threads. We could use an if structure to perform different operations in each thread. Here is how:
file: MyThread class MyThread extends Thread{ public MyThread (String name) { super( name );//call superclass constructor to assign name } public void run() { try { String name=getName(); if ( name.equals("thread1") ) { for (int c=0;c<50;c++) { System.err.println("I am thread 1." ); } } else { System.err.println("I am thread 2." ); } } // if thread is interrupted during its sleep, print stack trace catch ( InterruptedException exception ) { exception.printStackTrace(); } } }
Assume the file: MyThreadProgram is running this thread program.
Threads can be passed parameters when they are instantiated. These parameters follow the normal rules of parameters in methods: if a primitive data type is passed, it is passed by reference, if an array of a class is passed, it is passed by reference. This means that if we give two different threads the same object, if one thread changes that object, the other thread will see these changes. This is called a shared resource. It is useful in many types of applications which require inter-process communication.
A consumer/producer problem is one in which one process creates data or information and another process consumes or uses (reads) that data. A real world example of a consumer/producer problem would be a zoo keeper and a gorilla. The zoo keeper supplies bananas and the gorilla eats them. They have a shared resource, the feeding trough. There can be theoretical problems in such a relationship. The gorilla might come for food before it is served and get angry. The zoo keeper might put out bananas when the gorilla is not there. And more problematically, the zoo keeper might put bananas in the trough when the gorilla is trying to eat. That could cost the zoo keeper his hand!
When there is a single resource for two process we need to synchronize the use of this resource so that only one process acesses it at a time. Otherwise one process could overwrite the work of the other. The solution for this problem is to use what is called a semaphore. A semaphore is a flag to indicate that the resource is in use. Before a process accesses the resource he checks that the flag is not up. Then when he accesses the resource he raises the flag, when he leaves he lowers the flag. This system will prevent access overlap. In java this is called synchronization. The following is a simplistic implementation of a consumer/producer problem which does not deal with the problem of synchronizing acess to the shared resource.
file: Buffer.java // Buffer interface specifies methods called by Producer and Consumer. public interface Buffer { public void set( int value ); // place value into Buffer public int get(); // return value from Buffer } file: UnsynchronizedBuffer.java // UnsynchronizedBuffer represents a single shared integer. public class UnsynchronizedBuffer implements Buffer { private int buffer = -1; // shared by producer and consumer threads // place value into buffer public void set( int value ) { System.err.println( Thread.currentThread().getName() + " writes " + value ); buffer = value; } // return value from buffer public int get() { System.err.println( Thread.currentThread().getName() + " reads " + buffer ); return buffer; } } // end class UnsynchronizedBuffer file: Producer.java // Producer's run method controls a thread that // stores values from 1 to 4 in sharedLocation. public class Producer extends Thread { private Buffer sharedLocation; // reference to shared object // constructor public Producer( Buffer shared ) { super( "Producer" ); sharedLocation = shared; } // store values from 1 to 4 in sharedLocation public void run() { for ( int count = 1; count <= 4; count++ ) { // sleep 0 to 3 seconds, then place value in Buffer try { Thread.sleep( ( int ) ( Math.random() * 3001 ) ); sharedLocation.set( count ); } // if sleeping thread interrupted, print stack trace catch ( InterruptedException exception ) { exception.printStackTrace(); } } // end for System.err.println( getName() + " done producing." + "\nTerminating " + getName() + "."); } // end method run } // end class Producer file: Consumer.java // Consumer's run method controls a thread that loops four // times and reads a value from sharedLocation each time. public class Consumer extends Thread { private Buffer sharedLocation; // reference to shared object // constructor public Consumer( Buffer shared ) { super( "Consumer" ); sharedLocation = shared; } // read sharedLocation's value four times and sum the values public void run() { int sum = 0; for ( int count = 1; count <= 4; count++ ) { // sleep 0 to 3 seconds, read value from Buffer and add to sum try { Thread.sleep( ( int ) ( Math.random() * 3001 ) ); sum += sharedLocation.get(); } // if sleeping thread interrupted, print stack trace catch ( InterruptedException exception ) { exception.printStackTrace(); } } System.err.println( getName() + " read values totaling: " + sum + ".\nTerminating " + getName() + "."); } // end method run } // end class Consumer file: SharedBufferTest.java // SharedBufferTest creates mrPeebles and consumer threads. public class SharedBufferTest { public static void main( String [] args ) { // create shared object used by threads Buffer sharedLocation = new UnsynchronizedBuffer(); // create producer and consumer objects Producer mrPeebles = new Producer( sharedLocation ); Consumer magilla = new Consumer( sharedLocation ); mrPeebles.start(); // start producer thread magilla.start(); // start consumer thread } // end main }Here is an example with synchronization.
file: SynchronizedBuffer.java // SynchronizedBuffer synchronizes access to a single shared integer. public class SynchronizedBuffer implements Buffer { private int buffer = -1; // shared by producer and consumer threads private int bufferInUseCount = 0; // count of occupied buffers // place value into buffer public synchronized void set( int value ) { // for output purposes, get name of thread that called this method String name = Thread.currentThread().getName(); // while there are no empty locations, place thread in waiting state while ( bufferInUseCount == 1 ) { // output thread information and buffer information, then wait try { System.err.println( name + " tries to write." ); displayState( "Buffer full. " + name + " waits." ); wait(); } // if waiting thread interrupted, print stack trace catch ( InterruptedException exception ) { exception.printStackTrace(); } } // end while buffer = value; // set new buffer value // indicate producer cannot store another value // until consumer retrieves current buffer value ++bufferInUseCount; displayState( name + " writes " + buffer ); notify(); // tell waiting thread to enter ready state } // end method set; releases lock on SynchronizedBuffer // return value from buffer public synchronized int get() { // for output purposes, get name of thread that called this method String name = Thread.currentThread().getName(); // while no data to read, place thread in waiting state while ( bufferInUseCount == 0 ) { // output thread information and buffer information, then wait try { System.err.println( name + " tries to read." ); displayState( "Buffer empty. " + name + " waits." ); wait(); } // if waiting thread interrupted, print stack trace catch ( InterruptedException exception ) { exception.printStackTrace(); } } // end while // indicate that producer can store another value // because consumer just retrieved buffer value --bufferInUseCount; displayState( name + " reads " + buffer ); notify(); // tell waiting thread to become ready to execute return buffer; } // end method get; releases lock on SynchronizedBuffer // display current operation and buffer state public void displayState( String operation ) { StringBuffer outputLine = new StringBuffer( operation ); outputLine.setLength( 40 ); outputLine.append( buffer + "\t\t" + bufferInUseCount ); System.err.println( outputLine ); System.err.println(); } } // end class SynchronizedBuffer file: SharedBufferTest2.java // SharedBufferTest2creates producer and consumer threads. public class SharedBufferTest2 { public static void main( String [] args ) { // create shared object used by threads; we use a SynchronizedBuffer // reference rather than a Buffer reference so we can invoke // SynchronizedBuffer method displayState from main SynchronizedBuffer sharedLocation = new SynchronizedBuffer(); // Display column heads for output StringBuffer columnHeads = new StringBuffer( "Operation" ); columnHeads.setLength( 40 ); columnHeads.append( "Buffer\t\tOccupied Count" ); System.err.println( columnHeads ); System.err.println(); sharedLocation.displayState( "Initial State" ); // create producer and consumer objects Producer mrPeebles = new Producer( sharedLocation ); Consumer magilla = new Consumer( sharedLocation ); mrPeebles.start(); // start producer thread magilla.start(); // start consumer thread } // end main } // end class SharedBufferTest2
© Nachum Danzig February 2004