As the name suggests, object pool is the pool where objects are stored. Like thread pool, database connection pool and http connection pool, it is a typical pool design idea.
The advantage of object pool is that it can centrally manage the objects in the pool and reduce the frequent creation and destruction of long-term objects, so as to improve reusability and save resource consumption. It can effectively avoid frequent allocation of memory for objects and release memory in the heap, so as to reduce the burden of jvm garbage collector and avoid memory jitter.
Apache Common Pool2 is a general object pool technology implementation provided by Apache, which can easily customize the object pool you need. The famous Redis client Jedis internal connection pool is implemented based on it.
Core interface
The core internal classes of Apache Common Pool2 are as follows:
-
ObjectPool: object pool interface, object pool entity, and the place where objects are accessed
-
Object provision and return (factory operation): borowobject returnObject
-
Create object (using factory to create): addObject
-
Destroy object (use factory to destroy): invalideobject
-
Number of free objects and used objects in the pool: getNumActive getNumIdle
-
PooledObject: the wrapped object is an object in the pool. In addition to the object itself, it contains many information such as creation time, last called time, etc
-
PooledObjectFactory: object factory, which manages the life cycle of objects and provides a series of functions such as object creation, destruction, verification, passivation and activation
-
BaseObjectPoolConfig: provides some necessary configurations, such as whether the idle queue is first in first out, whether the factory needs to test before creating objects, and whether the objects are tested when they are taken out of the object pool. GenericObjectPoolConfig inherits this class and makes the default configuration. We can inherit it in actual use. We can expand the object pool configuration in combination with business conditions, For example, database connection pool thread prefix, string pool length or name rule, etc
-
Keyedobjectpool < K, V >: an object pool interface in the form of key value pairs, which is rarely used
-
Keyedpooledobjectfactory < K, V >: the same as above. It is the factory for managing objects in the key value pair object pool
The state of the pool object
Check the source code. All possible states of pool objects are listed under the PooledObjectState enumeration.
public enum PooledObjectState { //In idle queue,Not used yet IDLE, //in use ALLOCATED, //In idle queue,The conditions for expulsion are currently being tested EVICTION, //It is not in the idle queue and is currently being tested for possible eviction. Because during the test, an attempt was made to borrow an object and remove it from the queue. //After the recycle test is completed, it should be returned to the head of the queue. EVICTION\_RETURN\_TO\_HEAD, //In the queue, is being verified VALIDATION, //Not in queue, currently validating. The object was borrowed during validation because it was configured testOnBorrow, //So delete it from the queue and pre allocate it. Once validation is complete, it should be assigned. VALIDATION\_PREALLOCATED, //Not in queue, currently validating. An attempt was made to borrow the object before testing whether to remove it from the queue. //Once validation is complete, it should be returned to the head of the queue. VALIDATION\_RETURN\_TO\_HEAD, //invalid state(Such as test or verification),And will/Has been destroyed INVALID, //Determined to be invalid,Will be set to obsolete ABANDONED, //Finished using,Return to pool RETURNING }
State understanding
Disabled: it is marked in this status if it has not been used for a long time after being lent. As shown in the code, when the object is in the ALLOCATED state, that is, it is being lent out for use, and the time since it was last used exceeds the set getRemoveAbandonedTimeout, it is marked as abandoned.
private void removeAbandoned(final AbandonedConfig abandonedConfig) { // Generate a list of abandoned objects to remove final long now = System.currentTimeMillis(); final long timeout = now - (abandonedConfig.getRemoveAbandonedTimeout() \* 1000L); final ArrayList<PooledObject<T>> remove = new ArrayList<>(); final Iterator<PooledObject<T>> it = allObjects.values().iterator(); while (it.hasNext()) { final PooledObject<T> pooledObject = it.next(); synchronized (pooledObject) { if (pooledObject.getState() == PooledObjectState.ALLOCATED && pooledObject.getLastUsedTime() <= timeout) { pooledObject.markAbandoned(); remove.add(pooledObject); } } }
Process understanding
1. Where is the real object stored?
private PooledObject<T> create() throws Exception { ..... final PooledObject<T> p; try { p = factory.makeObject(); ..... allObjects.put(new IdentityWrapper<>(p.getObject()), p); return p; }
When we look at all objects, all objects are stored in the ConcurrentHashMap, except for the killed objects.
/* * All of the objects currently associated with this pool in any state. It * excludes objects that have been destroyed. The size of * {@link #allObjects} will always be less than or equal to {@link * #_maxActive}. Map keys are pooled objects, values are the PooledObject * wrappers used internally by the pool. */ private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects = new ConcurrentHashMap<>();
2. The logic of access object is summarized as follows
-
First, according to the configuration of AbandonedConfig, judge whether to perform the cleanup operation before fetching the object
-
Then try to get the object from idleObject. If you can't get it, create a new object
-
Judge whether blockWhenExhausted is set to true (this configuration means whether to block until there are idle objects when the number of objects in the active state of the object pool has reached the maximum value of maximum)
-
Yes, wait for the available object according to the set borrowMaxWaitMillis property. After the available object, call the factory's factory.activateObject method to activate the object.
-
When gettestonmirror is set to true, call factory.validateObject(p) to verify the object. After passing the verification, execute the next step
-
Call the updateStatsBorrow method to update some statistical items after the object is successfully lent, such as the number of objects returned to the object pool
//.... private final LinkedBlockingDeque<PooledObject<T>> idleObjects; //.... public T borrowObject(final long borrowMaxWaitMillis) throws Exception { assertOpen(); final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) && (getNumActive() > getMaxTotal() - 3) ) { removeAbandoned(ac); } PooledObject<T> p = null; // Get local copy of current config so it is consistent for entire // method execution final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; final long waitTime = System.currentTimeMillis(); while (p == null) { create = false; p = idleObjects.pollFirst(); if (p == null) { p = create(); if (p != null) { create = true; } } if (blockWhenExhausted) { if (p == null) { if (borrowMaxWaitMillis < 0) { p = idleObjects.takeFirst(); } else { p = idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); } } if (p == null) { throw new NoSuchElementException( "Timeout waiting for idle object"); } } else { if (p == null) { throw new NoSuchElementException("Pool exhausted"); } } if (!p.allocate()) { p = null; } if (p != null) { try { factory.activateObject(p); } catch (final Exception e) { try { destroy(p, DestroyMode.NORMAL); } catch (final Exception e1) { // Ignore - activation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( "Unable to activate object"); nsee.initCause(e); throw nsee; } } if (p != null && getTestOnBorrow()) { boolean validate = false; Throwable validationThrowable = null; try { validate = factory.validateObject(p); } catch (final Throwable t) { PoolUtils.checkRethrow(t); validationThrowable = t; } if (!validate) { try { destroy(p, DestroyMode.NORMAL); destroyedByBorrowValidationCount.incrementAndGet(); } catch (final Exception e) { // Ignore - validation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( "Unable to validate object"); nsee.initCause(validationThrowable); throw nsee; } } } } } updateStatsBorrow(p, System.currentTimeMillis() - waitTime); return p.getObject(); }
3. What is the use of the factory's passiveobject (pooledobject < T > P) and passiveobject (pooledobject < T > P), that is, the object activation and passivation methods?
As shown in the figure, when the object is returned to the object pool after use, if the verification fails, it is directly destroyed. If the verification passes, the object needs to be passivated first and then stored in the idle queue.
As for the method of activating the object, it will also be activated first and then taken out when accessing the object above.
Therefore, we can find objects in idle and in use. In addition to their inconsistent states, we can also add new differences between them through activation and passivation,
For example, we need to build an Elasticsearch connection pool. Each object is a connection instance with ip and port. Obviously, there are many different ip addresses accessing the es cluster, so the ip addresses accessed each time are not necessarily the same. We can assign ip and port values to the object in the activation operation, and set the ip and port to the default value or empty in the passivation operation, so the process is more standard.
public void returnObject(final T obj) { final PooledObject<T> p = allObjects.get(new IdentityWrapper<>(obj)); //.... //If verification fails, destroy it directly return //... try { factory.passivateObject(p); } catch (final Exception e1) { swallowException(e1); try { destroy(p, DestroyMode.NORMAL); } catch (final Exception e) { swallowException(e); } try { ensureIdle(1, false); } catch (final Exception e) { swallowException(e); } updateStatsReturn(activeTime); return; } //...... //Return to idle queue }
Object pool related configuration items
The object pool provides many configuration items. In the default basic object pool of GenericObjectPool we use, we can pass parameters into GenericObjectPoolConfig through the construction method. Of course, we can also see the basic class BaseObjectPoolConfig implemented at the bottom of GenericObjectPoolConfig, including the following configurations:
-
maxTotal: the maximum number of objects used in the object pool. The default value is 8
-
maxIdle: the maximum number of idle objects in an object. The default value is 8
-
minIdle: the minimum number of free objects in the object pool. The default is 8
-
lifo: whether to follow the principle of last in first out when obtaining idle instances in the object pool. The default value is true
-
blockWhenExhausted: whether to block the thread to get the instance when the object pool is in the exhausted state, that is, when the available instance is empty. The default is true
-
fairness: when the object pool is in the exhausted state, that is, when the available instances are empty, a large number of threads are blocking and waiting to obtain the available instances at the same time. fairness is configured to control whether to enable the fair lock algorithm, that is, first come, first served. The default is false. The premise of this item is that blockWhenExhausted is configured as true
-
maxWaitMillis: maximum blocking time. When the object pool is in the exhausted state, that is, the available instances are empty, a large number of threads are blocking at the same time waiting to obtain the available instances. If the blocking time exceeds maxWaitMillis, an exception will be thrown. When this value is negative, it means blocking indefinitely until it is available. The default is - 1
-
testOnCreate: whether to verify before creating an object (that is, call the validateObject() method of the factory). If the verification fails, the return of borobject () will fail. The default is false
-
Testonmirror: whether to check before fetching an object. The default value is false
-
testOnReturn: check before returning the object pool, that is, call the factory's returnObject(). If the check fails, the object will be destroyed instead of returned to the pool. The default value is false
-
Timebetween evictionrunsmillis: eviction cycle. The default value is - 1, which means no eviction test is performed
-
Testwhiteidle: whether the idle object in the idle queue is evicted by the evictor for verification. When the last running time of the object exceeds the value set by settimebetween evictionrunsmilis (long)), it will be evicted for verification. Call the validateObject() method. If the verification is successful, the object will be destroyed. The default is false
Use steps
-
Create factory class: inherit BaseGenericObjectPool or implement the basic interface PooledObjectFactory, and rewrite the creation, destruction, verification, activation and passivation methods of objects according to business requirements. Most of the destruction methods are connection closing, emptying, etc.
-
Create pool: inherit GenericObjectPool or implement the basic interface ObjectPool. It is recommended to use the former. It provides us with an idle object expulsion detection mechanism (that is, destroy the objects that have not been used for a long time in the idle queue to reduce memory consumption) and provides basic information about many objects, such as the last time the object was used Whether the object is inspected before use, etc.
-
Create pool related configuration (optional): increase the configuration control of thread pool by inheriting GenericObjectPoolConfig or BaseObjectPoolConfig. The former is recommended. It implements the basic method for us and only needs to add the required attributes.
-
Create wrapper class (optional): for the objects to exist in the object pool, add many basic attributes outside the actual objects to understand the real-time status of the objects in the object pool.
matters needing attention
Although we use the default implementation, we should also optimize it in combination with the actual production situation. We can't use thread pool, but the performance is lower.
In use, we should pay attention to the following matters:
- To set the idle queue for the object pool, the maximum and minimum values, the default maximum and minimum values, and the default maximum of 8 can not meet the needs
private volatile int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE; private volatile int minIdle = GenericObjectPoolConfig.DEFAULT_MIN_IDLE; public static final int DEFAULT_MAX_IDLE = 8; public static final int DEFAULT_MIN_IDLE = 0;
-
Set the maxWaitMillis property of the object pool, that is, the maximum waiting time of the object
-
After using the object, release the object in time and return the object to the pool. In particular, if an exception occurs, ensure the release through try.. chat.. finally to avoid occupying resources
Let's talk about the precautions. First, why do we set maxWaitMillis? We use the following methods to get objects
public T borrowObject() throws Exception { return borrowObject(getMaxWaitMillis()); }
You can see that the default maximum wait time is - 1L
private volatile long maxWaitMillis = BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS; //.... public static final long DEFAULT_MAX_WAIT_MILLIS = -1L;
Let's look at the object fetching logic again. blockWhenExhausted defaults to true, which means that when there are no idle objects in the pool, the thread will be blocked until there are new available objects.
From the above, we know that - 1L will execute idleObjects.takeFirst()
public T borrowObject(final long borrowMaxWaitMillis) throws Exception { //....... final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; final long waitTime = System.currentTimeMillis(); while (p == null) { //....... if (blockWhenExhausted) { if (p == null) { if (borrowMaxWaitMillis < 0) { p = idleObjects.takeFirst(); } else { p = idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); } } } } }
As follows, the blocking queue will block until there are free objects. This setting will cause a large area of blocking when the throughput increases
public E takeFirst() throws InterruptedException { lock.lock(); try { E x; while ( (x = unlinkFirst()) == null) { notEmpty.await(); } return x; } finally { lock.unlock(); } }
Another note is to remember to recycle resources, that is, call the public void returnObject(final T obj) method. The reason is obvious. The object pool is insensitive to whether we have used up the object. We need to call this method to recycle the object. In particular, we should ensure recycling in case of abnormalities. Therefore, the best practice is as follows:
try{ item = pool.borrowObject(); } catch(Exception e) { log.error("...."); } finally { pool.returnObject(item); }
Instance use
Example 1: implement a simple string pool
Create string factory
package com.anqi.demo.demopool2.pool.fac; import org.apache.commons.pool2.BasePooledObjectFactory; import org.apache.commons.pool2.PooledObject; import org.apache.commons.pool2.impl.DefaultPooledObject; /** * String pool factory */ public class StringPoolFac extends BasePooledObjectFactory<String> { public StringPoolFac() { super(); } @Override public String create() throws Exception { return "str-val-"; } @Override public PooledObject<String> wrap(String s) { return new DefaultPooledObject<>(s); } @Override public void destroyObject(PooledObject<String> p) throws Exception { } @Override public boolean validateObject(PooledObject<String> p) { return super.validateObject(p); } @Override public void activateObject(PooledObject<String> p) throws Exception { super.activateObject(p); } @Override public void passivateObject(PooledObject<String> p) throws Exception { super.passivateObject(p); } }
Create string pool
package com.anqi.demo.demopool2.pool; import org.apache.commons.pool2.PooledObjectFactory; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; /** * String pool */ public class StringPool extends GenericObjectPool<String> { public StringPool(PooledObjectFactory<String> factory) { super(factory); } public StringPool(PooledObjectFactory<String> factory, GenericObjectPoolConfig<String> config) { super(factory, config); } }
Test main class
First, we set setMaxTotal to 2, that is, at most two objects are taken out for use, and set setMaxWaitMillis to 3S, that is, it is blocked for 3S at most. We cycle for 3 times without releasing resources
import com.anqi.demo.demopool2.pool.fac.StringPoolFac; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class StringPoolTest { private static final Logger LOG = LoggerFactory.getLogger(StringPoolTest.class); public static void main(String[] args) { StringPoolFac fac = new StringPoolFac(); GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>(); config.setMaxTotal(2); config.setMinIdle(1); config.setMaxWaitMillis(3000); StringPool pool = new StringPool(fac, config); for (int i = 0; i < 3; i++) { String s = ""; try { s = pool.borrowObject(); LOG.info("str:{}", s); } catch (Exception e) { e.printStackTrace(); } finally { // if (!s.equals("")) { // pool.returnObject(s); // } } } } }
The results are as follows. After two successful calls, block 3S, and then the program stops with an error.
This is because the maximum available resources are 2. If they are not released, no resources will be available. The new caller will be blocked for 3S, and then a false access failure will be reported.
16:18:42.499 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
16:18:42.505 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
java.util.NoSuchElementException: Timeout waiting for idle object
We release the annotation and get the normal execution result after releasing the resources
16:20:52.384 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
16:20:52.388 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
16:20:52.388 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
reference resources: https://mp.weixin.qq.com/s/79bmLWS1N8ID1AjO91VbTQ