A striped Lock/Semaphore/ReadWriteLock. This offers the underlying lock striping
similar to that of ConcurrentHashMap in a reusable form, and extends it for
semaphores and read-write locks. Conceptually, lock striping is the technique of dividing a lock
into many stripes, increasing the granularity of a single lock and allowing independent
operations to lock different stripes and proceed concurrently, instead of creating contention
for a single lock.

The guarantee provided by this class is that equal keys lead to the same lock (or semaphore),
i.e. if (key1.equals(key2)) then striped.get(key1) == striped.get(key2)
(assuming java.lang.Object.hashCode() is correctly implemented for the keys). Note
that if key1 is not equal to key2, it is not
guaranteed that striped.get(key1) != striped.get(key2); the elements might nevertheless
be mapped to the same lock. The lower the number of stripes, the higher the probability of this
happening.

There are three flavors of this class: Striped<Lock>, Striped<Semaphore>,
and Striped<ReadWriteLock>. For each type, two implementations are offered:
strong and weak
Striped<Lock>, strong and lazyWeakSemaphore(int,int)Striped<Semaphore>, and readWriteLock(int) and weak
Striped<ReadWriteLock>. Strong means that all stripes (locks/semaphores) are
initialized eagerly, and are not reclaimed unless Striped itself is reclaimable.
Weak means that locks/semaphores are created lazily, and they are allowed to be reclaimed
if nobody is holding on to them. This is useful, for example, if one wants to create a Striped<Lock> of many locks, but worries that in most cases only a small portion of these
would be in use.

Prior to this class, one might be tempted to use Map<K, Lock>, where K
represents the task. This maximizes concurrency by having each unique key mapped to a unique
lock, but also maximizes memory footprint. On the other extreme, one could use a single lock
for all tasks, which minimizes memory footprint but also minimizes concurrency. Instead of
choosing either of these extremes, Striped allows the user to trade between required
concurrency and memory footprint. For example, if a set of tasks are CPU-bound, one could easily
create a very compact Striped<Lock> of availableProcessors() * 4 stripes,
instead of possibly thousands of locks which could be created in a Map<K, Lock>
structure.

Returns the stripes that correspond to the passed objects, in ascending (as per
getAt(int)) order. Thus, threads that use the stripes in the order returned
by this method are guaranteed to not deadlock each other.

It should be noted that using a Striped<L> with relatively few stripes, and
bulkGet(keys) with a relative large number of keys can cause an excessive number
of shared stripes (much like the birthday paradox, where much fewer than anticipated birthdays
are needed for a pair of them to match). Please consider carefully the implications of the
number of stripes, the intended concurrency level, and the typical number of keys used in a
bulkGet(keys) operation. See Balls
in Bins model for mathematical formulas that can be used to estimate the probability of
collisions.

Parameters:

keys arbitrary non-null keys

Returns:

the stripes corresponding to the objects (one per each object, derived by delegating
to get(java.lang.Object); may contain duplicates), in an increasing index order.

Implementation of Striped where up to 2^k stripes can be represented, using a Cache
where the key domain is [0..2^k). To map a user key into a stripe, we take a k-bit slice of the
user key's (smeared) hashCode(). The stripes are lazily initialized and are weakly referenced.