Assignment 9:

Due Date: 13 April 2010 – 7:20 PM

 

Purpose:

The purpose of this assignment is to acquire understanding and experience with Java Thread Pools.

Assignment Details:

 

This assignment is composed of several small Java programs that will be utilized to explore several features of concurrency primitives support supported by the Java Programming Language:

Background

 

Before starting this assignment it is important that you have a general understanding of Java Threads. The Java Programming Language conceptualizes and realizes the concept of a thread from both language constructs and semantics at compile time to the Java Virtual Machine at runtime. The concept and realization of threads have been in existence for many years and have been addressed in both hardware and software. In addition the concept of process is frequently used in engineering articles, papers, book, etc., that requires an engineer in many cases to understand the differences between the two concepts.

For this homework assignment we will explicitly define the concepts of process and thread:

·         Process – We define a process to be an execution unit. This execution unit runs an individual program. We do not delineate between hardware or software implementations and executions of a process, instead we abstract a process to be responsible for the execution of an individual program. We assert that multiple processes can be executing simultaneously to enhance resource utilization and provide fairness to multiple processes. Do not confuse process with parallelization of an individual program.

·         Thread – We define Thread not as a Java construct but a general concept for this definition. We implicitly state that a thread is contained within a process that can be scheduled, execute simultaneously, and communicate asynchronously.  Threads have the same memory space of the process that serves as their container enabling their access to the same heap variables and objects. This is interpreted as threads contained within a process can share data across member threads and not having to invoke inter-process services. This also implies that threads require a synchronization construct for coordinating shared accessed across threads in the container process.

·         Java Thread – We define a Java Thread that can transparently share access between any object in the heap. Each thread still has its own space for local variables (variables specific to the method the thread is executing). But objects are shared automatically and transparently. Therefore, a thread is a discrete task that operates on data shared with other threads.

Thread Creation

The Java Thread is based on the Java Thread class, which we show a part of the API specification below:

package java. lang;

public class Thread implements Runnable {

    public Thread( );

    public Thread(Runnable target) ;

    public Thread(ThreadGroup group, Runnable target) ;

    public Thread(String name) ;

    public Thread(ThreadGroup group, String name) ;

    public Thread(Runnable target, String name) ;

    public Thread(ThreadGroup group, Runnable target, String name) ;

    public Thread(ThreadGroup group, Runnable target, String name, long stackSize) ;

    public void start( );

    public void run( );

}

Note that threads are created with four attributes:

·         Thread name - The name of a thread is part of the information shown when a thread object is printed. Otherwise, it has no significance, so give your threads names that make sense to you when you see them printed. The default name for a thread is Thread-N, where N is a unique number.

·         Runnable target – A runnable object is the list of instructions that the thread executes. By default, this is the information in the run() method of the thread itself. N.B., the Thread class itself implements the Runnable interface.

·         Thread group – The Thread group is not utilized for SWE619.

·         Stack size – The Stack size is not utilized for SWE619.

It is important to understand an instance of the Java Thread class is an object. The Thread object can be passed to other method. A thread that has a reference to another thread can execute any method of the other thread’s Thread object. To summarize the Thread object is not the thread itself, but it is a set of methods and data that encapsulates information about the thread and that method and data can be accessed by any other thread.

Thread Lifecycle

We will simply the Java thread lifecycle to a UML state diagram shown below:

The key to understanding Java thread scheduling is to realize that a CPU is a scarce resource. When two or more threads want to run on a single-processor machine, they end up competing for the CPU, and it’s up to someone—the programmer, the Java virtual machine, or the operating system—to make sure that the CPU is shared among these threads. The same is true whenever a program has more threads than the machine hosting the program has CPUs. The essence is to understand how CPUs are shared among threads that want to access them.

The topic of thread scheduling is a difficult one to address because the Java specification does not require implementations to schedule threads in a particular manner. It provides guidelines that threads should be scheduled based on a thread’s priority, but they are not absolute, and different implementations of the Java virtual machine.

Problem 1:

 

Execute the following code with three different combinations of number of threads and factorial settings:

import java. util. *;

import java. text. *;

public class Task implements Runnable {

    long n;

    String id;

    private long fib(long n) {

        if (n == 0)

            return 0L;

        if (n == 1)

            return 1L;

        return fib(n - 1) + fib(n - 2) ;

    }

    public Task(long n, String id) {

        this. n = n;

        this. id = id;

    }

    public void run( ) {

        Date d = new Date( );

        DateFormat df = new SimpleDateFormat("HH: mm: ss: SSS") ;

        long startTime = System. currentTimeMillis( );

        d. setTime(startTime) ;

        System. out. println("Starting task " + id + " at " + df. format(d) ) ;

        fib(n) ;

        long endTime = System. currentTimeMillis( );

        d. setTime(endTime) ;

        System. out. println("Ending task " + id + " at " +  df. format(d) + " after " + (endTime - startTime) + " milliseconds") ;

    }

}

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

 

 

public class ThreadTest {

    public static void main(String[ ] args) {

            int nThreads = 1;

            BufferedReader keyboard;

            InputStreamReader reader;

            reader = new InputStreamReader(System.in);

            keyboard  = new BufferedReader(reader);

            String InputText = "";

            try {

                System.out.print("Input Number of Threads = ");

                InputText = keyboard.readLine( );

            } catch (IOException e) {

                System.err.println("Problem with input stream");

                e.printStackTrace();

            }

            nThreads = Integer.parseInt(InputText);

            long n = 0;

            try {

                System.out.print("Input factorial (n) = ");

                InputText = keyboard.readLine( );

            } catch (IOException e) {

                System.err.println("Problem with input stream");

                e.printStackTrace();

            }

            n = Long.parseLong(InputText);

            Thread t[] = new Thread[nThreads];

           

                for (int i = 0; i < t.length; i++) {

                            t[i] = new Thread((Runnable)new Task(n, "Task " + i));

                            t[i].setPriority((i % 10) + 1);

                            t[i].start();

                        }

                        for (int i = 0; i < t.length; i++) {

                            try {

                                t[i].join();

                            } catch (InterruptedException ie) {}

        }

            }

        }

Submission:

1.       Output file of three runtime executions, which include 3 runtime executions for each combinations of “Number of Threads” and “factorial (n)”

2.       Analysis description of each runtime execution, which includes the following:

a.       Output

b.      Analysis of each output (attempt to explain thread scheduling, explain variations across same runtime combination)

c.       Analysis across three different runtime execution sets.

Thread Pools

So far we have implemented and analyzed Java Threads that were created on demand. There is an overhead associated with creating threads on demand that may have an impact on overall program performance and resource management. To address these concerns thread pools are utilized. The idea behind a thread pool is to set up a number of threads that sit idle, waiting for work that they can perform. As your program has tasks to execute, it encapsulates those tasks into some object (typically a Runnable object) and informs the thread pool that there is a new task. One of the idle threads in the pool takes the task and executes it; when it finishes the task, it goes back and waits for another task. Thread pools have a maximum number of threads available to run these tasks. Consequently, when you add a task to a thread pool, it might have to wait for an available thread to run it. That may not sound encouraging, but it’s at the core of why you would use a thread pool.

The first reason thread pools are often recommended is because it’s felt that the overhead of creating a thread is very high; by using a pool, we can gain some performance when the threads are reused. The degree to which this is true depends a lot on your program and its requirements. It is true that creating a thread can take as much as a few hundred microseconds, which is a significant amount of time for some programs.

The second reason for using a thread pool is very important: it allows for better program design. If your program has a lot of tasks to execute, you can perform all the thread management for those tasks yourself, but, as we’ve started to see in our examples, this can quickly become tedious; the code to start a thread and manage its lifecycle isn’t very interesting. A thread pool allows you to delegate all the thread management to the pool itself, letting you focus on the logic of your program. With a thread pool, you simply create a task and send the task to the pool to be executed; this leads to much more elegant programs.

The third reason to use a thread pool is that they carry important performance benefits for applications that want to run many threads simultaneously. In fact, anytime you have more active threads than CPUs, a thread pool can play a crucial role in making your program seem to run faster and more efficiently.

Java Executors

Java’s implementation of thread pools is based on an executor. An executor is a

generic concept modelled by this interface:

package java. util. concurrent;

public interface Executor {

    public void execute(Runnable task) ;

}

Executors are a useful design pattern for multithreaded programs because they allow you to model your program as a series of tasks. You don’t need to worry about the thread details associated with the task: you simply create the task and pass it to the execute( ) method of an appropriate executor.

Problem 2:

Analyze the code below and make sure you are familiar with how this code creates a thread pool. You will need review the Java API for ThreadPoolExecutor and ExecutorService to understand how this implementation works.

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.util.concurrent.*;

 

public class ThreadPoolTest {

    public static void main(String[] args){

   

    int nTasks = 1;

    BufferedReader keyboard;

    InputStreamReader reader;

    reader = new InputStreamReader(System.in);

    keyboard  = new BufferedReader(reader);

    String InputText = "";

    try {

        System.out.print("Input Number of Tasks = ");

        InputText = keyboard.readLine( );

    } catch (IOException e) {

        System.err.println("Problem with input stream");

        e.printStackTrace();

    }

    nTasks = Integer.parseInt(InputText);

   

    long n = 0;

    try {

        System.out.print("Input factorial (n) = ");

        InputText = keyboard.readLine( );

    } catch (IOException e) {

        System.err.println("Problem with input stream");

        e.printStackTrace();

    }

    n = Long.parseLong(InputText);

   

    int tpSize =0;

        try {

            System.out.print("Input Thread Pool Size = ");

            InputText = keyboard.readLine( );

        } catch (IOException e) {

            System.err.println("Problem with input stream");

            e.printStackTrace();

        }

        tpSize = Integer.parseInt(InputText);

   

        ThreadPoolExecutor tpe = new ThreadPoolExecutor(

                    tpSize, tpSize, 50000L, TimeUnit.MILLISECONDS,

                    new LinkedBlockingQueue<Runnable>( ));

 

        Task[] tasks = new Task[nTasks];

        for (int i = 0; i < nTasks; i++) {

            tasks[i] = new Task(n, "Task " + i);

            tpe.execute(tasks[i]);

        }

        tpe.shutdown( );

    }

}

Submission:

1.       Output file of three runtime executions, which include 3 runtime executions for each combination of “Number of Threads”, “factorial (n)”, and “Thread Pool Size.”

2.       Analysis description of each runtime execution, which includes the following:

a.       Brief one page description that describes how a Java thread pool is implemented utilizing the Executor interface, ThreadPoolExecutor, and ExecutorService.

b.      Output

c.       Analysis of each output (indicate what you are observing for each combination of Number of Threads”, “factorial (n)”, and “Thread Pool Size.”

d.      Analysis across three different runtime execution sets (indicate what you are observing across all combination of Number of Threads”, “factorial (n)”, and “Thread Pool Size.” For example, is one combination better than another; does the number of CPUs have a factor; etc.