by Mario Fusco mario.fusco@gmail.com @mariofusco Comparing different concurrency models on the JVM
Moore's law The number of transistors on integrated circuits doubles approximately every two years Now achieved by increasing the number of cores idle
This is what typically happens in your computer
Concurrency & Parallelism Parallel programming Running multiple tasks at the same time Concurrent programming Managing concurrent requests Both are hard!
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
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?
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); } } }
The Java Concurrency Bible You know you're in big troubles when you feel the need of taking this from your bookshelf
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
How you designed it
What happens in reality
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 - Writing multithreaded programs is difficult but testing them is nearly impossible → a maintenance nightmare
Threads & Locks Concurrent programming Assembler Programming = Patient: "Doctor, it hurts when I do this.” Doctor: "Then stop doing it."
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 that needs fixing, it is only yourself.
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
The cause of the problem … Mutable state + Parallel processing = Non-determinism Functional Programming
Functional Programming
OOP makes code understandable by encapsulating moving parts FP makes code understandable by minimizing moving parts - Michael Feathers OOP vs FP
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
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 ?
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 ?
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); } }
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
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
Parallel reduction – Divide and Conquer Use Java 7 Fork/Join framework under the hood, but expose an higher abstraction level
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!!!
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())
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.
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 scheduled on threads is delegated to the library/framework - Great abstraction for parallelism not for concurrency
Actors
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); } }
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)
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
Defensive programming Vs. Let it crash!
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
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 - Actors do not prevent deadlocks → it’s possible for two or more actors to wait on one another for messages
The state quadrants Mutable Immutable Shared Unshared Actors Functional Programming Threads Determinism Non-determinism
Software Transactional Memory
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
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()); } }); } }
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
(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
Reactive Programming + Non-blocking composition with richer semantic + Event centric → async in nature + Can recover from failure - Callback Hell - Push instead of pull → Inverted control flow - Fast producer/slow consumer → May require blocking 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);
Wrap up – There's No Silver Bullet One size does NOT fit all ➢ 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 the one that best fit the problem at hand
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 Poly-paradigm programming is more effective than polyglot → you can experiment all those different concurrency models in plain Java Strive for immutability → Make fields and local variables final by default and make an exception only when strictly required
Suggested readings
Mario Fusco Red Hat – Senior Software Engineer mario.fusco@gmail.com twitter: @mariofusco Q A Thanks … Questions?

Comparing different concurrency models on the JVM

  • 1.
    by Mario Fusco mario.fusco@gmail.com @mariofusco Comparingdifferent concurrency models on the JVM
  • 2.
    Moore's law The number oftransistors on integrated circuits doubles approximately every two years Now achieved by increasing the number of cores idle
  • 5.
  • 6.
    Concurrency & Parallelism Parallelprogramming Running multiple tasks at the same time Concurrent programming Managing concurrent requests Both are hard!
  • 7.
    The native Javaconcurrency model Based on: They are sometimes plain evil … … and sometimes a necessary pain … … but always the wrong default Threads Semaphores SynchronizationLocks
  • 8.
    What do youthink 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 ConcurrencyBible You know you're in big troubles when you feel the need of taking this from your bookshelf
  • 11.
    Don't call alien methodswhile 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
  • 12.
  • 13.
  • 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 - Writing multithreaded programs is difficult but testing them is nearly impossible → a maintenance nightmare
  • 15.
    Threads & Locks Concurrentprogramming Assembler Programming = Patient: "Doctor, it hurts when I do this.” Doctor: "Then stop doing it."
  • 16.
    Do not tryand 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 that needs fixing, it is only yourself.
  • 17.
    What about Queuesinstead 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 ofthe problem … Mutable state + Parallel processing = Non-determinism Functional Programming
  • 19.
  • 20.
    OOP makes codeunderstandable by encapsulating moving parts FP makes code understandable by minimizing moving parts - Michael Feathers OOP vs FP
  • 21.
    Mutability Parameter binding isabout 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 mutablestate (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 mutablestate (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 + Internaliteration = 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 + Internaliteration = 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 ownForkJoinPool 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 aFP 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 scheduled on threads is delegated to the library/framework - Great abstraction for parallelism not for concurrency
  • 32.
  • 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 OOPis 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 thatI coined the term "objects", because it gets many people to focus on the lesser idea. The big idea is "messaging". Alan Kay
  • 36.
  • 37.
    Throwing an exceptionin 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 - Actors do not prevent deadlocks → it’s possible for two or more actors to wait on one another for messages
  • 39.
  • 40.
  • 41.
    Software Transactional Memory AnSTM 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 interfacewas 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-blockingcomposition with richer semantic + Event centric → async in nature + Can recover from failure - Callback Hell - Push instead of pull → Inverted control flow - Fast producer/slow consumer → May require blocking 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 all ➢ 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 the one that best fit the problem at hand
  • 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 Poly-paradigm programming is more effective than polyglot → you can experiment all those different concurrency models in plain Java Strive for immutability → Make fields and local variables final by default and make an exception only when strictly required
  • 48.
  • 49.
    Mario Fusco Red Hat– Senior Software Engineer mario.fusco@gmail.com twitter: @mariofusco Q A Thanks … Questions?