// lowlevel/BankTellerSimulation.java // (c)2016 MindView LLC: see Copyright.txt // We make no guarantees that this code is fit for any purpose. // Visit http://OnJava8.com for more book information. // Using queues and multithreading // {java BankTellerSimulation 5} import java.util.concurrent.*; import java.util.*; // Read-only objects don't require synchronization: class Customer { private final int serviceTime; public Customer(int tm) { serviceTime = tm; } public int getServiceTime() { return serviceTime; } public String toString() { return "[" + serviceTime + "]"; } } // Teach the customer line to display itself: class CustomerLine extends ArrayBlockingQueue { public CustomerLine(int maxLineSize) { super(maxLineSize); } @Override public String toString() { if(this.size() == 0) return "[Empty]"; StringBuilder result = new StringBuilder(); for(Customer customer : this) result.append(customer); return result.toString(); } } // Randomly add customers to a queue: class CustomerSupplier implements Runnable { private CustomerLine customers; private static SplittableRandom rand = new SplittableRandom(47); public CustomerSupplier(CustomerLine cq) { customers = cq; } @Override public void run() { try { while(!Thread.interrupted()) { TimeUnit.MILLISECONDS.sleep(rand.nextInt(300)); customers.put(new Customer(rand.nextInt(1000))); } } catch(InterruptedException e) { System.out.println("CustomerSupplier interrupted"); } System.out.println("CustomerSupplier terminating"); } } class Teller implements Runnable, Comparable { private static int counter = 0; private final int id = counter++; // Customers served during this shift: private int customersServed = 0; private CustomerLine customers; private boolean servingCustomerLine = true; public Teller(CustomerLine cq) { customers = cq; } public void run() { try { while(!Thread.interrupted()) { Customer customer = customers.take(); TimeUnit.MILLISECONDS.sleep( customer.getServiceTime()); synchronized(this) { customersServed++; while(!servingCustomerLine) wait(); } } } catch(InterruptedException e) { System.out.println(this + "interrupted"); } System.out.println(this + "terminating"); } public synchronized void doSomethingElse() { customersServed = 0; servingCustomerLine = false; } public synchronized void serveCustomerLine() { assert !servingCustomerLine: "already serving: " + this; servingCustomerLine = true; notifyAll(); } public String toString() { return "Teller " + id + " "; } public String shortString() { return "T" + id; } // Used by priority queue: public synchronized int compareTo(Teller other) { return customersServed < other.customersServed ? -1 : (customersServed == other.customersServed ? 0 : 1); } } class TellerManager implements Runnable { private ExecutorService exec; private CustomerLine customers; private PriorityQueue workingTellers = new PriorityQueue<>(); private Queue tellersDoingOtherThings = new LinkedList<>(); private int adjustmentPeriod; public TellerManager(ExecutorService e, CustomerLine customers, int adjustmentPeriod) { exec = e; this.customers = customers; this.adjustmentPeriod = adjustmentPeriod; // Start with a single teller: Teller teller = new Teller(customers); exec.execute(teller); workingTellers.add(teller); } public void adjustTellerNumber() { // This is actually a control system. By adjusting // the numbers, you can reveal stability issues in // the control mechanism. // If line is too long, add another teller: if(customers.size() / workingTellers.size() > 2) { // If tellers are on break or doing // another job, bring one back: if(tellersDoingOtherThings.size() > 0) { Teller teller = tellersDoingOtherThings.remove(); teller.serveCustomerLine(); workingTellers.offer(teller); return; } // Else create (hire) a new teller Teller teller = new Teller(customers); exec.execute(teller); workingTellers.add(teller); return; } // If line is short enough, remove a teller: if(workingTellers.size() > 1 && customers.size() / workingTellers.size() < 2) reassignOneTeller(); // If there is no line, we only need one teller: if(customers.size() == 0) while(workingTellers.size() > 1) reassignOneTeller(); } // Give a teller a different job or a break: private void reassignOneTeller() { Teller teller = workingTellers.poll(); teller.doSomethingElse(); tellersDoingOtherThings.offer(teller); } @Override public void run() { try { while(!Thread.interrupted()) { TimeUnit.MILLISECONDS.sleep(adjustmentPeriod); adjustTellerNumber(); System.out.print(customers + " { "); for(Teller teller : workingTellers) System.out.print(teller.shortString() + " "); System.out.println("}"); } } catch(InterruptedException e) { System.out.println(this + "interrupted"); } System.out.println(this + "terminating"); } @Override public String toString() { return "TellerManager "; } } public class BankTellerSimulation { static final int MAX_LINE_SIZE = 50; static final int ADJUSTMENT_PERIOD = 1000; public static void main(String[] args) throws Exception { ExecutorService es = Executors.newCachedThreadPool(); // If line is too long, customers will leave: CustomerLine customers = new CustomerLine(MAX_LINE_SIZE); es.execute(new CustomerSupplier(customers)); // Manager will add and remove tellers as necessary: es.execute(new TellerManager( es, customers, ADJUSTMENT_PERIOD)); if(args.length > 0) // Optional argument TimeUnit.SECONDS.sleep(new Integer(args[0])); else { System.out.println("Press 'Enter' to quit"); System.in.read(); } es.shutdownNow(); } } /* Output: [768][193][807][125] { T1 T0 } [125][634][682][267][954][506][639][213] { T2 T0 T1 } [213][592][770][919][552][727][998][902] { T2 T0 T1 } [552][727][998][902][769][373][313][683][177][526] { T3 T2 T1 T0 } TellerManager interrupted Teller 3 interrupted CustomerSupplier interrupted Teller 3 terminating Teller 2 interrupted Teller 2 terminating TellerManager terminating Teller 1 interrupted Teller 0 interrupted Teller 0 terminating Teller 1 terminating CustomerSupplier terminating */