|
|
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 4 of 7
Compile LockDemo.java and run the application. You should observe output similar to the following:
Thread A has entered its critical section.
Thread A is performing work for 2 seconds.
Thread A has finished working.
Thread B has entered its critical section.
Thread B is performing work for 2 seconds.
Thread B has finished working.
Comment out rl.lock(); and rl.unlock(); and you should observe interleaved output like what is shown below:
Thread A is performing work for 2 seconds.
Thread B is performing work for 2 seconds.
Thread A has finished working.
Thread B has finished working.
The Condition interface factors out the java.lang.Object monitor methods (wait(), notify(), and notifyAll()) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary
Lock implementations. Where Lock replaces synchronized methods and statements, Condition replaces Object monitor methods.
Condition declares the following methods:
void await() forces the current thread to wait until it's signalled or interrupted.
boolean await(long time, TimeUnit unit) forces the current thread to wait until it's signalled or interrupted, or the specified waiting time elapses.
long awaitNanos(long nanosTimeout) forces the current thread to wait until it's signalled or interrupted, or the specified waiting time elapses.
void awaitUninterruptibly() forces the current thread to wait until it's signalled.
boolean awaitUntil(Date deadline) forces the current thread to wait until it's signalled or interrupted, or the specified deadline elapses.
void signal() wakes up one waiting thread.
void signalAll() wakes up all waiting threads.
The classic producer-consumer example nicely demonstrates conditions. In this example, a producer thread repeatedly produces items for consumption by a consumer thread.
The producer thread must not produce a new item until the previously produced item has been consumed. Similarly, the consumer thread must not consume an item that hasn't been produced. This is known as lockstep synchronization.
Listing 2 demonstrates conditions (and locks) in a producer-consumer context.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CondDemo
{
public static void main(String[] args)
{
Shared s = new Shared();
new Producer(s).start();
new Consumer(s).start();
}
}
class Shared
{
// Fields c and available are volatile so that writes to them are visible to
// the various threads. Fields lock and condition are final so that they're
// initial values are visible to the various threads. (The Java memory model
// promises that, after a final field has been initialized, any thread will
// see the same [correct] value.)
private volatile char c;
private volatile boolean available;
private final Lock lock;
private final Condition condition;
Shared()
{
c = '\u0000';
available = false;
lock = new ReentrantLock();
condition = lock.newCondition();
}
Lock getLock()
{
return lock;
}
char getSharedChar()
{
lock.lock();
try
{
while (!available)
try
{
condition.await();
}
catch (InterruptedException ie)
{
ie.printStackTrace();
}
available = false;
condition.signal();
}
finally
{
lock.unlock();
return c;
}
}
void setSharedChar(char c)
{
lock.lock();
try
{
while (available)
try
{
condition.await();
}
catch (InterruptedException ie)
{
ie.printStackTrace();
}
this.c = c;
available = true;
condition.signal();
}
finally
{
lock.unlock();
}
}
}
class Producer extends Thread
{
// l is final because it's initialized on the main thread and accessed on the
// producer thread.
private final Lock l;
// s is final because it's initialized on the main thread and accessed on the
// producer thread.
private final Shared s;
Producer(Shared s)
{
this.s = s;
l = s.getLock();
}
@Override
public void run()
{
for (char ch = 'A'; ch <= 'Z'; ch++)
{
l.lock();
s.setSharedChar(ch);
System.out.println(ch + " produced by producer.");
l.unlock();
}
}
}
class Consumer extends Thread
{
// l is final because it's initialized on the main thread and accessed on the
// consumer thread.
private final Lock l;
// s is final because it's initialized on the main thread and accessed on the
// consumer thread.
private final Shared s;
Consumer(Shared s)
{
this.s = s;
l = s.getLock();
}
@Override
public void run()
{
char ch;
do
{
l.lock();
ch = s.getSharedChar();
System.out.println(ch + " consumed by consumer.");
l.unlock();
}
while (ch != 'Z');
}
}
Listing 2 presents four classes: CondDemo, Shared, Producer, and Consumer. CondDemo drives the application, Shared encapsulates the logic for setting and getting a shared variable's value, Producer describes the producer thread, and Consumer describes the consumer thread.
Previous articles in Java 101: The next generation
java.util.concurrent, with Jeff Friesen's detailed introduction to the Executor framework, synchronizer types, and the Java Concurrent Collections
package.
Concurrency tutorials on JavaWorld:
java.util.concurrent to work around deadlock and similar threading pitfalls.
CountDownLatch and Executors.newFixedThreadPool concurrency utilities.
java.util.concurrent classes were used to optimize thread use for faster performance in a real-world application.
ExecutorService class with ForkJoinPool in a web crawler.