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
- Retrieve the current value of c
- Increment the retrieved value by 1
- 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:
- Synchronised methods
- Synchronised statements
synchronised Methods
Making the two methods above synchronised has two effects:
- Two invocations of synchronised methods can't overlap
- 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,796