Lecture 24 Concurrency 2 (D&D 23) Date
Goals By the end of this lesson, you should: Understand the implications of producer-consumer relationships Be able to use wait() and notifyAll() in order to synchronize threads across methods.
synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary The synchronized keyword lets us ensure that only one method at a time can enter the protected block of code, e.g., a method. This does not protect shared data from inconsistency. Consider a shared instance variable being accessed by two different synchronized methods. Even if each method can only be executed by one thread at a time, this still allows one thread to execute the first method and the other to execute the second method, at the same time. This means that the shared instance variable can be accessed by both threads, with potentially unwanted results.
synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary Often, this situation arises in the context of buffers. For example, a FIFO buffer (first-in-first-out) has data added to its end and removed from the front. FIFO buffers are also known as (simple) queues and are usually implemented as a linked list (-> COMPSCI105/107) FIFO buffers are often used with threaded applications: One thread adds data to the end of the queue, one thread reads the data from the front of the queue. Example: In an audio player, one thread reads audio data from disk. This thread typically blocks while it reads data from the disk and then writes a lot of data to the FIFO in big chunks at irregular times. The other thread reads data at regular intervals in small quantities off the front of the queue for immediate replay. Data in buffer Data added here Data read from here
synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary Data in buffer Data added here Data read from here Two problems can occur here: The reading thread may consume data faster than the writing thread is able to produce data, causing the buffer to run empty. The writing thread may produce data faster than the reading thread can consume it, causing the writing thread to overfill the buffer beyond capacity.
Library example synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary This is a simplified example of a producer-consumer relationship. Consider a library that owns a certain number of books. Each library patron may borrow a certain number of books. The patron keeps the books for a while and then returns them. The library cannot loan out more books than it owns. In other words: In real life, a patron can’t come and borrow more books than the library currently has on its shelves.
Library example synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary In a program: The number of books currently in the library is an int. Each patron is a thread. The library is an object shared by all patron threads. The library has methods for borrowing and returning books. These methods are called by the patrons, passing the number of books they wish to borrow or return. These methods are synchronized, meaning that only one patron thread at a time can borrow books, or return books. Note: Many, even all patrons, can hold borrowed books at any given time. They just can’t take them out simultaneously, or return them simultaneously. Each patron chooses the number of books to take out (a number smaller than the number of books that the library owns). The library patrons act as both consumers (they take books out) and producers (they bring books back) of data (books) in the buffer (the library).
The Library interface + class synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary // Common interface for the thread-safe and non-thread-safe // library classes public interface Library { public void borrowBooks(String borrower, int numberOfBooks); public void returnBooks(String borrower, int numberOfBooks); } Implementation (for now): public class NonThreadSafeLibrary implements Library { private int books = 40; public synchronized void borrowBooks(String patron, int numberToBorrow) { books -= numberToBorrow; System.out.println(patron + " borrowed " + numberToBorrow + " books"); System.out.println("Library has " + books); } public synchronized void returnBooks(String patron, int numberToReturn) { books += numberToReturn; System.out.println(patron + " returned " + numberToReturn + " books");
LibraryPatron class synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary import java.security.SecureRandom; public class LibraryPatron implements Runnable { private static final SecureRandom randomGenerator = new SecureRandom(); public String name; public Library library; public LibraryPatron(String name, Library library) { this.name = name; this.library = library; } public void run() { for (int i=0; i<20; i++) { // Borrow some books (up to 14) int numberOfBooks = randomGenerator.nextInt(15); library.borrowBooks(name, numberOfBooks); // Hang onto them for a while try { Thread.sleep(randomGenerator.nextInt(20)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Return them library.returnBooks(name, numberOfBooks);
LibraryTest main() synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary public static void main(String[] args) { Library library = new NonThreadSafeLibrary(); LibraryPatron alice = new LibraryPatron("Alice", library); LibraryPatron bob = new LibraryPatron("Bob", library); LibraryPatron charlie = new LibraryPatron("Charlie", library); LibraryPatron donna = new LibraryPatron("Donna", library); LibraryPatron eve = new LibraryPatron("Eve", library); LibraryPatron fred = new LibraryPatron("Fred", library); ExecutorService es = Executors.newCachedThreadPool(); es.execute(alice); es.execute(bob); es.execute(charlie); es.execute(donna); es.execute(eve); es.execute(fred); es.shutdown(); }
Library test results synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary … Donna returned 5 books Library has 12 Donna borrowed 10 books Library has 2 Bob returned 6 books Library has 8 Bob borrowed 11 books Library has -3 Charlie returned 5 books Charlie borrowed 8 books Library has -6 Donna returned 10 books Library has 4 …
What can we do? synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary Normally: The library would ask a patron wait until books become available. The library would notify waiting patrons that books have become available. We can do the same here. Note: A patron would be told to wait() when they try and borrow books. Since only one patron can enter the borrowBooks() method at any time, only that patron can be told to wait. All other patrons are blocked until that patron has vacated borrowBooks(). The library could either notify() that patron specifically, or notifyAll() waiting patrons – in this case, it makes little difference because the only patron that does actually wait() is the one that executes borrowBooks().
The ThreadSafeLibrary class synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary public class ThreadSafeLibrary implements Library { private int books = 40; public synchronized void borrowBooks(String patron, int numberToBorrow) { // While the library is short of books, let the thread wait while (numberToBorrow > books) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); books -= numberToBorrow; System.out.println(patron + " borrowed " + numberToBorrow + " books"); System.out.println("Library has " + books); public synchronized void returnBooks(String patron, int numberToReturn) { books += numberToReturn; System.out.println(patron + " returned " + numberToReturn + " books"); // Let all waiting threads know that we have had books come back in notifyAll(); Don’t forget to change the class in LibraryTest!
Is it really threadsafe? synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary Running our example with the “thread-safe” class will generally not cause problems. But: Consider the following two lines: Each of these involve reading the value of books, subtracting or adding the value of numberToBorrow or numberToReturn, and then saving the result back to books. This will generally happen very, very quickly. Which guarantee do we have that these operations are atomic, i.e., cannot be completed by two different threads at the same time? None. Could both threads read the number of books, with one thread then changing it upwards without the other knowing it? Unlikely, but yes. Have a look at the ReallyThreadSafeLibrary example for a solution. books -= numberToBorrow; books += numberToReturn;
Adding complexity synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary Consider adding an AcquisitionsLibrarian who occasionally adds books to the library, and a shelf capacity that the library cannot exceed. You could also add a Librarian tasked with throwing out old books on a regular basis. Challenge: Implement these classes. Will the library class remain thread-safe under these circumstances? E.g., what would happen if you had a separate method to dispose of books? What might happen if Librarian tried to dispose of books via this method, and a borrower thread took some out at the same time?
Notes synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary Terminology: A situation as in our incrementor example where threads use the value of a shared variable that may since have been updated by another thread is known as a dirty read. If several threads write to a variable an unsynchronized fashion, as in our incrementor and library examples, we have a race condition – the threads are in a “race” to write to the variable (and the thread scheduling, which we can’t predict, determines who “wins”) It’s possible for two threads to end up waiting such that neither cannot return to runnable state without notification from the other. This is called a deadlock. Topics we haven’t dealt with here but that are worth exploring (and which you’re ready to explore) include: timed waiting, explicit acquisition/release of locks, data structures that assist in multi-threading, etc.
What do we know synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary Thread write access to shared memory can lead to unpredictable behaviour of multi-threaded software. The use of synchronized in Java does not offer complete protection. A common scenario in which multiple threads access the same data is when a producer thread writes data to a buffer from which a consumer thread reads. This happens, e.g., in audio or video players where one thread reads the data from disk or network, and the other thread replays. This can result in buffer underruns or buffer overruns. We can tell a Java thread to wait(). This puts the thread into the waiting state. A waiting thread can be interrupted to resume execution. A call to the notify() method of the waiting thread object, or to notifyAll() interrupts the/all waiting thread(s) and causes it/them to return to runnable state. Writing truly thread-safe software is challenging!
Resources & Homework synchronized is not enough Producers and consumers: Library example Is it really threadsafe? Notes Summary https://docs.oracle.com/javase/tutorial/essential/concurrency/ https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html (wait(), notify(), notifyAll()) https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html D & D Chapter 23
Next Exam. Good luck everyone!