Pract7

advertisement

Concurrent & Distributed Computing

PRACTICAL 7

– More on Java Threads

Note

In Java certain thread APIs were introduced to facilitate thread suspension, resumption, and termination but were later deprecated because of inherent design weaknesses. For example, the Thread.stop() method causes the thread to immediately throw a ThreadDeath exception, which usually stops the thread. It is advisable to not use deprecated or obsolete classes or methods.

Invoking Thread.stop() results in the release of all locks a thread has acquired, potentially exposing the objects protected by those locks when those objects are in an inconsistent state.

Consequently, programs must not invoke Thread.stop().

Question 1 – Using Thread.stop() – this is dangerous!

This noncompliant code example shows a thread that fills a vector with pseudorandom numbers. The thread is forcefully stopped after a given amount of time. import java.util.*; public class NumberHolder1 implements Runnable { private final int CAPACITY = 1000; private final ArrayList<Integer> vector = new ArrayList<>( CAPACITY ); public ArrayList<Integer> getVector() { return vector ;

}

@Override public synchronized void run() {

Random number = new Random(); int i = CAPACITY ; while ( i > 0) { vector .add( number .nextInt(100)); try {

Thread.sleep(50);

} catch (InterruptedException ie ) {

} i --;

}

} // end run

} //end NumberHolder

1

import java.util.*; public class TestNumberHolder1 { public static void printGeneratedNumbers(ArrayList<Integer> thisVector ) {

// enumerate the elements in the vector.

System.

out .println( "\nElements in vector:" ); for (Integer i : thisVector ) {

System.

out .print( i + " " );

}

System.

out .println();

} // end printGeneratedNumbers public static void main(String[] args ) throws InterruptedException {

NumberHolder1 holder = new NumberHolder1();

Thread numberHolderThread = new Thread( holder ); numberHolderThread .start();

Thread.sleep(1000); numberHolderThread .stop();

System.

out .println( "The thread was terminated using the deprecated Java stop() method" );

ArrayList<Integer> temp = holder .getVector();

System.

out .println( "The vector has the following " + temp .size() + " values:" );

printGeneratedNumbers( temp );

} // end main

} //end TestNumberHolder1

Test this code to verify its behavior. Change the sleep delays to get different sizes of vectors generated. Note that stop() is deprecated (indicated by a line through it)

The ArrayList class is not thread-safe, and so operations performed by multiple threads on its shared instance may leave it in an inconsistent state. For instance, the Vector.size() method may not always return the correct number of elements in the vector.

The Thread.stop() method causes the thread to stop what it is doing and throw a ThreadDeath exception. All acquired locks are subsequently released. If the thread were in the process of adding a new integer to the vector when it was stopped, the vector would become accessible while it is in an inconsistent state. For example, this could result in temp.size() returning an incorrect element count if the thread was stopped between incrementing size and adding the element to the vector.

Question 2

– Using a Volatile flag to stop a thread – this is OK

This compliant solution uses a volatile flag to request thread termination. The shutdown() method is used to set the flag to true. The thread's run() method polls the done flag and terminates when it is set.

2

import java.util.*; public class NumberHolder2 implements Runnable { private final int CAPACITY = 1000; private final ArrayList <Integer> vector = new ArrayList<>(1000); private volatile boolean done = false ; public ArrayList<Integer> getVector() { return vector ;

} public void shutdown() { done = true ;

}

@Override public synchronized void run() {

Random number = new Random(); int i = CAPACITY ; while (!

done && i > 0) { vector .add( number .nextInt(100)); try {

Thread.sleep(50);

} catch (InterruptedException ie ) {

} i --;

}

} // end run

} //end NumberHolder2 import java.util.*; public class TestNumberHolder2 { public static void printGeneratedNumbers(ArrayList<Integer> thisVector ) {

System.

out .println( "\nElements in vector:" ); for (Integer i : thisVector ) {

System.

out .print( i + " " );

}

System.

out .println();

} // end printGeneratedNumbers public static void main(String[] args ) throws InterruptedException {

NumberHolder2 holder = new NumberHolder2();

Thread numberHolderThread = new Thread( holder ); numberHolderThread .start();

Thread.sleep(500); holder .shutdown(); // stops the thread

System.

out .println( "The thread was terminated using the volatile flag method" );

ArrayList<Integer> temp = holder .getVector();

3

System.

out .println( "The vector has the following " + temp .size() + " values:" );

printGeneratedNumbers( temp );

} // end main

} //end TestNumberHolder2

Test this code to verify its behaviour. Change the sleep delays to get different sizes of vectors generated and verify that the value that temp.size() returns always matches the actual number of elements.

Check out the role of volatile variables.

Note on volatile variables

In the case where one thread reads and writes the value of a volatile variable, and other threads only read the variable, then the reading threads are guaranteed to see the latest value written to the volatile variable. Without making the variable volatile, this would not be guaranteed.

If two threads are both reading and writing to a shared variable, then using the volatile keyword for that is not enough. You need to use synchronization in that case to guarantee that the reading and writing of the variable is atomic.

See the following web link, for further details: http://tutorials.jenkov.com/java-concurrency/volatile.html http://www.javamex.com/tutorials/synchronization_volatile.shtml

Question 3

– Using Thread.interrupt() to stop a Thread – this may be OK

In this compliant solution, the Thread.interrupt() method is called from main() to terminate the thread. Invoking Thread.interrupt() sets an internal interrupt status flag. The thread polls that flag using the Thread.interrupted() method, which both returns true if the current thread has been interrupted and clears the interrupt status flag. import java.util.*; public class NumberHolder3 implements Runnable { private final int CAPACITY = 1000; private final ArrayList<Integer> vector = new ArrayList<>(1000); public ArrayList<Integer> getVector() { return vector ;

}

@Override public synchronized void run() {

Random number = new Random(); int i = CAPACITY ; while (!Thread.interrupted() && i > 0) { vector .add( number .nextInt(100)); try {

Thread.sleep(50);

} catch (InterruptedException ie ) {

}

4

i --;

}

} // end run

} //end NumberHolder3 import java.util.*; public class TestNumberHolder3 { public static void printGeneratedNumbers(ArrayList<Integer> thisVector ) {

System.

out .println( "\nElements in vector:" ); for (Integer i : thisVector ) {

System.

out .print( i + " " );

}

System.

out .println();

} // end printGeneratedNumbers public static void main(String[] args ) throws InterruptedException {

NumberHolder3 holder = new NumberHolder3();

Thread numberHolderThread = new Thread( holder ); numberHolderThread .start();

Thread.sleep(500); numberHolderThread .interrupt(); // interrupts the thread

System.

out .println( "The thread was terminated using the interrupt() method" );

ArrayList<Integer> temp = holder .getVector();

System.

out .println( "The vector has the following " + temp .size()

+ " values:" );

printGeneratedNumbers( temp );

} // end main

} // end TestNumberHolder3

Test this code to verify its behavior. Change the sleep delays to get different sizes of vectors generated.

Note the thread is interrupted rather than stopped but the net result is to exit the loop and hence terminate processing. A thread may use interruption for performing tasks other than cancellation and shutdown. Consequently, a thread should be interrupted only when its interruption policy is known in advance.

Question 4 - Thread Interaction and Synchronization

A running thread can access any object to which it has a reference; so what happens if two threads have access to the same object? The can step on each other ’s toes and cause wrong values to be computed. To solve this problem we need to learn to synchronize access to shared objects.

First consider an example which uses threads but does not synchronize them.

Example

Here we simulate a bank with 10 accounts and randomly generate transactions that move money between these accounts. There will be 10 threads, one for each account. Each

5

transaction will move a random amount of money from the account serviced by the thread to another random account.

Consider the program listing, there are three classes:

UnsynchBankTest : main program to start the transfers

Bank : to simulate the bank activity

TransactionSource: transfers an amount from the source to random destination.

//Class UnSynchBankTest class UnSynchBankTest { public static void main(String[] args ) {

Bank b = new Bank(); for ( int i = 0; i < Bank.

NACCOUNTS ; i ++) new TransactionSource( b , i ).start();

} // end main

} // UnSynchBankTest class Bank { public static final int INITIAL_BALANCE = 10000; public static final int NACCOUNTS = 10; private final long [] accounts ; private int ntransacts ; public Bank() { accounts = new long [ NACCOUNTS ]; for ( int i = 0; i < NACCOUNTS ; i ++) { accounts [ i ] = INITIAL_BALANCE ;

} ntransacts = 0;

test();

} // end constructor public void transfer( int from , int to , int amount ) { while ( accounts [ from ] < amount ) { try {

Thread.sleep(5);

} catch (InterruptedException e ) {

}

} accounts [ from ] -= amount ; accounts [ to ] += amount ; ntransacts ++; if ( ntransacts % 5000 == 0) {

test();

}

} // end transfer private void test() { long sum = 0; for ( int i = 0; i < NACCOUNTS ; i ++) { sum += accounts [ i ];

6

}

System.

out .println( "Transactions:" + ntransacts + " Sum: " + sum );

} // end test

} // end Bank import java.util.Random;

//Class TransactionSource class TransactionSource extends Thread { private final Bank bank ; private final int from ; private final Random rnd ; public TransactionSource(Bank b , int i ) { from = i ; bank = b ; rnd = new Random();

} // end constructor

@Override public void run() { while ( true ) { int to = rnd .nextInt(Bank.

NACCOUNTS ); if ( to == from ) to = ( to + 1) % Bank.

NACCOUNTS ; int amount = rnd .nextInt(Bank.

INITIAL_BALANCE / 4); bank .transfer( from , to , amount ); try {

sleep(1);

} catch (InterruptedException e ) {

}

} // end while

} // end run

} // end TransactionSource

The simulation proceeds as follows:

The transfer (…) method (in class Bank) transfers some amount of money from one account to another. If the source account does not have enough money in it, then the thread calling the transfer (…) method is put to sleep for a short time so that other threads have a chance to transfer some money into this account.

Each thread generates a random transaction, calls transfer (…) on the bank object and then puts itself to sleep. (see run method of TransactionSource class)

(a) Run the program and observe the results.

Before the simulation starts each bank account holds 10000 pounds. When the simulation runs we do not know how much money is in any one bank account at any time. But we do know that the total amount of money should remain unchanged, since all we do is move money from one account to another.

7

Every 5,000 transactions, the transfer (…) method calls a test() method that re-computes the total and prints it.

The program never finishes. Just hit CTRL+C to kill the program.

Here is part of a typical printout:-

Transactions:0 Sum:100000

Transactions:5000 Sum:100000

Transactions:15000 Sum:99840

Transactions:20000 Sum:98079

....

As you can see something is wrong. The total balance is changing! What is the problem?

The problem occurs when two threads are simultaneously trying to update an account.

Suppose two threads simultaneously carry out the instruction: accounts[to] += amount;

The problem is that these are not atomic operations. The instruction might be processed as follows:

1) load account[to] into a register;

2) add amount ;

3) move the result back to accounts[to]

Now suppose the first thread executes step1 and 2, and then it is interrupted. Suppose the second thread awakens and updates the same entry in the amount array. Then the first thread awakens and completes step 3. This wipes out the modifications of the other thread. As a result, the total is no longer correct.

We need a way of ensuring that once a thread has begun an operation it can carry it out to completion without being interrupted. Java has a mechanism based on monitors (invented by

Tony Hoare, Queens University, Belfast ). We simply tag any operation shared by multiple threads as synchronized , like this: public synchronized void transfer(int from, int to, int amount);

{

...

accounts[from] -= amount;

accounts[tp] += amount;

ntransacts++;

if (ntransacts % 5000 == 0) test();

...

}

When one thread enters a synchronized method, Java guarantees that it can finish it before another thread can execute any synchronized method on the same object. When one thread calls transfer, and then another thread calls transfer, the second thread must wait for the first thread to finish.

(Note. When you create an object with one or more synchronized methods, Java sets up a queue of all the threads waiting to be “let inside” the object. Whenever one thread has

8

completed its work with the object, the highest-priority thread in the waiting queue gets a turn next. An object that can block threads and notify them when it becomes available is called a monitor .)

In our simulation the code: if (bank.getBalance(from) >= amount)

bank.transfer(from, to, amount); we must ensure that the thread cannot be interrupted between the test and the insertion, so we put the test inside a synchronized version of the transfer method. public synchronized void transfer (int from, int to, int amount) { while (accounts[from] < amount) {

// wait

}

// transfer funds

}

If the amount[from] happens to be less than amount then the wait suspends this thread and allows another thread to hopefully come in and increase the accounts[from] and issue a notify to the waiting thread (which now becomes activated to continue with its task).

(Note: See the synchronized version of transfer(..) in the program listing below.) public synchronized void transfer( int from , int to , int amount ) { while ( accounts [ from ] < amount ) { try {

wait(); // this wait() replaces sleep()

} catch (InterruptedException e ) {

}

} accounts [ from ] -= amount ; accounts [ to ] += amount ; ntransacts ++; if ( ntransacts % 5000 == 0)

test();

notify(); // this notify() is added to notify waiting threads

} // end transfer

(b) Modify your program to accommodate this synchronized transfer(…) . Note the replacement of sleep(…) with wait(…) and the introduction of notify().

Run the modified program and the total balance should stay at 100000. Make sure you understand the role of wait () and notify().

Question 5 - Deadlock

Even with synchronization deadlock can occur. Consider for example, the following situation:

Account1: £2000

Account2: £3000

Thread1: Transfer £3000 from Account1 to Account2

Thread2: Transfer £4000 from Account2 to Account1

9

Both threads become blocked because there is not sufficient money in either account and so neither can be increased (if we are working with only two accounts).

Such a situation could also occur with 10 accounts (even if it is less likely).

However in the previous example deadlock cannot occur because each transfer is for at most

10,000. Since there are 10 accounts and a total of 100,000 in them, then at least one of them will have more than 10,000 at any time. The thread moving money out of that account can then proceed.

However there are two ways to change the program so that deadlock could occur:

(1) remove the 10,000 transaction limit;

(2) make the (i)th thread responsible for putting money into the (i)th account, rather than for taking it out of the (i)th account (all threads could gang up on one account (by trying to take money out of it) - not so very likely, however).

Modify the program to see if you can cause deadlock in each case.

END

10

Download