Scientific Programming II

Programming in Java

Threads


According to Wikipedia,

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. The implementation of threads and processes differs between operating systems, but in most cases a thread is a component of a process. Multiple threads can exist within the same process and share resources such as memory, while different processes do not share these resources. In particular, the threads of a process share its instructions (executable code) and its context (the values of its variables at any given moment).

When you execute a Java (or really, any) program, the operating system spawns a "process", which if we consider our program executable a class is an instance of that class. Within this process, however, multiple "threads" exist. But what is a thread? A thread is a "lightweight process", able to execute some code and then switch to another thread to do other code. The operating system handles this switching automagically, allowing the developer and end user to believe that many things are happening in parallel.

HOWEVER, even if you have a multi-core machine, your process is delegated to a single core, even if you have multiple threads, unless you specifically design your program to use classes from the java.util.concurrent.


Threads are very common in GUI programming, as we will see in this refactored solution for the midterm:

package edu.govschool.threads;

import java.awt.*;
import java.awt.image.*;
// We need these classes to make a "thread pool"
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import javax.swing.*;

/**
 * Refactored solution to the midterm using threads instead of 
 * <code>Timer</code>.
 * @author Mr. Davis
 */
public class ThreadsExample extends JApplet
{
    private static BufferedImage image;
    
    private static final int SIZE = 600;
    private static final int MULT = 12;
    
    @Override
    public void init()
    {   
        setSize(SIZE, SIZE);
        // Create a thread pool. A "thread pool" is a collection of threads
        // managed by the JVM for you. All we have to do is add threads to it
        // later on
        ExecutorService threads = Executors.newCachedThreadPool();
        
        image = new BufferedImage(getWidth(), getHeight(), 
                                    BufferedImage.TYPE_INT_RGB);
        
        Graphics g = image.getGraphics();
        g.setColor(Color.LIGHT_GRAY);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());
        
        // Here, we start our five projectiles as threads
        for (int i = 0; i < 5; i++) {
            threads.execute(new ProjectileThread(15.0, 15.0 * (i+1)));
        }
        
        // After all of the threads are finished, shutdown the thread pool
        threads.shutdown();
    }
    
    @Override
    public void paint(Graphics g)
    {
        g.drawImage(image, 0, 0, this);
    }
    
    // The <code>Runnable</code> interface defines in code the concept of a
    // block of code to be called and executed elsewhere. It is the actual
    // code we want to run on different threads. In this case, we want to 
    // update the projectile's location as it travels and redraw it to the
    // screen
    private class ProjectileThread extends Projectile implements Runnable
    {
        private double time;
        private int xCoord = 0;
        private int yCoord = (SIZE - 20);
        
        public ProjectileThread(double initVelo, double theta)
        {
            super(initVelo, theta);
            this.time = 0;
        }
        
        // The 'run()' method contains all of the code we want to run
        // on threads. It contains an infinite loop, so we never actually
        // stop execution on our threads until we quit the application
        @Override
        public void run()
        {
            while (true) {
                Graphics g = image.getGraphics();
                g.setColor(Color.LIGHT_GRAY);
                g.fillOval(xCoord, yCoord, 10, 10);

                g.setColor(Color.RED);
                xCoord = this.getX(this.time) * MULT;
                yCoord = (SIZE - 20) - (this.getY(this.time) * MULT);
                g.fillOval(xCoord, yCoord, 10, 10);

                this.time += 0.25;

                try {
                    Thread.sleep(75);
                } catch (InterruptedException e) {}
                
                repaint();
            }
        }
    }
}

This program clearly illustrates the necessary components required to use threads in a Java program using the best practices, at least for our usage. We need several things:


One incredibly important thing to notice is that our threads do not share any variables, they're completely self contained. Every single thread has a seperate reference to the Graphics of the applet in order to draw to it.

But what do we do when we need to share variables? Very often, an application will use multiple threads that all need to update or read from a single, shared variable. Remember, we have no control over the order of thread execution, that's left up to the JVM. So, let's see what happens when two threads try to access the same variable.

Buffer.java
package edu.govschool.threads.synch;

/**
 * Interface to represent a buffer to read and write from using threads
 * @author Mr. Davis
 */
public interface Buffer 
{
    /**
     * Set the value of the buffer
     * @param val the value to set
     * @throws InterruptedException the buffer could not be set 
     */
    public void set(int val) throws InterruptedException;
    
    /**
     * Get the value contained in the buffer
     * @return the current value of the buffer
     * @throws InterruptedException the buffer could not be accessed
     */
    public int get() throws InterruptedException;
}
UnsynchronizedBuffer.java
package edu.govschool.threads.synch;

/**
 * Class representing an unsynchronized buffer. If more than one thread
 * attempts to access the data within, chances are their accessing will be out 
 * of sync.
 * @author Mr. Davis
 */
public class UnsynchronizedBuffer implements Buffer
{
    // The stored data. The Producer will write to this, and the Consumer will
    // read from it
    private int buffer = -1;
    
    /**
     * Set the value of the buffer.
     * @param val the value to set
     * @throws InterruptedException the buffer could not be accessed
     */
    @Override
    public void set(int val) throws InterruptedException
    {
        System.out.printf("Producer writes\t" + val);
        buffer = val;
    }
    
    /**
     * Get the value of the buffer.
     * @return the value of the buffer
     * @throws InterruptedException the buffer could not be accessed
     */
    @Override
    public int get() throws InterruptedException
    {
        System.out.printf("Consumer reads\t" + buffer);
        return buffer;
    }
}
Producer.java
package edu.govschool.threads.synch;

import java.util.Random;

/**
 * Class to continually write to a shared buffer. The <code>Runnable</code>
 * interface defines in code the concept of a block of code to be called and
 * executed elsewhere. We will pass instances of this and the
 * <code>Consumer</code> class to our threads to execute.
 * @author Mr. Davis
 */
public class Producer implements Runnable 
{
    // We want to sleep, or pause, for a random amount of time
    private final static Random time = new Random();
    // A reference to our shared buffer
    private final Buffer sharedLocation;
    
    /**
     * The constructor simply sets the reference to the shared buffer.
     * @param shared the buffer to share with Consumer
     */
    public Producer(Buffer shared)
    {
        this.sharedLocation = shared;
    }
    
    /**
     * The code to run in our thread. We loop 10 times, sleeping for a random
     * time up to three seconds, then we write to the buffer.
     */
    @Override
    public void run()
    {
        int sum = 0;
        
        for (int count = 1; count <= 10; count++) {
            try {
                Thread.sleep(time.nextInt(3000));
                this.sharedLocation.set(count);
                sum += count;
                System.out.printf("\t%2d\n", sum);
            } catch (InterruptedException e) {}
        }
        
        System.out.println("Producer done producing\nTerminating Producer");
    }
}
Consumer.java
package edu.govschool.threads.synch;

import java.util.Random;

/**
 * Class to continually write to a shared buffer. The <code>Runnable</code>
 * interface defines in code the concept of a block of code to be called and
 * executed elsewhere. We will pass instances of this and the
 * <code>Producer</code> class to our threads to execute.
 * @author Mr. Davis
 */
public class Consumer implements Runnable
{
    // We want to sleep, or pause, for a random amount of time
    private final static Random time = new Random();
    // A reference to our shared buffer
    private final Buffer sharedLocation;
    
    /**
     * The constructor simply sets the reference to the shared buffer.
     * @param shared the buffer to share with Producer
     */ 
    public Consumer(Buffer shared)
    {
        this.sharedLocation = shared;
    }
    
    /**
     * The code to run in our thread. We loop 10 times, sleeping for a random
     * time up to three seconds, then we read from the buffer. We also sum the
     * values read from the buffer.
     */
    @Override
    public void run()
    {
        int sum = 0;
        
        for (int count = 1; count <= 10; count++) {
            try {
                Thread.sleep(time.nextInt(3000));
                sum += this.sharedLocation.get();
                System.out.printf("\t\t\t%2d\n", sum);
            } catch (InterruptedException e) {}
        }
        
        System.out.printf("\n%s %d\n%s\n", "Consumer reads vals totaling", sum, 
                "Terminating Consumer");
    }
}
UnsynchTest.java
package edu.govschool.threads.synch;

// We need these classes to make our thread pool.
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

/**
 * Class to test the use of the <code>UnsynchronizedBuffer</code>.
 * @author Mr. Davis
 */
public class UnsynchTest 
{
    public static void main(String[] args)
    {
        // Create our thread pool
        ExecutorService threadPool = Executors.newCachedThreadPool();
        
        // Create an unsynchronized buffer to share between Producer
        // and Consumer
        Buffer sharedLocation = new UnsynchronizedBuffer();
        
        // Print some nice formatting
        System.out.println("Action\t\tValue\tSum of Produced\tSum of Consumed");
        System.out.println("------\t\t-----\t---------------\t---------------");
        
        // Start our threads
        threadPool.execute(new Producer(sharedLocation));
        threadPool.execute(new Consumer(sharedLocation));
        
        // Stop our thread pool after the threads finish executing
        threadPool.shutdown();
    }
}

What we expect is that the Producer will write the numbers 1-10 to the buffer, while the Consumer will read those numbers 1-10 and sum them, producing 55. Our output is thus:

Action          Value       Sum of Produced Sum of Consumed
------          -----       --------------- ---------------
Producer writes 1           1
Consumer reads  1                           1
Consumer reads  1                           2
Producer writes 2           3
Consumer reads  2                           4
Consumer reads  2                           6
Consumer reads  2                           8
Producer writes 3           6
Producer writes 4           10
Consumer reads  4                           12
Consumer reads  4                           16
Consumer reads  4                           20
Producer writes 5           15
Consumer reads  5                           25
Producer writes 6           21
Consumer reads  6                           31

Consumer reads vals totaling 31
Terminating Consumer
Producer writes 7           28
Producer writes 8           36
Producer writes 9           45
Producer writes 10          55
Producer done producing
Terminating Producer

Uh-oh...that's not what we expected! The problem goes back to the issue that we have no control over the order threads execute. In this case, Consumer ran much more quickly than Producer, causing it to terminate first. If we were to run this program again, it could be that Producer would finish first, instead of Consumer. The thing to note is that it's random.

We do, luckily, have a solution! We use the technique called synchronization to ensure that shared variables are locked when one thread is accessing them, and that other threads will have to wait() until the original thread is finished with it. Then, the original thread can call notifyAll() to inform other threads that it is done with the synchronized variables.

Using this knowledge, we can write a class SynchronizedBuffer that utilizes synchronization to produce the output we want! We need to modify our test program slightly, as below, but we do get the desired output:

SynchronizedBuffer.java
package edu.govschool.threads.synch;

/**
 * Class representing a synchronized buffer. This class enables a built-in Java
 * features called <i>locks</i> to prevent threads accessing data when another
 * thread is working with it.
 * @author Mr. Davis
 */
public class SynchronizedBuffer implements Buffer
{
    private int buffer = -1;
    private boolean occupied = false;
    
    // The synchronized keyword ensures that our lock, the occupied variable,
    // will prevent the Producer and Consumer from accessing the buffer in the
    // incorrect order
    @Override
    public synchronized void set(int val)
    {
        while (occupied) {
            try {
                System.out.println("Producer tries to write.");
                statusUpdate("Buffer full. Producer waits.");
                wait();
            } catch (InterruptedException e) {}
        }
        
        buffer = val;
        
        occupied = true;
        
        statusUpdate("Producer writes " + buffer);
        
        notifyAll();
    }
    
    @Override
    public synchronized int get()
    {
        while (!occupied) {
            try {
                System.out.println("Consumer tries to read.");
                statusUpdate("Buffer empty. Consumer waits.");
                wait();
            } catch (InterruptedException e) {}
        }
        
        occupied = false;
        statusUpdate("Consumer reads " + buffer);
        
        notifyAll();
        
        return buffer;
    }
    
    public void statusUpdate(String status)
    {
        System.out.printf("%-40s%d\t\t%b\n\n", status, buffer, occupied);
    }
}
SynchTest.java
package edu.govschool.threads.synch;

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

/**
 * Class to test the use of the <code>SynchronizedBuffer</code>.
 * @author Mr. Davis
 */
public class SynchTest 
{
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();

        // We need to use our new, synchronized buffer!
        Buffer sharedLocation = new SynchronizedBuffer();

        // The output is slightly different
        System.out.printf("%-40s%s\t\t%s\n%-40s%s\n\n", "Operation", "Buffer",
                "Occupied", "---------", "------\t\t--------");

        threadPool.execute(new Producer(sharedLocation));
        threadPool.execute(new Consumer(sharedLocation));

        threadPool.shutdown();
    }
}
Operation                               Buffer      Occupied
---------                               ------      --------

Consumer tries to read.
Buffer empty. Consumer waits.           -1          false

Producer writes 1                       1           true

Consumer reads 1                        1           false

Consumer tries to read.
Buffer empty. Consumer waits.           1           false

Producer writes 2                       2           true

Consumer reads 2                        2           false

Consumer tries to read.
Buffer empty. Consumer waits.           2           false

Producer writes 3                       3           true

Consumer reads 3                        3           false

Consumer tries to read.
Buffer empty. Consumer waits.           3           false

Producer writes 4                       4           true

Consumer reads 4                        4           false

Consumer tries to read.
Buffer empty. Consumer waits.           4           false

Producer writes 5                       5           true

Consumer reads 5                        5           false

Consumer tries to read.
Buffer empty. Consumer waits.           5           false

Producer writes 6                       6           true

Consumer reads 6                        6           false

Consumer tries to read.
Buffer empty. Consumer waits.           6           false

Producer writes 7                       7           true

Consumer reads 7                        7           false

Consumer tries to read.
Buffer empty. Consumer waits.           7           false

Producer writes 8                       8           true

Consumer reads 8                        8           false

Consumer tries to read.
Buffer empty. Consumer waits.           8           false

Producer writes 9                       9           true

Consumer reads 9                        9           false

Producer writes 10                      10          true

Producer done producing
Terminating Producer
Consumer reads 10                       10          false

Consumer reads vals totaling 55
Terminating Consumer