Threads and Runnables
- Before starting a new thread you have to specify the code to be executed by this thread, often called the task. This is done by implementing
Runnable- a functional interface defining a single void no-args method run().1
2
3
4
5
6
7
8
9
10
11Runnable task = () -> {
String threadName = Thread.currentThread().getName();
System.out.println("Hello " + threadName);
};
task.run();
Thread thread = new Thread(task);
thread.start();
System.out.println("Done!"); - Due to concurrent execution we cannot predict if the runnable will be invoked before or after printing ‘done’. The order is non-deterministic, thus making concurrent programming a complex task in larger applications.
Executors
ExecutorServiceas a higher level replacement for working with threads directly.- Executors are capable of running asynchronous tasks and typically manage a pool of threads, so we don’t have to create new threads manually.
1 | ExecutorService executor = Executors.newSingleThreadExecutor(); |
Callables and Futures
In addition to Runnable executors support another kind of task named Callable. Callables are functional interfaces just like runnables but instead of being void they return a value.
1 | ExecutorService executor = Executors.newFixedThreadPool(1); |
Synchronized
When writing such multi-threaded code you have to pay particular attention when accessing shared mutable variables concurrently from multiple threads. Let’s just say we want to increment an integer which is accessible simultaneously from multiple threads.
We define a field count with a method incrementSync() to increase count by one:1
2
3
4
5int count = 0;
synchronized void incrementSync() {
count = count + 1;
}
When using incrementSync() concurrently we get the desired result count of 10000. No race conditions occur any longer and the result is stable with every execution of the code:1
2
3
4
5
6
7
8ExecutorService executor = Executors.newFixedThreadPool(2);
IntStream.range(0, 10000)
.forEach(i -> executor.submit(this::incrementSync));
stop(executor);
System.out.println(count); // 10000
Internally Java uses a so called monitor also known as monitor lock or intrinsic lock in order to manage synchronization. This monitor is bound to an object, e.g. when using synchronized methods each method share the same monitor of the corresponding object.
All implicit monitors implement the reentrant characteristics. Reentrant means that locks are bound to the current thread. A thread can safely acquire the same lock multiple times without running into deadlocks (e.g. a synchronized method calls another synchronized method on the same object).
AtomicInteger
An operation is atomic when you can safely perform the operation in parallel on multiple threads without using the synchronized keyword or locks.
Internally, the atomic classes make heavy use of compare-and-swap (CAS), an atomic instruction directly supported by most modern CPUs. Those instructions usually are much faster than synchronizing via locks. So my advice is to prefer atomic classes over locks in case you just have to change a single mutable variable concurrently.
1 | AtomicInteger atomicInt = new AtomicInteger(0); |
By using AtomicInteger as a replacement for Integer we’re able to increment the number concurrently in a thread-safe manor without synchronizing the access to the variable. The method incrementAndGet() is an atomic operation so we can safely call this method from multiple threads.
AtomicInteger supports various kinds of atomic operations. The method updateAndGet() accepts a lambda expression in order to perform arbitrary arithmetic operations upon the integer:
1 | AtomicInteger atomicInt = new AtomicInteger(0); |
ConcurrentMap
The interface ConcurrentMap extends the map interface and defines one of the most useful concurrent collection types. Java 8 introduces functional programming by adding new methods to this interface.
Reference
http://winterbe.com/posts/2015/04/07/java8-concurrency-tutorial-thread-executor-examples/