Week 7 Concurrent Programming: Thread Synchronization CS 180 Sunil Prabhakar Department of Computer Science Purdue University
Announcements Exam 1 tonight 6:30 pm - 7:30 pm MTHW 210 2
Outcomes Understand race conditions Critical sections and mutual exclusion Synchronization mechanism synchronized methods, block of code wait() and notify() Understand potential problems busy waiting deadlock livelock 3
Thread Scheduling Remember Once a thread is runnable it can be scheduled at any time It is hard to predict the actual ordering With multiple threads, we must ensure that any possible interleaving of threads would still be a correct execution 4
Race Conditions If the result of a computation changes depending upon the actual ordering of threads we call it a race condition. Can occur even with very simple concurrent programs. Consider a program that simply increments a single variable, count 100000 times the task is divided among multiple threads 5
public class RaceCondition extends Thread { private static int counter = 0; public static final int THREADS = 4; public static final int COUNT = 10000000; Race Example public static void main(string[] args){ RaceCondition[] threads = new RaceCondition[THREADS]; for(int i=0;i<threads;i++) { threads[i] = new RaceCondition(); threads[i].start(); try{ for(int i=0;i<threads;i++) threads[i].join(); catch (InterruptedException e) { e.printstacktrace(); System.out.println( Counter: + counter); public void run(){ for(int i=0;i<count/threads;i++) counter++; 6
Race Without multiple threads, always correct. With multiple threads, often incorrect! Why? multiple threads modifying the same value counter++ is not atomic Atomic operations A java statement translates to multiple machine level statements the thread can be swapped in the middle of these machine level statements 7
The memory hierarchy Speed Cost Size ALU Registers Cache CPU Cache (MB) Main Memory (GB) 8
Multi Core possibility ALU Core ALU Core Registers Registers Cache CPU Cache Cache (MB) Main Memory (GB) 9
Multi Core possibility 2 ALU Core ALU Core Registers Registers CPU Cache (MB) Main Memory (GB) 10
Multi CPU possibility ALU CPU ALU Registers ALU Registers Registers Cache Cache Cache Cache (MB) Cache (MB) CPU Program Main Memory (GB) Data 11
Atomic operations and Caching Data is operated on in ALUs register = count; register++; count = register; Thus if a thread is swapped out just before the last statement, and another thread swapped in, then we have a lost update The memory hierarchy caches data values. If multiple threads are using some data, it gets copied into multiple caches this can cause different threads to not see all changes made by other threads! 12
Shared mutable state How do we ensure correct behavior? Many problems of synchronization can be traced to data that is concurrently read and written by multiple threads shared mutable state if no thread writes -- no problems For example, the variable count is shared mutable state. Only one thread should modify at a given time, and no thread should read the value during this time. 13
Critical Section A section of code that should not be accessed by multiple threads concurrently We need to guarantee mutual exclusion in these sections Java solution: use a lock that only one thread holds at a time also called a monitor any object can be locked 14
Synchronized The synchronized keyword can be used to declare a method or block of code as a critical section i.e., only one thread can be executing any statements in that method or block of code. public synchronized int methoda(){...//critical section public int methoda(){... synchronized (someobject) {... //Critical section 15
Method vs. block of code When synchronized is used with a block of code, we explicitly specify what object should be used for locking; can arbitrarily limit the critical section can have independent locks for different code. A synchronized method implicitly locks the object (this) on which the method is called the class, in the case of static methods 16
(No) Race Condition public class NoRaceCondition extends Thread { private static int counter = 0; public static final int THREADS = 4; public static Object lock = new Object(); public static final int COUNT = 1000000; public static void main(string[] args){ NoRaceCondition[] threads = new NoRaceCondition[THREADS]; for(int i=0;i<threads;i++) { threads[i] = new NoRaceCondition(); threads[i].start(); Note: lock is static. try{ for(int i=0;i<threads;i++) threads[i].join(); catch (InterruptedException e) { e.printstacktrace(); System.out.println( Counter: + counter); public void run(){ for(int i=0;i<count/threads;i++){ synchronized(noracecondition.lock){ counter++; Note: not entire method or for loop! 17
Synchronization Any code that is synchronized can only be executed by a thread that holds the appropriate lock Only one thread can hold the lock for a given object Any changes made in a critical section will be visible to all threads once they get the lock (ensured by Java) 18
Announcements No class on Monday No labs next week 19
Producer-Consumer A very common scenario a producer process creates data a consumer process processes data (and deletes it) e.g., networking, unix pipes, device drivers A shared data structure (buffer) is used to hold data before processing limited size if empty -- consumer waits if full -- produce waits 20
Producer-Consumer import java.util.*; public class ProducerConsumer extends Thread{ private static Buffer buffer = new Buffer(); private boolean isconsumer; public ProducerConsumer(boolean isc){isconsumer=isc; public static void main ( String[] args ) { ProducerConsumer consumer = new ProducerConsumer(true); ProducerConsumer producer = new ProducerConsumer(false); consumer.start(); producer.start(); try{ consumer.join(); producer.join(); catch (InterruptedException e) { e.printstacktrace(); // run method 21
public void run(){ String s; for(int i=0;i<1000;i++){ try{ if(isconsumer){ s = (String) buffer.removeitem(); System.out.println( Consumed: + s); else { s = Item + i; buffer.additem(s); System.out.println( \t\tproduced: + s); sleep(math.ceil(math.random()*10)) ; catch (InterruptedException e) { e.printstacktrace(); 22
Buffer class How should the Buffer class be implemented? The buffer is shared mutable state! We must ensure that producers and consumers do not adversely affect each other. When the producer thread is adding a new entry, the consumer should not remove an entry and vice versa. Assume: entries are all packed in the array (i.e., there are no holes) addition/deletion is done at end of array. 23
Potential Solution public class Buffer { public final static int SIZE = 10; private Object [] objects = new Object [ SIZE ]; private int count = 0; public synchronized void additem ( Object object ) { objects [ count ++] = object ; public synchronized Object removeitem () { Object object = objects [--count]; return object ; Note: typo in book! not count-- 24
Problem If we simply use synchronized methods, what should we do if buffer is full when adding, or buffer is empty when deleting? We could throw an exception and let the user handle this situation. Not a good solution. Ideally, we should wait for the other process to delete (or add) something HOW? 25
public class Buffer {... public void additem ( Object object ) { while (true) { synchronized (this) { Potential Solution 2 if ( count!= SIZE ){ objects [ count ++] = object ; break; public Object removeitem () { Object object; while (true) { synchronized(this) { if(count!= 0 ) { object = objects [--count]; break; return object ; 26
Busy-waiting! This works, but it is inefficient The thread continuously consumes resources while waiting for something to happen - called busy waiting We would like the thread to wait until some condition is true. How do we suspend a thread until some condition is potentially true? This is achieved using the wait() and notify () methods. 27
Thread States new thread start() Runnable notify() wake up Not Runnable schedule yield() (un)schedule Running sleep() wait() run() terminates uncaught exception terminated
wait(), notify(), and notifyall() These methods can be called on any object. The calling thread must own the lock (monitor) for the object When the current thread executes wait(), it becomes not runnable until the thread is woken up due to a call to notify() or notifyall() on the object on which it is waiting the thread is interrupted by some other thread the timeout interval has passed 29
The wait() method The calling thread gives up its lock on the object until it is woken up or interrupted wait() waits forever to be woken up by notify() or notifyall () wait(long timeout) waits only for timeout milliseconds wait(long timeout, int nanos) specify interval with greater accuracy When the thread waits, it gives up the lock on the object (but not other locks) The thread only resumes after it has re-acquired the lock on the object on which is called wait. 30
Notify methods Calling thread must own the lock on the object notifyall() causes all threads waiting on this object to be woken up notify() causes one (arbitrarily chosen) thread waiting on this object to be woken up Both calls result in the lock on the object to be released one of the newly woken threads will get the lock and be able to move on Repeated waits are necessary to ensure that the necessary conditions have been met 31
Producer-Consumer Solution public class Buffer { public final static int SIZE = 10; private Object [] objects = new Object [ SIZE ]; private int count = 0; public synchronized void additem ( Object object ) throws InterruptedException { while ( count == SIZE ) wait (); objects [ count ++] = object ; notifyall (); public synchronized Object removeitem () throws InterruptedException { while ( count == 0 ) wait (); Object object = objects [--count]; notifyall (); return object ; 32
Synchronization Examples 33
Bank Account Example Create a multi-threaded banking class. Allow threads to deposit; withdraw; getbalance Only one thread should be modifying balance at any time. Multiple threads can be reading balance at the same time. 34
Solution Keep a count of current active readers All access to readers is synchronized on the account object. Changes to balance are made only in the changebalance() method which is synchronized on the account object. can only change if there are no current readers note: wait() until this is true note: readers must notify() when done! 35
SynchronizedAccount public class SynchronizedAccount { private double balance = 0.0; private int readers = 0; public double getbalance () throws InterruptedException { double amount ; synchronized ( this ) { readers ++; amount = balance ; synchronized ( this ) { if( --readers == 0 ) notifyall (); return amount ; public void deposit ( double amount ) throws InterruptedException { changebalance ( amount ); System.out. println (" Deposited $" + amount + "." );... 36
SynchronizedAccount public boolean withdraw ( double amount ) throws InterruptedException { boolean success = changebalance ( -amount ); if( success ) System.out.println (" Withdrew $" + amount + "." ) ; else System.out.println (" Failed to withdraw $" + amount + ": insufficient funds." ); return success ; private synchronized boolean changebalance ( double amount ) throws InterruptedException { boolean success ; while ( readers > 0 ) wait (); if( success = ( balance + amount > 0) ) balance += amount ; return success ; 37
Account tester public class TestSynchronizedAccount extends Thread { static final int THREADS = 10; static SynchronizedAccount account = new SynchronizedAccount(); static TestSynchronizedAccount[] threads = new TestSynchronizedAccount[THREADS]; static double netchange[] = new double[threads]; int threadid; public TestSynchronizedAccount(int id) { threadid=id; public static void main(string[] args) { double totalchange=0; try{ for(int i=0;i<threads;i++) { threads[i] = new TestSynchronizedAccount(i); threads[i].start(); for(int i=0;i<threads;i++){ threads[i].join(); totalchange+=netchange[i]; System.out.println("Net Change: " + totalchange + "\tcurrent Balance: "+ account.getbalance()); catch (InterruptedException e) { e.printstacktrace(); 38
Account tester (cont.) public void run(){ double totalwithdrawals=0; double amount; try{ for(int i=0;i<10;i++) { amount = (Math.ceil(Math.random()*10000))/100.0; if(math.random() <0.5) { if(testsynchronizedaccount.account.withdraw(amount)) { netchange[i]-=amount; System.out.println("\t\t\tThread " + i + " Withdrew: " + amount); else { TestSynchronizedAccount.account.deposit(amount); netchange[i]+=amount; System.out.println("\t\t\tThread " + i + " Deposited: " + amount); catch (InterruptedException e) { e.printstacktrace(); 39
Starvation, Livelock, Deadlock It is possible for the changebalance() method to always be prevented from running by some checkbalance() method Thus deposit/withdrawal can starve A deadlock is a situation where more than one threads are waiting for each other in a circular fashion -- none of them will ever make progress! Another potential problem is livelock, i.e., some thread(s) is making progress, but some others may be blocked indefinitely 40
The Dining Philosophers Consider N philosophers that think and eat There is a circular table with N seats and N chopsticks When a philosopher wants to eat she tries to pick up two chopsticks and then eats Once done eating she replaces (after washing of course) the chopsticks. How do we simulate this? There should be no starving philosopher! 41
Dining Philosopher (deadlock) public void DeadlockPhilosopher extends Thread { public static final int SEATS = 5; private static boolean [] chopsticks = new boolean [SEATS]; private int seat ; public DeadlockPhilosopher ( int seat ) { this.seat = seat ; public void run () { try { getchopstick ( seat ); Thread.sleep (50) ; getchopstick ( seat - 1 ); catch ( InterruptedException e ) { e. printstacktrace (); eat (); 42
Dining Philosophers (cont.) private void getchopstick ( int location ) throws InterruptedException { if( location < 0 ) location += SEATS ; synchronized ( chopsticks ) { while ( chopsticks [ location ] ) chopsticks.wait (); chopsticks [ location ] = true ; System. out. println (" Philosopher " + seat + " picked up chopstick " + location + "."); private void eat () { // done eating, put back chopsticks synchronized ( chopsticks ) { chopsticks [ seat ] = false ; if( seat == 0) chopsticks [ SEATS - 1] = false ; else chopsticks [ seat - 1] = false ; chopsticks.notifyall (); 43
Dining Philosophers (Good) import java.util.*; public class DiningPhilosopher2 extends Thread { public static final int SEATS = 5; private static boolean [] chopsticks = new boolean [ SEATS ]; private int seat ; public DiningPhilosopher2 ( int seat ) { this.seat = seat; public static void main ( String args []) { for ( int i = 0; i < SEATS ; i++ ) chopsticks [i] = false ; DiningPhilosopher2 [] philosophers = new DiningPhilosopher2 [ SEATS ]; for ( int i = 0; i < SEATS ; i++ ) { philosophers [i] = new DiningPhilosopher2 ( i ); philosophers[i].start (); try { for ( int i = 0; i < SEATS ; i++ ) philosophers[i].join (); catch ( InterruptedException e ) { e.printstacktrace (); System.out.println ("All philosophers done."); 44
public void run () { for ( int i = 0; i < 100; i++ ) { think (); getchopsticks (); eat (); private void think () { Random random = new Random (); try { sleep ( random. nextint (20) + 10 ); catch ( InterruptedException e ) { e. printstacktrace (); private void eat () { // eat rice first synchronized ( chopsticks ) { chopsticks [ seat ] = false ; if( seat == 0) chopsticks [ SEATS - 1] = false ; else chopsticks [ seat - 1] = false ; chopsticks. notifyall (); 45
private void getchopsticks () { int location1 = seat ; int location2 ; if( seat == 0 ) location2 = SEATS - 1; else location2 = seat - 1; synchronized ( chopsticks ) { while ( chopsticks [ location1 ] chopsticks [ location2 ] ) { try { chopsticks. wait (); catch ( InterruptedException e ) { e. printstacktrace (); chopsticks [ location1 ] = true ; chopsticks [ location2 ] = true ; System.out.println (" Philosopher " + seat + " picked up chopsticks " + location1 + " and " + location2 + "."); 46