Skip to content

About

A flexible thread-safe generic object pool

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

APACHE v2 License Latest Release Javadocs Codacy

generic-object-pool

generic-object-pool is a lightweight generic object pool, providing object lifecycle management, metrics, claim / release mechanism and object invalidation, as well as auto initialize a core pool and auto expiry policies.

Origins

The work that became generic-object-pool began in June 2019 with a fork of KBOP, created by Jeremy Unruh. It was subsequently modernized and extensively redesigned for Simple Java Mail's SMTP connection pooling, and has continued as an independent library. KBOP's original work is used under the MIT License; its complete copyright and license notice is included in NOTICE.txt.

Setup

Maven Dependency Setup

<dependency>
	<groupId>com.github.bbottema</groupId>
	<artifactId>generic-object-pool</artifactId>
	<version>2.6.0</version>
</dependency>

For JPMS applications, the published JAR declares the stable automatic module name org.bbottema.genericobjectpool.

Release Notes

2.6.0 (12 October 2026)

  • #26: Pause automatic pool top-up with Allocator.isBackgroundAllocationAllowed(), for example while your application handles a service outage. Existing allocators keep automatic top-up enabled; ordinary claims, existing resources and expiration are unchanged.
  • Returning false postpones spare-resource creation without an allocation error or reserved capacity. Your application decides when to allow it again; the pool does not add retries or health checks. See Pausing automatic top-up for an example.
  • AllocationContext.getClaimIdentity() correlates one acquisition across pool integration layers. Narrowing its budget preserves the identity; starting another claim creates a new one. Java 8 and existing APIs remain supported.

2.5.1 (17 September 2026)

  • #24, #25: Keep creation-age and last-allocation timeout policies distinct when their thresholds or randomized bounds match, so combined policies retain both rules and each rule keeps its own expiry state.
  • Matching configurations of the same policy type remain equal. Java 8, existing APIs, and the JPMS module name are unchanged.

Usage

Creating pools

// basic pool with no eager loading and no expiry policy
PoolConfig<Foo> poolConfig = PoolConfig.<Foo>builder()
   .maxPoolsize(10)
   .build();

GenericObjectPool<Foo> pool = new GenericObjectPool<>(poolConfig, new MyFooAllocator());
// more advanced pool with eager loading and auto expiry
PoolConfig<Foo> poolConfig = PoolConfig.<AtomicReference<Integer>>builder()
   .corePoolsize(20) // keeps 20 objects eagerly allocated at all times
   .maxPoolsize(20)
   // deallocate after 30 seconds, but every time an object is claimed the expiry timeout is reset
   .expirationPolicy(new TimeoutSinceLastAllocationExpirationPolicy<Foo>(30, TimeUnit.SECONDS))
   .build();

GenericObjectPool<Foo> pool = new GenericObjectPool<>(poolConfig, new MyFooAllocator());

Pausing automatic top-up

The pool normally creates spare resources in the background to maintain corePoolsize. You can pause that work without stopping callers from borrowing resources. For example, your application might avoid opening spare service connections while it handles an outage, then permit them again after recovery.

Override Allocator.isBackgroundAllocationAllowed() and return your application's current decision. Here, MyFooAllocator is the application's allocator from the pool examples above, and AtomicBoolean makes the flag safe across threads:

AtomicBoolean automaticTopUp = new AtomicBoolean(true);
Allocator<Foo> allocator = new MyFooAllocator() {
    @Override
    public boolean isBackgroundAllocationAllowed() {
        return automaticTopUp.get();
    }
};

PoolConfig<Foo> config = PoolConfig.<Foo>builder()
    .corePoolsize(2)
    .maxPoolsize(10)
    .build();
GenericObjectPool<Foo> pool = new GenericObjectPool<>(config, allocator);

automaticTopUp.set(false); // Application decides not to create spare resources right now.
// Normal claim/release calls still work, including allocation needed by an explicit claim.
automaticTopUp.set(true);  // The maintenance worker can top up again on a later pass.

The numbers are example pool sizes, not limits supplied by the library. With no core pool size configured, there is no automatic top-up to pause. Returning false is not an allocation error and keeps no pool capacity reserved. Existing resources, expiration and disposal continue normally; an allocation already underway may still finish.

Keep the check quick and thread-safe: the maintenance worker polls it outside pool bookkeeping locks and checks again immediately before creating a resource. A runtime exception in the check is logged and postpones top-up. This is not a circuit breaker for explicit claims, and it does not schedule retries or detect service recovery for you. Existing allocators need no changes: the default returns true. This API is available from 2.6.0.

Claim / release API

Claiming objects from the pool (blocking):

// borrow an object and block until available
PoolableObject<Foo> obj = pool.claim();

Claiming objects from the pool (blocking until timeout):

PoolableObject<Foo> obj = pool.claim(1, TimeUnit.SECONDS); // null if timed out

Claiming an already available object matching a predicate:

PoolableObject<Foo> obj = pool.claimMatching(
	poolable -> poolable.idleAgeMs() >= TimeUnit.MINUTES.toMillis(5),
	1,
	TimeUnit.SECONDS);

if (obj != null) {
	try {
		obj.getAllocatedObject().ping();
		obj.release();
	} catch (IOException e) {
		obj.invalidate();
	}
}

The predicate is evaluated while the pool claim lock is held, so keep it fast and side-effect free. Run slow work such as ping/keep-alive checks after the object has been claimed.

Releasing Objects back to the Pool:

PoolableObject<Foo> obj = pool.claim();
obj.release(); // make available for reuse
// or
obj.invalidate(); // remove from pool, deallocating

Invalidation wakes ordinary callers that are already waiting for capacity. With a core size of zero, a waiting caller can create a replacement; a configured core pool also replenishes itself. Final cleanup remains asynchronous and need not finish before a replacement can be used. Matching claims still only take available objects: they do not allocate replacements themselves.

Optional cancellation and a total acquisition budget

For example, a cancelled export job should stop waiting for another database connection. Create the control before starting its worker, then retain it in the job's stop handler:

ClaimControl claimControl = new ClaimControl();
ClaimOptions options = ClaimOptions.withTimeout(30, TimeUnit.SECONDS)
    .withClaimControl(claimControl);

// On the job's worker; still an ordinary blocking call.
PoolableObject<Foo> resource = pool.claim(options);
// null: budget expired. CancellationException: stop requested. InterruptedException: worker interrupted.
if (resource != null) {
    try {
        resource.getAllocatedObject().doWork();
    } finally {
        resource.release();
    }
}

// In the stop handler, potentially on another thread:
claimControl.requestCancellation();

Creating a control does not cancel anything. A control is thread-safe and one-shot; options are immutable and reusable. Each call starts a new monotonic budget covering selection by an outer pool, lock waiting, availability and preparation. claimMatching(predicate, options) has the same contract but never allocates resources. Zero budget starts no preparation. The legacy timeout methods retain their existing wait semantics; they do not gain a total deadline implicitly.

Pool integrations can correlate one running claim across layers using context.getClaimIdentity(). The opaque object is unique to each ClaimOptions.start() call; limitedTo(...) keeps the same identity while narrowing its budget. For example, register a caller's observation by that identity before claimWithContext(context), then remove it in finally after the claim settles. It is not a resource ID or a reusable job ID. Ordinary callers need no extra wiring.

Cancellation ends at handoff: requesting it after claim returns does not revoke the borrowed resource. It does not interrupt an executor thread. Allocator callbacks run on the claiming thread (core replenishment remains pool-owned), and allocation/reuse/release preparation remains serialized per pool. Final deallocation runs on the cleanup worker.

Override allocate(AllocationContext) or allocateForReuse(resource, AllocationContext) to cooperate: check context.throwIfCancellationRequested(), use context.getRemainingTime(unit) for your own I/O timeout, and optionally register a quick, non-blocking abort action with context.onCancellation(...). An earlier request invokes that handler immediately. Handler runtime exceptions are logged and ignored. Close registrations when their resource is no longer owned; the pool also detaches them before handoff. An allocator must clean up anything it creates but never returns.

An old allocator that blocks in allocate() remains supported, but cannot be forcibly stopped: cancellation is recorded promptly, then the claim waits for allocation to exit and disposes any late resource instead of handing it off. Required cleanup can also outlast the acquisition budget. New context-aware claims settle only after that cleanup finishes; cleanup failures are reported (or suppressed on the original failure), not treated as successful disposal. A preparation exception after cancellation was requested is retained as the cause of CancellationException.

invalidate() still only schedules cleanup. If physical disposal matters, use resource.getDisposalCompletion().toCompletableFuture().get(). A healthy release() does not complete this stage; the resource is still reusable. Each returned stage is detached from the pool's internal signal. Keep non-async stage callbacks short because they can run on the cleanup worker.

Shutting down a pool

Future<?> shutdownSequence = pool.shutdown();

// wait for shutdown to complete
shutdownSequence.get();
// until timeout
shutdownSequence.get(10, TimeUnit.SECONDS);

Creating your objects

Implementing a simple Allocator to create your objects when populating the pool either eagerly or lazily. Every method except allocate is optional:

static class FooAllocator extends Allocator<Foo> {
	/**
	 * Initial creation and initialization.
	 * Called when claim comes or when pool is eagerly loading for core size.
	 */
	@Override
	public AtomicReference<Integer> allocate() {
		return new Foo();
	}
}

More comprehensive life cycle management:

static class FooAllocator extends Allocator<Foo> {
	/**
	 * Initial creation and initialization.
	 * Called when claim comes or when pool is eagerly loading for core size.
	 */
	@Override
	public AtomicReference<Integer> allocate() {
		return new Foo();
	}
	
	/**
	 * Uninitialize an instance which has been released back to the pool, until it is claimed again.
	 */
	@Override
	protected void deallocateForReuse(Foo object) {
		object.putAtRest();
	}
	
	/**
	 * Reinitialize an object so it is ready to be claimed.
	 */
	@Override
	protected void allocateForReuse(Foo object) {
		object.reinitialize();
	}
	
	/**
	 * Clean up an object no longer needed by the pool.
	 */
	@Override
	protected void deallocate(Foo object) {
		object.clear();
	}
}

Metrics

PoolMetrics metrics = pool.getPoolMetrics();
metrics.getCurrentlyClaimed(); // currently claimed by threads and not released yet
metrics.getCurrentlyWaitingCount(); // currently waiting threads that want to claim
metrics.getCorePoolsize(); // number of instances to auto allocated (eager loading)
metrics.getMaxPoolsize(); // max number of objects allowed at all times
metrics.getCurrentlyAllocated(); // available + claimed objects
metrics.getTotalAllocated(); // total number of allocations during pool's existence
metrics.getTotalClaimed(); // total number of claims during pool's existence

For idle maintenance, PoolableObject#idleAgeMs() reports how long an object has been available for claiming. It returns 0 while the object is claimed.

If for some reason you need to have more control over how threads are created, you can provide you own ThreadFactory:

PoolConfig<Foo> poolConfig = PoolConfig.<AtomicReference<Integer>>builder()
   .threadFactory(new MyCustomThreadFactory())
   .build();

Other Expiry strategies

You can expire objects based on age since creation or age since last allocation. For these use:

  • TimeoutSinceCreationExpirationPolicy
  • TimeoutSinceLastAllocationExpirationPolicy

You can also spread the expiry around in a bandwidth to avoid having everything expire at the same time, hogging system resources. for these use:

  • SpreadedTimeoutSinceCreationExpirationPolicy
  • SpreadedTimeoutSinceLastAllocationExpirationPolicy

You can also combine multiple expirations, by passing instances of them as a set to:

  • CombinedExpirationPolicies

Since 2.5.1, creation-age and last-allocation policies remain distinct even when their timeout values or randomized bounds match. Matching configurations of the same policy type still represent one rule.

Finally, you can extend any of these or create your own from scratch by implementing:

  • ExpirationPolicy

To aid you in creating your own expiry policy, you can calculate and store an expiry age on the poolable object:

poolableObject.getExpiries().put(this, calculatedAge);
Long previouslyCalculateAge = poolableObject.getExpiries().get(this);

You can always extend one the abstract classes SpreadedTimeoutExpirationPolicy and TimeoutExpirationPolicy, which do this for you.

About

A flexible thread-safe generic object pool

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages