- A Somewhat Complex Example
- Starting and Stopping
- Spectators
- Timing, Scoring, and Spectating
- Startup and Shutdown
- Wrap Up
Timing, Scoring, and Spectating
One of the more complicated aspects of this simulation is keeping track of every racer's time. A Racer indicates the completion of a sector by adding a SectorTime object to one of three ConcurrentLinkedQueue objects (one per sector). This class allows for simultaneous additions and removals, meaning that one car can deposit its time at the tail of the Queue while the timing system pulls another's time from the headwithout blocking each other. So, as the cars fly by, they don't need to wait for the timing system.
To process updates, we'll start an instance of SectorIntervalMonitor for each Queue, a Runnable class we've written to pull SectorTime objects from the Queues and pass them along to the timing system. Overall times are stored in a two-dimensional array of longs, one entry for every sector of every lap for every Racer. To limit access to the array, we'll use the new ReentrantReadWriteLock class, which allows for multiple concurrent read locks, and allows write locks that can be downgraded to read, among other things. Here's the code to update the array:
protected void updatePosition(SectorTime sectorTime) { Racer racer = sectorTime.getRacer(); int sector = (sectorTime.getLap() * NUM_SECTORS) + sectorTime.getSectorNumber(); long currentTime = sectorTime.getCurrentTime(); ... Lock sectorWrite = getSectorTimesLock().writeLock(); Lock positionsWrite = getPositionsLock().writeLock(); //Get a write lock and modify the array sectorWrite.lock(); getSectorTimes()[racer.getID()][sector] = currentTime - getEvent().getRaceStartTime(); //Indicate that next spectator must get updated results positionsWrite.lock(); updatePositions(); positionsWrite.unlock(); sectorWrite.unlock(); }
Each call to writeLock() gets us a reference to the lock, and the subsequent call to lock() will block until the lock is available. We use two locks: one to get to the array called sectorTimes, and a second to update a global String called positions. The Spectator threads don't look at the timing array directly, instead retrieving the positions Stringa leader board listing all racers, their positions, and their time differentials. Because we'll have many more Spectators than Racers, more reads will happen than writes, so we want to cache that leader board and generate it only when the data behind it has changed, not whenever a Spectator comes along. The Spectator threads need worry only about retrieving the String:
public String getPositionsString() { getPositionsLock().readLock().lock(); try { return positions; } finally { getPositionsLock().readLock().unlock(); } }
As the value we're returning is itself the subject of the lock, we must call unlock() in a finally block. Calls to unlock() must always be made or the Lock will remain locked, even after the objects go out of scope! Typically you'd want them in a finally block anywhere an exception could occur, or if you must return a value that has a lock, as here.