BoundedConcurrentQueue.top() blocks indefinitely (infinite if the producers have terminated) if the queue is empty due to aquiring semaphoreFreeSpots. If the intention is indeed to just peek, it should 1) be called peek, 2) not aquire semaphoreFreeSpots and 3) return nullnull if the size field indicates that the queue is empty:
Another alternative would be to establish a new method top(long timeoutInMilis) that returns null or throws if the timeout expires without an element showing up.
The termination of the Consumers doesn't seem well-thought out to me. Clearly, currently the intention seems to be that they bail when they pop a halting element from the queue, however, nothing currently ensures that the correct number of halting elements is produced (Producers seem to be arbitrarely producing 0 or 1 depending on which if inside their run() method is hit, and in any case there could be an unequal number of producers and consumers). Indeed, with the following settings:
a number of consumers block infinitely at the end waiting inside top() for an element that will never arrive. If you implemented the top()top() suggestion above, that method returning nullnull would be a good opportunity to check this.sharedState.isHaltRequested() (that field is inherited from the superclass and can IMO stay there, although they do would need to initialise it properly in that case) on wether its time to bail out. Otherwise, I think the best way would be to have the producers become aware of how many consumers there are via a AtomicInteger field in sharedState and have them work together to produce exactly the correct number of halting elements.
Since all those setters in Simulator set stuff thats ultimatively required, move them to the constructor (or, better, use a builder pattern).