Per anni i threads sono stati il solo modello di concorrenza sulla JVM. Tuttavia usarli correttamente è difficile, e anche per questo altri modelli di concorrenza stanno guadagnando popolarità. Akka ha reso disponibile sulla JVM gli attori originalmente implementati in Erlang. Clojure ha separato una referenza dalla serie di valori che assume nel tempo introducendo il concetto di STM. Infine anche la programmazione funzionale sta giocando un ruolo importante nel semplificare le tecniche di parallelizzazione. Lo scopo del talk è comparare pro e contro di questi diversi modelli di concorrenza.
2. Moore's
law
The number of transistors on integrated
circuits doubles approximately every two years
Now achieved
by increasing
the number
of cores
idle
6. Concurrency & Parallelism
Parallel programming
Running multiple tasks at
the same time
Concurrent programming
Managing concurrent requests
Both are hard!
7. The native Java concurrency model
Based on:
They are sometimes plain evil …
… and sometimes a necessary pain …
… but always the wrong default
Threads
Semaphores
SynchronizationLocks
8. What do you think when I say parallelism?
Threads
And what do you think when I say threads?
Locks
What are they for?
They prevent multiple threads to run in
parallel
Do you see the problem?
9. Summing attendants ages (Threads)
class Blackboard {
int sum = 0;
int read() { return sum; }
void write(int value) { sum = value; }
}
class Attendee implements Runnable {
int age;
Blackboard blackboard;
public void run() {
synchronized(blackboard) {
int oldSum = blackboard.read();
int newSum = oldSum + age;
blackboard.write(newSum);
}
}
}
10. The Java Concurrency Bible
You know you're in big troubles when you feel
the need of taking this from your bookshelf
11. Don't call alien
methods while holding a lock
Threads – Good practices
Acquire multiple locks
in a fixed, global order
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
rwl.writeLock().tryLock(3L, TimeUnit.SECONDS);
Use interruptible locks instead of intrinsic synchronization
Avoid blocking using concurrent
data structures and atomic
variable when possible
Use thread pools instead of creating threads directly
Hold locks for the
shortest possible
amount of time
Learn Java Concurrency API
14. Threads and Locks – Pros & Cons
+ “Close to the metal” → can be very efficient when
implemented correctly
+ Low abstraction level → widest range of applicability and
high
degree of control
+ Threads and lock can be implemented in existing imperative
object-oriented languages with little effort
- Low abstraction level → threads-and-locks programming is
HARD and understanding the Java Memory Model even HARDER
- Inter-threads communication done with shared mutable state
leads to non-determinism
- Threads are a scarce resource
- It works only with shared-memory architectures → no support
for distributed memory → cannot be used to solve problems
that are too large to fit on a single system
15. Do not try and fix the deadlock, that's
impossible. Instead, only try and realize
the truth.... there is no deadlock.
Then you will see it is not the deadlock
16. Threads & Locks
Concurrent programming
Assembler
Programming
=
Patient: "Doctor, it hurts when I do
this.”Doctor: "Then stop doing it."
17. What about Queues instead of Locks?
In reality actors are just a more structured and powerful way of
using queues. More on this later …
+ Better decoupling
+ Message passing
instead of shared memory
- High wake-up latency
- No built-in failure recovery
- Heavyweight
- Unidirectional
18. The cause of the problem …
Mutable state +
Parallel processing =
Non-determinism
Functional
Programming
20. OOP makes code
understandable by
encapsulating moving parts
FP makes code understandable
by minimizing moving parts
- Michael Feathers
OOP vs FP
21. Mutability
Parameter binding is about assigning names to things
Mutating variables is about assigning things to names
Does that second
one sound weird?
… well it's because
it IS weird
22. Dangers of mutable state (1)
public class DateParser {
private final DateFormat format =
new SimpleDateFormat("yyyy-MM-dd");
public Date parse(String s) throws ParseException {
return format.parse(s);
}
}
Hidden Mutable State
final == thread-safe ?
23. Dangers of mutable state (2)
public class Conference {
private final List<Attendee> attendees = new LinkedList<>();
public synchronized void addAttendee(Attendee a) {
attendees.add(a);
}
public synchronized Iterator<Attendee> getAttendeeIterator() {
return attendees.iterator();
}
}
Escaped Mutable State
synchronized == thread-safe ?
24. Summing attendants ages (Functional)
class Blackboard {
final int sum;
Blackboard(int sum) { this.sum = sum; }
}
class Attendee {
int age;
Attendee next;
public Blackboard addMyAge(Blackboard blackboard) {
final Blackboard b = new Blackboard(blackboard.sum + age);
return next == null ? b : next.addMyAge(b);
}
}
25. FP + Internal iteration = free parallelism
public int sumAges(List<Attendee> attendees) {
int total = 0;
for (Attendee a : attendees) {
total += a.getAge();
}
return total;
}
public int sumAges(List<Attendee> attendees) {
return attendees.stream()
.map(Attendee::getAge)
.reduce(0, Integer::sum);
}
External iteration
Internal iteration
26. FP + Internal iteration = free parallelism
public int sumAges(List<Attendee> attendees) {
int total = 0;
for (Attendee a : attendees) {
total += a.getAge();
}
return total;
}
public int sumAges(List<Attendee> attendees) {
return attendees.stream()
.map(Attendee::getAge)
.reduce(0, Integer::sum);
}
External iteration
Internal iteration
public int sumAges(List<Attendee> attendees) {
return attendees.parallelStream()
.map(Attendee::getAge)
.reduce(0, Integer::sum);
}
The best way to write parallel
applications is NOT to have
to think about parallelism
27. Parallel reduction – Divide and Conquer
Use Java 7 Fork/Join
framework under the
hood, but expose an
higher abstraction level
28. Using your own ForkJoinPool with
parallel Streams
public int sumAges(List<Attendee> attendees) {
return new ForkJoinPool(2).submit(() ->
attendees.parallelStream()
.map(Attendee::getAge)
.reduce(0, Integer::sum)
).join();
}
CompletableFuture<Integer> sum =
CompletableFuture.supplyAsync(() ->
attendees.parallelStream()
.map(Attendee::getAge)
.reduce(0, Integer::sum),
new ForkJoinPool(2));
}
Don't do this at home!!!
29. public static <T> void sort(List<T> list,
Comparator<? super T> c)
Essence of Functional Programming
Data and behaviors are the same thing!
Data
Behaviors
Collections.sort(persons,
(p1, p2) -> p1.getAge() – p2.getAge())
30. Map/Reduce is a FP pattern
public int sumAges(List<Attendee> attendees) {
return attendees.stream()
.map(Attendee::getAge)
.reduce(0, Integer::sum);
}
Do these methods' names remember you something?
Fast also because, when possible, Map/Reduce moves
computation (functions) to the data and not the opposite.
31. Functions – Pros & Cons
+ Immutability definitively prevents any non-determinism
+ Declarative programming style improves readability
→ focus on the “what” not on the “how”
+ Parallelizing functional (side-effect free) code can be trivially
easy in many cases
+ Better confidence that your program does what you think it
does
+ Great support for distributed computation
- “Functional thinking” can be unfamiliar for many OOP
developers
- Can be be less efficient than its imperative equivalent
- In memory managed environment (like the JVM) put a bigger
burden on the garbage collector
- Less control → how the computational tasks are splitted and
33. Summing attendants ages (Actors)
class Blackboard extends UntypedActors {
int sum = 0;
public void onReceive(Object message) {
if (message instanceof Integer) {
sum += (Integer)message;
}
}
}
class Attendant {
int age;
Blackboard blackboard;
public void sendAge() {
blackboard.tell(age);
}
}
34. The way OOP is
implemented in most
common imperative
languages is probably
one of the biggest
misunderstanding in
the millenarian history
of engineering
This is Class Oriented Programming
Actors are the real OOP (Message Passing)
35. I'm sorry that I coined the term "objects", because it
gets many people to focus on the lesser idea. The big
idea is "messaging".
Alan Kay
37. Throwing an exception in concurrent code will just simply blow
up the thread that currently executes the code that threw it:
1. There is no way to find out what went wrong, apart from
inspecting the stack trace
2. There is nothing you can do to recover from the problem
and bring back your system to a normal functioning
What’s wrong in trying to prevent errors?
Supervised actors
provide a clean error
recovery strategy
encouraging
non-defensive
programming
38. Actors – Pros & Cons
+ State is mutable but encapsulated → concurrency is
implemented with message flow between actors
+ Built-in fault tolerance through supervision
+ Not a scarce resource as threads → can have multiple actors for
each thread
+ Location transparency easily enables distributed programming
+ Actors map real-world domain model- Untyped messages don't play well with Java's lack of pattern
matching
- It's easy to violate state encapsulation → debugging can be hard
- Message immutability is vital but cannot be enforced in Java
- Actors are only useful if they produce side-effects
- Composition can be awkward
41. Software Transactional Memory
An STM turns the Java heap into a transactional data set with
begin/commit/rollback semantics. Very much like a regular
database.
It implements the first three letters in ACID; ACI:
Atomic → all or none of the changes made during a transaction get
applied
Consistent → a transaction has a consistent view of reality
Isolated → changes made by concurrent execution transactions are
not visible to each other
➢ A transaction is automatically retried when it runs into some
read or write conflict
➢ In this case a delay is used to prevent further contentions
➢ There shouldn’t be side-effects inside the transaction to avoid to
repeat them
42. Summing attendants ages (STM)
import org.multiverse.api.references.*;
import static org.multiverse.api.StmUtils.*;
public class Blackboard {
private final TxnRef<Date> lastUpdate;
private final TxnInteger sum = newTxnInteger(0);
public Blackboard() {
this.lastUpdate = newTxnRef<Date>(new Date());
}
public void sumAge(Attendant attendant) {
atomic(new Runnable() {
public void run() {
sum.inc(attendant.getAge());
lastUpdate.set(new Date());
}
});
}
}
43. STM – Pros & Cons
+ Eliminates a wide range of common problems related with
explicit synchronization
+ Optimistic and non-blocking
+ Many developers are already used to think in transactional terms
+ It's possible to compose multiple transactional blocks nesting
them in a higher level transaction
- Write collision are proportional to threads contention level
- Retries can be costly
- Unpredictable performance
- Transactional code has to be idempotent, no side-effects are
allowed
- No support for distribution
44. (Completable)Future
The Future interface was introduced in Java 5 to model an asynch
computation and then provide an handle to a result that will be
made available at some point in the future.
CompletableFuture introduced in Java 8 added
fluent composability, callbacks and more.
CompletableFuture
.supplyAsync(() -> shop.getPrice(product))
.thenCombine(CompletableFuture.supplyAsync(
() -> exchange.getRate(Money.EUR,
Money.USD)),
(price, rate) -> price * rate)
.thenAccept(System.out::println);
+ Non-blocking composition
+ Freely sharable
+ Can recover from failure
- Callback Hell
- Debugging can be hard
- Closing over mutable state
45. Reactive Programming
+ Non-blocking composition
with richer semantic
+ Event centric → async in
nature
- Callback Hell
- Push instead of pull →
Inverted control flow
- Fast producer/slow consumer
Reactive programming consists in asynch processing and
combining streams of events ordered in time.
RxJava is a library, including a DSL, for composing asynch and
event-based programs using observable sequences
Observable<Stock> stockFeed =
Observable.interval(1, TimeUnit.SECONDS)
.map(i -> StockServer.fetch(symbol));
stockFeed.subscribe(System.out::println);
46. Wrap up – There's No Silver Bullet
One
size
does
NOT
fit
➢ Concurrency and parallelism
will be increasingly
important in the near future
➢ Using threads & locks by
default is (at best)
premature optimization
➢ There are many different
concurrency models with
different characteristic
➢ Know them all and choose
47. Wrap up – The Zen of Concurrency
Avoid shared mutability → if there is no clear
way to avoid mutability, then favor isolated
mutability
Sequential programming idioms (e.g. external
iteration) and tricks (e.g. reusing variables) are
detrimental for parallelization
Prototype different solutions with different
concurrency models and discover their strengths
and weaknesses
Premature optimization is evil especially in
concurrent programming → Make it right first
and only AFTER make it faster
Strive for immutability → Make fields and local variables final by
default and make an exception only when strictly required