Vous êtes sur la page 1sur 17

J.E.D.I.

1 Java Threads
1.1

Objectives

This chapter discusses Java threads. This includes synchronization commands as well as
solutions to the synchronization problems discussed in a previous chapter.

1.2

Chapter Outline

Creating a Java Thread

Synchronized Keyword

Wait and Notify

High Level Concurrency Objects

1.3

Creating a Java Thread

Java threads are a way to have different parts of your program running at the same time. For
example, you can create an application that accepts input from different users at the same
time, each user handled by a thread. Also, most networking applications involve threads; you
would need to create a thread that would wait for incoming messages while the rest of your
program handles your outgoing messages.
This section discusses how to create java threads. There are two ways to create a Java thread,
one is by extending the thread class, the other is by implementing the Runnable interface. The
run() method of a thread is its main method, thread execution always starts from this method.

1.3.1

Extending the Thread class

We will create a simple thread that simply prints out a number 500 times in a row.
class MyThread extends Thread {
int i;
MyThread(int i) {
this.i = i;
}
public void run() {
for (int ctr=0; ctr < 500; ctr++) {
System.out.print(i);
}
}
}
To show the difference between parallel and non-parallel execution, we have the following
executable MyThreadDemo class
class MyThreadDemo {
public static void main(String args[]) {
MyThread t1 = new MyThread(1);
MyThread t2 = new MyThread(2);
MyThread t3 = new MyThread(3);
t1.run();
t2.run();
Operating Systems

J.E.D.I.

}}

t3.run();
System.out.print("Main ends");

Upon executing MyThreadDemo, we will get output that looks like this
> java MyThreadDemo
1111111111...22222222.....33333......Main ends
As can be seen, our program first executed t1's run method, which prints 500 consecutive 1's.
After that t2's run method, which prints consecutive 2's, and so on. Main ends appear at the
last part of the output as it is the last part of the program. No multithreaded execution
occurred.

To execute these threads concurrently, we invoke the built-in start() method, instead of calling
the run() method directly.
class MyThreadDemo {
public static void main(String args[]) {
MyThread t1 = new MyThread(1);
MyThread t2 = new MyThread(2);
MyThread t3 = new MyThread(3);
t1.start();
t2.start();
t3.start();
System.out.print("Main ends");
}}

Operating Systems

J.E.D.I.

Our second thread demo would produce the following output.


> java MyThreadDemo
111111111122331122321122333331111Main ends111333222...
We can see that threads t1, t2 and t3 are all running at the same time as printing of 1's and
2's and 3's are interspersed. Very interesting to note is the appearance of the "Main ends"
string in the middle of the output sequence, which indicates that the main method has already
finished executing while thread 1, thread 2 and thread 3 are still running.
Running a main method creates a thread. Normally, your program ends when the main thread
ends. However, creating and then running a thread's start method creates a whole new thread
that executes its run() method independent of the main method.

1.3.2

Implementing Runnable

Another way to create a thread is to implement the Runnable interface. This may be desired if
your thread extends another class. By extending another class, you will be unable to extend
class thread as Java classes can only extend a single class. However, a class can implement
more than one interface.
The following is our MyThread class created by implementing the runnable interface. In
implementing the runnable interface, you must define the run() method.
class MyThread implements Runnable {
... <thread body is mostly the same>
}
The main difference comes in instantiating MyThread:
Operating Systems

J.E.D.I.

Thread t1 = new Thread(new MyThread(1));


MyThread is passed as a parameter to the constructor of a thread object.

1.3.3

Pausing Threads

Threads can be paused by the sleep() method. For example, to have MyThread pause for half a
second before it prints the next number, we add the following lines of code to our for loop.
for (int ctr=0; ctr < 500; ctr++) {
System.out.print(i);
try {
Thread.sleep(500); // 500 miliseconds
} catch (InterruptedException e) { }
}
The sleep() method is a static member of the Thread class and can be invoked from any
thread, including the main thread. For example, if we wanted to pause for a second before
starting thread t2 in our main method, we could have:
public static void main(String args[]) {
...
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) { }
t2.start();

1.3.4

Joins

You can also make a thread stop until another thread finishes running by invoking a thread's
join() method. For instance, to make our main thread to stop running until thread t1 is
finished, we could simply say...
public static void main(String args[]) {
...
t1.start();
try {
t1.join();
} catch (InterruptedException e) { }
t2.start();
This would make t1 finish running before thread 2 is started.

1.4

Synchronized Keyword

In this section , we will find out how to implement a solution to the critical section problem
using Java. Recall that only a single process can enter its critical section, all other processes
would have to wait. No context switching occurs inside a critical section.
Instead of printing a continuous stream of numbers, MyThread calls a print10() method in a
class MyPrinter. print10() prints 10 continuous numbers on a single line before a newline.
Our goal is to have these 10 continuous numbers printed without any context switches
occurring. In other words, our output should be:
...
1111111111
Operating Systems

J.E.D.I.

1111111111
2222222222
3333333333
1111111111
...

1.4.1

Defining the solution

We will define a class MyPrinter that will have our print10() method
class MyPrinter {
public void print10(int value) {
for (int i = 0; i < 10; i++) {
System.out.print(value);
}
System.out.println(""); // newline after 10 numbers
}
}
Instead of printing numbers directly, we use the print10() method in MyThread as shown by
the following:
class MyThread extends Thread {
int i;
MyPrinter p;
MyThread(int i) {
this.i = i;
p = new MyPrinter();
}
public void run() {
for (int ctr=0; ctr < 500; ctr++) {
p.print10(i);
}
}
}
First we will see the output of just a single thread by commenting out the other threads:
class MyThreadDemo {
public static void main(String args[]) {
MyThread t1 = new MyThread(1);
// MyThread t2 = new MyThread(2);
// MyThread t3 = new MyThread(3);
t1.start();
// t2.start();
// t3.start();
System.out.print("Main ends");
}}
When we run our MyThreadDemo, we can see that the output matches what we want:
> java MyThreadDemo
1111111111
1111111111
1111111111
Operating Systems

J.E.D.I.

1111111111
1111111111
...
However, running the other threads would give us output that looks like this:
> java MyThreadDemo
1111111111
111112222222
1111
22233333332
...
We do not achieve our goal of printing 10 consecutive numbers in a row as all three threads
are executing the print10's method at the same time. The end result is that we have more
than one number appearing on a single line.
There should not be a context switch while we are printing 10 consecutive numbers. Thus, a
solution to this problem is also a solution to the Critical Section Problem.

1.4.2

Monitors in Java

Java uses a monitor construct. Only a single thread may run inside the monitor. To turn an
object into a monitor, simply put the synchronized keyword on your method definitions. Only a
single thread can run any synchronized method in an object.
class MyPrinter {
public synchronized void print10(int value) {
for (int i = 0; i < 10; i++) {
System.out.print(value);
}
System.out.println(""); // newline after 10 numbers
}
}
However, even if we did turn our print10() method into a synchronized method, it will still not
work. To find out how to make it work, we need to first find out how the synchronized keyword
works.
Every object in Java has a lock. When a thread tries to run a synchronized method, it first tries
to get a lock on the object. If it gets the lock then the thread runs the synchronized method, if
not, then it waits until the thread who owns the lock unlocks it.

Operating Systems

J.E.D.I.

Our example does not work because each thread has its own copy of the MyPrinter object.
class MyThread extends Thread {
int i;
MyPrinter p;
MyThread(int i) {
this.i = i;
p = new MyPrinter();
// each MyThread creates its own MyPrinter!
}
public void run() {
for (int ctr=0; ctr < 500; ctr++) {
p.print10(i);
}
}
}
Our object diagram would look like the one below. As we can see, each thread owns its own
lock thus all of them can run the synchronized method

A proper solution only gives one copy of a MyPrinter object to all threads.

Operating Systems

J.E.D.I.

A way to visualize it is that all Java objects can be doors. A thread tries to see if the door is
open. Once the thread goes through the door, it locks it behind it. No other threads can enter
the door because our first thread has locked it from the inside. Other threads can enter if the
thread inside unlocks the door and goes out. Lining up will only occur if everyone only has a
single door to the critical region
Again, only a single thread can run any synchronized method in an object. If an object has a
synchronized method and a regular method, only one thread can run the synchronized method
while multiple threads can run the regular method. Threads that you want to have
synchronized must share the same monitor object. Running MyThreadDemo now correctly
shows synchronized methods at work.

1.4.3

Synchronized blocks

Aside from synchronized methods, Java also allows for synchronized blocks. You must specify
what object the intrinsic lock comes from. Synchronized blocks allow for flexibility, in the case
if we want our intrinsic lock to come from an object other than the current object. Also, we can
have parts of a method having a different lock than others.
Consider the following modified MyPrinter class
class MyPrinter {
Object lock1 = new Object();
Object lock2 = new Object();
public void print10(int value) {
synchronized(lock1) {
for (int i = 0; i < 10; i++) {
System.out.print(value);
}
System.out.println(""); // newline after 10 numbers
}
}
public int squareMe(int i) {
synchronized (lock2) {
return i * i;
}
}
}
Only one thread can run inside the synchronized block of print10() and squareMe(). However,
two different threads can be running in both synchronized blocks at the same time as we use
different locks for the two of them (as they don't really interfere with one another).
Synchronized blocks would also allow other parts of a method to be run in parallel with others,
Operating Systems

J.E.D.I.

as shown by the following diagram.

1.5

Wait and Notify

1.5.1

Producer-consumer problem

At this stage, we can already implement some solutions to our synchronization problems.
Here, we will now discuss a solution to the Producer-Consumer problem. Instead of using beer,
the producer will store in a shared array a random integer value which will be retrieved by the
consumer process. For discussion purposes, we will assume that the array can store a very
large number of integers. The Producer will attempt to produce 100 integers in total.
First, we create a class that would store our shared variables. We have discussed before that
we do not want insertValue() and getValue() to run at the same time so we convert these to
synchronized methods and make sure that both Consumer and Producer get the same
SharedVars object.
class SharedVars {
int array[] = new int[10000];
int top;
// method for inserting a value
Operating Systems

J.E.D.I.

public synchronzed void insertValue(int value) {


if (top < array.length) {
array[top] = value;
top++;
}
}
// method for getting a value
public synchronzied int getValue() {
if (top > 0) {
top--;
return array[top];
}
else
return -1;
}

}
Next, we define our Producer class as such.

class Producer extends Thread {


SharedVars sv;
Producer(SharedVars sv) {
// Producer's reference to the SharedVars object created in main
this.sv = sv;
}
public void run() {
for (int i = 0; i < 100; i++) {
// assigning a random number from 0 to 200
int value = (int)(Math.random() * 200);
System.out.println("Producer inserts " + value);
sv.insertValue(value);
try {
// wait a random amount of time before inserting again
Thread.sleep((int)(Math.random() * 10000));
} catch (InterruptedException e) { }
}
}
}
For our consumer thread, we have the following code:
class Consumer extends Thread {
SharedVars sv;
Consumer(SharedVars sv) {
// Consumer's reference to the SharedVars object created in main
this.sv = sv;
}
public void run() {
for (int i = 0; i < 100; i++) {
int value = sv.getValue();
System.out.println("Consumer got:" + value);
try {
// wait a random amount of time before getting again
Thread.sleep((int)(Math.random() * 10000));
}catch (InterruptedException e) { }
}
}
}
Our main class would look like this:
class ProducerConsumerDemo {
public static void main(String args[]) {
Operating Systems

10

J.E.D.I.

SharedVars sv = new SharedVars();


Producer p = new Producer(sv);
Consumer c = new Consumer(sv);
p.start();
c.start();
}
}
The output of this code would be an orderly sequence of output:
Producer
Consumer
Consumer
Producer
Producer
Consumer
...

inserts value: 15
got: 15
got: -1
inserts value: 50
inserts value: 75
got: 75

Notice that, if we try to get a value from the array and there isn't any, we return a value -1.
You can see this clearly if you increase the Producer delay to 10s, which means the Producer
adds a value every 10s, consumer gets one every 5s. At some point in time, the consumer
would get from an empty array.
Wouldn't it be better if we have our Consumer wait for the Producer to produce something
instead of just getting -1?

1.5.2

Wait method

The wait() method, defined in class Object and therefore inherited by all objects will cause the
thread invoking wait() to suspend itself. However, a thread invoking wait() must own the
intrinsic lock of the object it is calling wait() from. If we are going to call this.wait() it has to be
in synchronized(this) block
Also, as with all methods that suspend thread execution, like join() and sleep(), our wait()
method must be in a try-catch block that catches InterruptedExceptions.
The following code shows our modification of the getValue() method in class SharedVars:
// called by Consumer thread
public int getValue() {
synchronized(this) {
if (top <= 0) {
try {
this.wait();
} catch (InterruptedException e) { }
}
top--;
return array[top];
}}

Operating Systems

11

J.E.D.I.

1.5.3

Notify method

All threads that call wait() on an object are placed in a pool of waiting threads for that object.
Execution resumes when another thread calls the notify() method of the object our first thread
is waiting on.
For our example, our producer thread, after inserting on an empty array, would notify the
consumer thread that it has placed a value in our array by calling the notify() method on the
SharedVars object. Recall that Producer and Consumer both have a reference to the same
SharedVars object.
The producer calling notify() from insertValue() would inform any the consumer waiting on the
same SharedVar object.
The following is our ne insertValue() method
// called by producer
public void insertValue(int value) {
synchronized(this) {
if (top < array.length) {
array[top] = value;
top++;
if (top == 1) { // insert on an empty array
Operating Systems

12

J.E.D.I.

this.notify(); // notify sleeping thread


}
}

When the notify() method of an object is called, then a single waiting thread on that object is
signaled to get ready to resume execution.
After our Producer thread exists insertValue and releases the lock, our Consumer thread gets
the lock once again and resumes its execution.

If you run the demo program again, you can see that, even if the producer is slow, the
consumer waits until the producer gives out a value before getting that value. Note that you
must also consider inserting on a full array. Producer must wait until consumer has taken away
some values before inserting.
We will leave the implementation of this as an exercise.

1.6

High level concurrency objects

The methods discussed have been defined in Java from the very start. Since Java 1.5,
additional classes have been added in the java.util.concurrent package that assist in the
creation and manipulation of threads. We will discuss these in passing in this section.

Operating Systems

13

J.E.D.I.

1.6.1

Lock Objects

Any object in Java can be used as a lock for synchronization. However, the
java.util.concurrent.locks package defines classes that have additional locking features. The
basic interface of this package is the Lock interface.
An object of type Lock can only be owned by a single thread at a time. To acquire a lock, a
thread must call its lock() method. To release a lock, it must call its unlock() method. Instead
of the implicit locking and unlocking of a synchronized block or method, a lock provides the
flexibility of manually calling lock() and unlock().
A Lock also has a tryLock() method. This tryLock() method returns a true or false value, true if
it managed to get the lock, false otherwise. The program does not block if tryLock() does not
succeed, in contrast to the lock() method, and the behavior of a thread if it tries to enter a
synchronized block or method. In addition, a different version of tryLock() allows you to
specify the amount of time the thread waits to establish a lock before it returns true or false.
The following is our class MyPrinter implementation (in our MyThread2 example) using locks.
Note that there is no longer the implicit unlocking that happens at the end of a synchronized
block, so any lock() or tryLock() call must be followed by an unlock() call.
import java.util.concurrent.locks.*;
class MyPrinter {
final Lock l = new ReentrantLock(); // ReentrantLock is an implementation
// of the Lock interface
public void print10(int value) {
boolean gotLock = false;
while (!gotLock) {
gotLock = l.tryLock();
if (!gotLock) {
System.out.println("Unable to get lock, try again
next time");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) { }
}
}
for (int i = 0; i < 10; i++) {
System.out.print(value);
}
System.out.println(""); // newline after 10 numbers
l.unlock();
}
}
Our wait() and notify() methods can only be called on the intrinsic lock of a synchronized
method or block. To do the same with the Lock class, we can use its newCondition() method,
which returns an object of type Condition. A condition object has an await() method which is
the equivalent of a wait() call (it waits on the Lock object the condition was extracted from). To
wake up a thread that is waiting on a lock object, all condition objects have a signal() method,
which is the equivalent of a notify() call.
For example, this is an excerpt our SharedVars class modified using locks and conditions.
import java.util.concurrent.locks.*;
class SharedVars {
final Lock l = new ReentrantLock();
final Condition emptyCondition = l.newCondition();
// method for inserting a value
Operating Systems

14

J.E.D.I.

public void insertValue(int value) {


l.lock() // engage lock
if (top < array.length) {
array[top] = value;
top++;
if (top == 1){
emptyCondition.notify();
}
}
l.unlock();
}
// method for getting a value
public int getValue() {
l.lock();
if (top <= 0) {
try {
emptyCondition.wait();
} catch (InterruptedException e) { }
}
top--;
l.unlock();
return array[top];
}
}
There are other classes in the java.util.concurrent.locks package. One of them is the
ReentrantReadWriteLock class, which has methods which return locks. These locks can be used
as a solution to the Readers-Writers problem.

1.6.2

Executors

Whenever we create a thread, we manually state when that thread is executed. For instance,
given the following thread:
class MyThread implements Runnable { ... }
We manually start the thread by the two commands:
Thread t1 = new Thread(mt1);
t1.start();
An implementing class of the Executor interface abstracts thread starting from the
programmer. For instance, instead of the above code, we have the following:
MyThread mt1 = new MyThread();
Executor e = new ThreadPoolExecutor(10); // accepts max 10 threads
e.execute(mt1);
e.execute(otherThreads);
Our example uses the ThreadPoolExecutor class. This class runs our inputted threads (via the
execute() method) via a ready pool of execution threads. These execution threads reduce the
overhead needed for thread creation, a thread is merely passed as a parameter to an already
running worker thread for execution.
A parameter specified in the constructor indicates how many threads the executor can run all
at once. If an executor is not used, then threads are run at the moment they are started. If

Operating Systems

15

J.E.D.I.

there are too many threads that are running all at once, the system can not cope and all
threads are stopped. An executor allows for a specified maximum number of running threads,
allowing the system to handle only the number of threads it can control and reject or queue
those it cannot.
Additional methods indicate how incoming threads are to be queued or to when to terminate
inactive threads. A subclass of ThreadPoolExecutor, the ScheduledThreadPoolExecutor class,
has methods that allow for a partciular thread to start at a certain time of the day, or be
repeated periodically.

1.6.3

Concurrent Collections

The java.util.concurrent package includes collection classes that can be used with
multithreaded programs use without having to worry about synchronization problems. For
instance, the BlockingQueue class creates a first-in-first out queue that automatically blocks
threads when they try to add to a full queue or retrieve values from an empty one. Execution
automatically resumes when another thread removes a value from a full queue or adds data to
an empty one.

1.6.4

Atomic Variables

As we have seen in our Producer-Consumer problem, the commands:


array[top] = <new value>;
top++;
must be executed without any interleaving. This is called atomic execution, each command
must be executed as a single whole.
However, even single commands are not atomic. Consider the command:
top ++;
Top++ is divided into three parts when run on the CPU
1. Get the value of top
2. Add 1 to that value
3. Save that value to the address of top.
All these three commands must be executed without any interleaving commands from other
threads. One way around this is to have a synchronized increment method.
class SharedVars {
int top;
public synchronized inctop() {
top++;
}
}
Most of the other arithmetic operations are sadly, non-atomic as well, meaning each
mathematical operation, when dealing with shared variables, has to be in a synchronized block
to be safe. This includes even assignment statements. To avoid this tediousness, the
java.util.concurrent.atomic package provides already thread-safe classes.
For instance, the AtomicInteger class provides some of the methods below:
Operating Systems

16

J.E.D.I.

void incrementAndGet() - performs an increment operation atomically

void addAndGet(int delta) adds delta to the AtomicInteger atomically.

There are also implementations like this for an integer array or an object reference variable.
Also, most of the classes in java.util are already thread safe.

Operating Systems

17

Vous aimerez peut-être aussi