Title Text

Title Text

Title Text

Title Text

Title Text

Introduction to programming

Concurrency

Title Text

Concurrent programming

Concurrency

We expect our computers to be able to do more than one thing at a time.

 

Even a single application is often expected to do multiple things at a time.

Software that works like this is known as concurrent software.

A TRIP DOWN MEMORY LANE

In the beginning, computers didn't have Operating Systems.

This meant programs were executed one at a time, from beginning to end.

 

The birth of the OS allowed machines to execute more than one program in what we know as processes. Each process was run isolated from the others, getting its own share of computer resources.

 

Processes would, if needed, communicate with each other through IPC mechanisms.

Processes and threads

In concurrent programming, there are two basic units of execution: processes and threads.

A computer system has many active processes and threads.

 

If a computer has a single core, processing time will be shared among processes and threads through time slicing.

Nowadays, computers have either multiple processors, or a processor with multiple cores.

Processes

Process isn't a synonym to application or program.

 

A process is a program loaded in RAM and being executed by the CPU. Processes are isolated and don't share memory among themselves. However, two processes can communicate.

A process can contain multiple threads.

Thread

Threads exist within a process; every process has at least one.

 

Threads share the process's resources, which makes for efficient, yet potentially problematic, communication, but have their own stack and local variables.

Threads in java

Thread object

Threads are the easiest way to take advantage of the full computer power of multi-processor/multi-core systems.

 

Threads are inescapable. They are part of the Java language, even if we don't explicitly create them in our code.

 

Each thread is associated with an instance of the class Thread.

Thread Creation

An application that creates an instance of Thread must provide the code that will be run in that thread. There are two ways of doing this:

// METHOD 1: PROVIDING A RUNNABLE OBJECT

public class MyRunnable implements Runnable {
    
    @Override
    public void run() {
        System.out.println("I'm running!");
    }
}
// METHOD 2: EXTENDING THREAD

public class MyThread extends Thread {
	
    @Override
    public void run() {
        System.out.println("A running piece of code!");
    }
}
public class Main{
    
    public static void main(String args[]) {
        
        // THE RUNNABLE METHOD
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        
        // THE THREAD EXTENSION METHOD
        MyThread t2 = new Thread();
        t2.start();
    }
}

Thread Creation

Which method should we use?

 

Although extending Thread seems easier to implement, creating a Runnable object is more flexible, because that object can subclass a class other than Thread.

public class MyRunnable extends SomeClass implements Runnable {
    
    @Override
    public void run() {
        System.out.println("I'm running!");
    }
}

Live coding

Thread.sleep()

JOINING THREADS

Sometimes we will need that one thread waits for the completion of another. Using Thread.sleep() isn't a great solution because there's no way of knowing how much time we need to wait.

 

A better solution would be to use the join() method.

 

When the method join() is called for thread1 within another thread, that thread will wait until thread1 ends.

Live coding

thread.join()

Thread Synchronisation

As we've seen before, threads share their process's resources. This makes communication between threads very  efficient, but might create two types of problems: thread interference and memory consistency errors.

 

The solution mostly lies in the synchronisation of threads, which controls the accesss of several threads to the shared resources.

Thread interference

Interference happens when two operations, running in different threads, but acting on the same data, overlap.

 

Thread interference bugs can be difficult to detect and fix, because they are unpredictable.

 

Thread interference problems are often related to lack of atomicity.

ATOMICITY

An atomic action is one that effectively happens all at once. It either happens completely or it doesn't happen at all.

public class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        --c;
    }

    public int value() {
        return c;
    }
}

Is the operation          atomic?

c++

ATOMICITY

The answer is no.

There are 3 steps involved in an increment operation

 

  1. Retrieve the current value of c
  2. Increment the retrieved value by 1
  3. Store the incremented value back in c

 

The reading and writing operations are atomic by themselves, but the increment (and decrement) operation as a whole, isn't.

Memory consistency errors

Memory consistency errors occur when different threads have inconsistent views of what should be the same data.

 

Memory consistency errors occur majorly due to visibility or reordering problems.

Memory consistency errors -

Visibility

When using multiple threads, it is possible that changes to shared data performed by one thread are not visible by other threads (at the moment of reading).

This is due to the way the shared data is read/written:

Before the changes to a variable are saved to the main memory, other threads might read the now outdated value on it.

Memory consistency errors - Reordering

The Java compiler can change the order of execution if it believes the outcome won't change. When dealing with multithreading, this could be a problem.

// THREAD 1 EXECUTING:
int operationResult = executeVeryVeryComplexComputation();
boolean done = true;

// THREAD 2 
if (done) {
    System.out.println(operationResult);
}

Live coding

Memory Consistency Problems - Visibility

Solving concurrency issues

Thread-Safety

Code that executes and behaves properly when accessed simultaneously by different threads is called thread-safe.

Solving concurrency issues

The naive way of dealing with concurrency issues is to have our objects be immutable. But that isn't very practical, is it?

final class Counter {

    final int count;

    public Counter(int value) {
        this.count = value;
    }

    public int getCount() {
        return count;
    }

    public Counter increment() {
        return new Counter(count + 1);
    }
}

Solving concurrency issues

Visibility problems (caching) and atomic primitive memory reads/writes can be solved using the volatile keyword.

public class Example {

    // VOLATILE VALUES WILL NEVER BE CACHED
    private volatile int num = 0;
    private volatile long num2 = 10;
    
    (...)
}

Non-atomic operations and code reordering can be solved using synchronisation and Locks.

synchronised

The Java programming language provides two ways of synchronising blocks of code: 

  1. Synchronised methods
  2. Synchronised statements

synchronised Methods

Making the two methods above synchronised has two effects:

  1. Two invocations of synchronised methods can't overlap
  2. It guarantees that changes to the state of the object are visible to all threads.
public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

Intrinsic locks

Synchronisation is built around an internal entity known as the intrinsic lock.

Every object has an intrinsic lock associated with it.

When a thread invokes a synchronised method, it automatically acquires the intrinsic lock for that method's object, and releases it when the method returns.

Synchronised statements

Unlike synchronised methods, synchronised statements must specify the object that provides the intrinsic lock.

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

Threads acquire the lock when entering the synchronised block, and release it when exiting.

 

Only one thread can hold the lock at a time. The thread that attempts to acquire a lock that is already taken, will block.

Live coding

Synchronized

Live coding

Deadlock

Live coding

Guarded Blocks

EXERCISE

The Producer-Consumer Problem

Title Text

Synchronized collections

Synchronized Collections

When working in multithreaded environments, it can be scary, and error-prone, to use plain collections.

 

Hence, the Java platform provides different synchronised collection objects like Vector and HashTable, as well as the Collections.synchronizedCollection() method.

 

State is encapsulated and all public methods are synchronised. This makes all operations on these classes atomic at the cost of poor concurrency, which can affect performance

Concurrent collections

Designed for concurrent access from multiple threads.

The substitution of synchronised collections with concurrent collections can offer good scalability improvements.

Some examples of concurrent collections:

Title Text

THREAD POOLS AND EXECUTORS

The Executor Framework

The java.util.concurrent package provides a flexible thread pool implementation– The Executor Framework.

 

A thread pool is a managed collection of threads that are available to perform tasks.

The Executor Interface

Executor is an object that executes submitted Runnable tasks.

 

This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc.

public interface Executor {

    /**
     * Executes the given command at some time in the future.  
     * The command may execute in a new thread, 
     * in a pooled thread, or in the calling
     * thread, at the discretion of the implementation.
     */
    void execute(Runnable command);
}

Executors importance

Reusing an existing thread reduces the costs of creating a new one for each request; task execution will not be delayed, improving responsiveness.

 

It gives you access to other actions such as tuning, management, monitoring, logging, or error reporting.

EXECUTOR SERVICE

The ExecutorService that provides multiple methods to manage the lifecycle of the Executor and submission of tasks.

 

An ExecutorService can be shut down, which will cause it to reject new tasks

 

Executors is a class that provides factory and utility methods for ExecutorService. 

FIXED THREAD POOL

Reuses a fixed number of threads.

At any point, at most n threads will be active. If additional tasks are submitted when all threads are active, they will wait in the queue until a thread is available

// CREATE A POOL OF 5 FIXED THREADS
ExecutorService fixedPool = Executors.newFixedThreadPool(5);

// SUBMIT 6 TASKS TO BE EXECUTED
for(int i = 0; i < 6; i++){
    fixedPool.submit(() -> doSomeAction());
}

// SHUT DOWN THE EXECUTOR
fixedPool.shutdown();

CACHED THREAD POOL

Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available. These pools will typically improve the performance of programs that execute many short-lived tasks. 

// CREATE THE POOL
ExecutorService cachedPool = Executors.newCachedThreadPool();

// SUBMIT 20 TASKS TO BE EXECUTED
for(int i = 0; i < 6; i++){
     cachedPool.submit(() -> doSomeAction());
}

// SHUT DOWN THE THEAD POOL EVEN IF THE SUBMITTED TASKS HAVEN'T FINISHED EXECUTING
cachedPool.shutdownNow();

SINGLE THREAD EXECUTOR

Creates an Executor with a single working thread that processes tasks, replacing it if it dies unexpectedly. Tasks are guaranteed to be processed sequentially

// CREATE A SINGLE WORKING THREAD
ExecutorService singleExecutor = Executors.newSingleThreadExecutor();

// SUBMIT 6 THREADS TO BE EXECUTED SEQUENTIALLY
for(int i = 0; i < 6; i++){
    singleExecutor.submit(() -> doSomeAction());
}

EXERCISE

The Concurrent Chat Server

Concurrency

By Soraia Veríssimo

Concurrency

  • 1,039