Download Race Conditions and Synchronization - Object Oriented Programming and Data Structures - Lecture Slides and more Slides Object Oriented Programming in PDF only on Docsity!
Race Conditions and
Synchronization
Reminder
- A “race condition” arises if two threads try and share some data
- One updates it and the other reads it, or both update the data
- In such cases it is possible that we could see the data “in the middle” of
being updated
- A “race condition”: correctness depends on the update racing to completion
without the reader managing to glimpse the in-progress update
- Synchronization (aka mutual exclusion) solves this
Java Synchronization (Locking)
public void doSomething() { synchronized (this) { ... } } public synchronized void doSomething() { ... }
• You can lock on any object, including this
is equivalent to
How locking works
- Only one thread can “hold” a lock at a time
- If several request the same lock, Java somehow decides which will get it
- The lock is released when the thread leaves the synchronization block
- synchronized(someObject) { protected code }
- The protected code has a mutual exclusion guarantee: At most one thread can
be in it
- When released, some other thread can acquire the lock
Visualizing deadlock
Process A Process B X Y
A has a lock on X
wants a lock on Y
B has a lock on Y
wants a lock on X
Deadlocks always involve cycles
- They can include 2 or more threads or processes in a waiting cycle
- Other properties:
- The locks need to be mutually exclusive (no sharing of the objects being
locked)
- The application won’t give up and go away (no timer associated with the lock
request)
- There are no mechanisms for one thread to take locked resources away from
another
thread – no “preemption”
“... drop that mouse or you’ll be down to 8 lives”
Higher level abstractions
- Locking is a very low-level way to deal with synchronization
- So many programmers work with higher level concepts. Sort of like
ADTs for synchronization
- We’ll just look at one example today
- There are many others; take cs4410 to learn more
A producer/consumer example
- Thread A produces loaves of bread and puts them on a shelf with
capacity K
- For example, maybe K=
- Thread B consumes the loaves by taking them off the shelf
- Thread A doesn’t want to overload the shelf
- Thread B doesn’t wait to leave with empty arms producer shelves consumer
Things to notice
- Wait needs to wait on the same object that you used for
synchronizing (in our example, “this”, which is this instance of the
Bakery)
- Notify wakes up just one waiting thread, notifyall wakes all of them
up
- We used a while loop because we can’t predict exactly which thread
will wake up “next”
Bounded Buffer
- Here we take our producer/consumer and add a notion of passing
something from the producer to the consumer
- For example, producer generates strings
- Consumer takes those and puts them into a file
- Question: why would we do this?
- Keeps the computer more steadily busy
Bounded Buffer example
class BoundedBuffer { int putPtr = 0, getPtr = 0; // Next slot to use int available = 0; // Items currently available final int K = 10; // buffer capacity T[] buffer = new T[K]; public synchronized void produce(T item) { while(available == K) this.wait(); // Wait until not full buffer[putPtr++ % K] = item; ++available; this.notifyall(); // Signal: not empty } public synchronized T consume() { while(available == 0) this.wait(); // Wait until not empty --available; T item = buffer[getPtr++ % K]; this.notifyall(); // Signal: not full return item; } }
In an ideal world…
- Bounded buffer allows producer and consumer to both run
concurrently, with neither blocking
- This happens if they run at the same average rate
- … and if the buffer is big enough to mask any brief rate surges by either of
the two
- But if one does get ahead of the other, it waits
- This avoids the risk of producing so many items that we run out of computer
memory for them. Or of accidentally trying to consume a non-existent item.
Code we’re given is unsafe
class BST { Object name; // Name of this node Object value; // Value of associated with that name BST left, right; // Children of this node // Constructor public void BST(Object who, Object what) { name = who; value = what; } // Returns value if found, else null public Object get(Object goal) { if(name.equals(goal)) return value; if(name.compareTo(goal) < 0) return left==null? null: left.get(goal); return right==null? null: right.get(goal); } // Updates value if name is already in the tree, else adds new BST node public void put(Object goal, object value) { if(name.equals(goal)) { this.value = value; return; } if(name.compareTo(goal) < 0) { if(left == null) { left = new BST(goal, value); return; } left.put(goal, value); } else { if(right == null) { right = new BST(goal, value); return; } right.put(goal, value); } } }
Attempt
- Just make both put and get synchronized:
- public synchronized Object get(…) { … }
- public synchronized void put(…) { … }
- Let’s have a look….