Threads

All Java programs that we have seen so far have in common that they only use one thread of execution. This is not effective for utilizing the CPUs in even a cell phone since modern CPUs providing multiple threads (cores) of execution. Furthermore, a program may have to wait for an external resource, e.g., a disk to finish a request before it can make progress. For instance, a program may wait for a user to provide a response. Another typical example are graphical user interfaces (GUIs). While a program is executing a long running operation the GUI should still be responsive, e.g., to allow the user to abort the operation.

In Java concurrency is realized through threads. Threads are running in parallel independent of each other. However, syncronization primitives are provided for threads to coordinate their execution, e.g., if they have to access the same data. A Java program consists at least of one thread that is started automatically and evaluates the main method of the program. A program finishes execution once all of its threads have finished execution. Additional threads can be created by creating an object that extends the class Thread and overrides its run method and call start() on the object. The run method contains the code that should be executed in the thread. Once the run method returns, the thread finishes execution. A Thread can wait for another thread to finish execution by calling join(). The caller will then be blocked until the joined thread finishes execution.

Scheduling

The JVM is responsible for scheduling threads and unless threads are wainting for each other, e.g., because of join, the execution order of operations among different threads is non-deterministic.

Further Reading

In [2]:
import java.lang.Thread;

Thread t = new Thread() {
    
    public void run() {
        System.out.println(1);
        System.out.println(2);
    }
};

t.start();
t.join();
System.out.println(3);
1
2
3
Out[2]:
null

Waiting

When calling Thread.sleep(n) from a thread, the calling thread will be blocked for n seconds. If the sleep is interrupted an InterruptedException will be thrown.

In [6]:
import java.lang.Thread;


final int steps = 15;

Thread t1 = new Thread() {
    public void run() {
        for(int i = 0; i < steps; i++) {
            System.out.println("I am 1 at step " + i);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {

            }
        }
    }   
    
};

Thread t2 = new Thread() {
    
    public void run() {
        for(int i = 0; i < steps; i++) {
            System.out.println("I am 2 at step " + i);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {

            }
        }
    }
};

t1.start();
t2.start();
t1.join();
System.out.println("THREAD 1 FINISHED");
t2.join(); // each execution may lead to a different order, because execution order is non-deterministic
System.out.println("THREAD 2 FINISHED");
I am 2 at step 0
I am 1 at step 0
I am 2 at step 1
I am 1 at step 1
I am 2 at step 2
I am 1 at step 2
I am 2 at step 3
I am 1 at step 3
I am 2 at step 4
I am 2 at step 5
I am 2 at step 6
I am 2 at step 7
I am 2 at step 8
I am 1 at step 4
I am 2 at step 9
I am 1 at step 5
I am 2 at step 10
I am 1 at step 6
I am 2 at step 11
I am 1 at step 7
I am 2 at step 12
I am 1 at step 8
I am 2 at step 13
I am 1 at step 9
I am 2 at step 14
I am 1 at step 10
I am 1 at step 11
I am 1 at step 12
I am 1 at step 13
I am 1 at step 14
THREAD 1 FINISHED
THREAD 2 FINISHED
Out[6]:
null

Syncronization, Visibility, Java's Memory Model, and Locks

All threads in Java have access to all data structures subject to Java's visibility rules, e.g., private fields cannot be accessed from outside the class declaring the field. This allows for information to be passed between threads. However, since the interleaving of the operations from multiple threads is non-deterministic, this can lead to concurrency bugs. To understand what guarantees Java gives for concurrent access to data structures we will first briefly review Java's memory model and then discuss primitives which allow for safe concurrent access to data.

Java's memory model

In Java, conceptually each thread has a cache which can store copies of data structures. Unless if explicity one of the synchronization constructs introduced below are used, there is almost no guarantee when a thread will access cached data or data from memory. That is, even though two threads access the same data, they both may access local cached copies and never see each others changes. Even worse, the JVM is allowed to reorder operations to optimize performance as long as from the view of a single thread its own execution follows the order of statements as written in the code it is executing.

Locks, Synchronization, and Volatile

In the light of these weak guarantees, there is a need for language constructs to control cross-thread access to data. In Java, every object has a lock associated with it. Locks are used in so-called synchronized blocks of code to block multiple threads from concurrently running such syncronized blocks as long as they are guarded by the same object. Also synchronization forces a thread entering a synchronized section to read the current values of the fields of the object on which we are synchronizing from memory. Similarly, when exiting a block synchronized on an object o, all changes to o's fields made by the thread are written back to memory. That is in addition to limiting access to an object to one thread at a time, synchonization also makes changes of other threads to the fields of the object visible to the thread entering the sycronized block as long as these changes where made in a block of code synchronized on the same object.

synchronized (object) {
...
}

A synchronized method in Java is just a shortcut for a method whose whole body is sychronized on the object on which the method is called. For example, the following two are equivalent:

public void synchronized myMethod() {
    CODE
}

public void myMethod() {
    synchronized (this) {
        CODE
    }
}

In addition to synchronized, Java also support volatile fields. Any read or write to a volatile field causes the thread performing the operation to sync the state of the volatile field from/to memory. However, volatile does not guarantee any synchronization otherwise and does not provide the same guarantees for fields of an object pointed to from a volatile variable. The lack of synchronization makes a, possibly suprising, difference for operations that are not atomic. For instance, if a is a volatile int then a++ can lead to suprising results, because this operation is considered as two atomic operations: read the current value of a and write a new (one larger) value back to a. A concurrent thread may potentially read or write a between these two operations.

In [13]:
Object lock = new Object(); // we are using this object as a lock to synchronize on

final int steps = 10;

Thread t1 = new Thread() {
    public void run() {
        synchronized (lock) {
            for(int i = 0; i < steps; i++) {
                System.out.println("I am 1 at step " + i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {

                }
            }
        }
    }   
    
};

Thread t2 = new Thread() {
    
    public void run() {
        synchronized (lock) {
            for(int i = 0; i < steps; i++) {
                System.out.println("I am 2 at step " + i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {

                }
            }
        }
    }
};

t1.start();
t2.start();
t1.join();
t2.join(); // either all outputs from 1 are printed first or all outputs from 2 are printed first since one of the two threads takes the lock on `lock` first, blocking the other one from making progress
I am 1 at step 0
I am 1 at step 1
I am 1 at step 2
I am 1 at step 3
I am 1 at step 4
I am 1 at step 5
I am 1 at step 6
I am 1 at step 7
I am 1 at step 8
I am 1 at step 9
I am 2 at step 0
I am 2 at step 1
I am 2 at step 2
I am 2 at step 3
I am 2 at step 4
I am 2 at step 5
I am 2 at step 6
I am 2 at step 7
I am 2 at step 8
I am 2 at step 9
Out[13]:
null
In [22]:
package lecture;

public class AtomicCounter {   // a counter that synchronizes all access (get, set, increment) 
    
    private int val = 0;
    
    public synchronized int get() {
        return val;
    }
    
    public synchronized void set(int newVal) {
        this.val = val;
    }
    
    public synchronized void increment() {
        this.val++;
    }
}
Out[22]:
lecture.AtomicCounter
In [29]:
import lecture.AtomicCounter;

final AtomicCounter progressOne = new AtomicCounter(); // we are using this integer to keep track of how far t1 has progressed and prevening t2 from "overtaking" t1

final int steps = 15;

Thread t1 = new Thread() {
        
    public void run() {
        for(int i = 0; i < 10; i++) {
            synchronized (progressOne) {
                System.out.println("I am 1 at step " + i);
                System.out.flush();
                progressOne.increment();
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {

            }            
        }
    }   
    
};

Thread t2 = new Thread() {
    
    public void run() {
        for(int i = 0; i < 10; i++) {
            while(progressOne.get() < i) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {

                    }
            }
            synchronized (progressOne) {
                System.out.println("I am 2 at step " + i);
                System.out.flush();
            }
        }
    }
};

t1.start();
t2.start();
t1.join();
t2.join(); // exact order is still non-deterministic, but thread 2 will not progress further than thread 1
I am 1 at step 0
I am 2 at step 0
I am 2 at step 1
I am 1 at step 1
I am 2 at step 2
I am 1 at step 2
I am 2 at step 3
I am 1 at step 3
I am 2 at step 4
I am 1 at step 4
I am 2 at step 5
I am 1 at step 5
I am 2 at step 6
I am 1 at step 6
I am 2 at step 7
I am 1 at step 7
I am 2 at step 8
I am 1 at step 8
I am 2 at step 9
I am 1 at step 9
Out[29]:
null