享元模式:Integer缓存池、String常量池、线程池的享元实现原理
享元模式:Integer缓存池、String常量池、线程池的享元实现原理
适读人群:中高级Java开发者 | 阅读时长:约22分钟 | 模式类型:结构型
开篇故事
有一道经典的 Java 面试题,我见过的候选人有80%都会答错:
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b); // true 还是 false?
System.out.println(c == d); // true 还是 false?答案是 true 和 false。很多人觉得奇怪:为什么 a == b 是 true,而 c == d 是 false?
这就是 Java 内置的享元模式实现——Integer 类内部有一个缓存(IntegerCache),对于 -128 到 127 之间的整数,Integer.valueOf() 会直接返回缓存中已有的对象,而不是每次都 new Integer()。
a == b 是 true,是因为 100 在缓存范围内,a 和 b 指向同一个缓存对象;c == d 是 false,是因为 200 超出了缓存范围,每次都创建新对象,所以引用不同。
这个面试题背后是享元模式的核心思想:运用共享技术来有效支持大量细粒度对象的复用,减少内存占用和对象创建开销。
一、模式动机:对象太多导致的内存压力
享元模式(Flyweight Pattern)的适用场景:
- 系统中有大量相似对象,占用了大量内存
- 这些对象中有大量重复的状态可以共享
- 对象的身份(对象引用)不重要,重要的是它所包含的数据
享元模式将对象的状态分为:
- 内部状态(Intrinsic State):可以共享的、不随环境变化的状态
- 外部状态(Extrinsic State):不能共享的、随环境变化的状态(由调用者传入)
二、模式结构
三、Java 中的享元模式源码分析
3.1 Integer 缓存池(IntegerCache)
// Integer.IntegerCache 完整源码
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
static {
// JVM参数可以调整缓存的高位边界
// -XX:AutoBoxCacheMax=<size> 或 -Djava.lang.Integer.IntegerCache.high=<size>
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
h = Math.min(h, Integer.MAX_VALUE - (-low) - 1);
} catch (NumberFormatException nfe) {
// 忽略非法配置
}
}
high = h;
// 初始化缓存数组
cache = new Integer[(high - low) + 1];
int j = low;
for (int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
// Integer.valueOf() 的享元工厂方法
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; // 返回缓存对象
return new Integer(i); // 超出范围才new
}同样的缓存机制在 Long(-128 到 127)、Short(-128 到 127)、Byte(全部缓存,因为 Byte 只有 256 个值)中也存在。
但 Double 和 Float 没有缓存,因为小数有无限多个值,不可能预先缓存。
3.2 String 常量池
// String的intern()方法:将字符串放入/获取常量池中的引用
String s1 = new String("hello"); // 堆上的对象,不在常量池
String s2 = "hello"; // 字面量,在常量池
System.out.println(s1 == s2); // false,s1在堆,s2在常量池
System.out.println(s1.intern() == s2); // true,intern()返回常量池中的引用
// 字符串拼接的享元行为
String a = "hel" + "lo"; // 编译期常量折叠,等同于 "hello",在常量池
String b = "hello"; // 同一个常量池对象
System.out.println(a == b); // true
String prefix = "hel";
String c = prefix + "lo"; // 运行时拼接(prefix不是final常量),结果在堆上
System.out.println(c == b); // false3.3 线程池——最重要的享元实现
线程是重量级资源,线程池通过享元思想复用线程:
// ThreadPoolExecutor 的核心线程管理逻辑(简化版)
public class ThreadPoolExecutor extends AbstractExecutorService {
// 核心工作线程集合(享元对象池)
private final HashSet<Worker> workers = new HashSet<>();
// 任务提交:复用已有线程,避免频繁创建销毁
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
int c = ctl.get();
// 如果线程数 < corePoolSize,创建新线程(核心线程是"永久"的享元对象)
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) return;
}
// 尝试将任务放入队列(线程忙时排队,而不是创建新线程)
if (isRunning(c) && workQueue.offer(command)) {
// 任务入队成功
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command)) reject(command);
else if (workerCountOf(recheck) == 0) addWorker(null, false);
}
// 队列也满了,尝试创建非核心线程(临时享元)
else if (!addWorker(command, false)) {
reject(command); // 线程数达到maximumPoolSize,触发拒绝策略
}
}
// Worker(工作线程):享元对象,可以复用执行多个任务
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread; // 实际的线程
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
@Override
public void run() {
runWorker(this);
}
}
// Worker不断从任务队列中取任务执行(复用线程)
final void runWorker(Worker w) {
Runnable task = w.firstTask;
w.firstTask = null;
while (task != null || (task = getTask()) != null) { // 循环取任务
try {
task.run(); // 执行任务
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
// 没有任务了,线程销毁(如果是非核心线程)
}
}3.4 数据库连接池——另一个典型享元
HikariCP 连接池中,Connection 对象是享元:
// HikariCP的连接包装(ProxyConnection)
// 调用close()不是真正关闭连接,而是将连接归还到池中
public final class ProxyConnection implements Connection {
private final PoolEntry poolEntry;
@Override
public void close() throws SQLException {
// 不是真正关闭,而是归还到连接池
if (delegate != ClosedConnection.CLOSED_CONNECTION) {
leakTask.cancel();
try {
if (!isAutoCommit && !isReadOnly && dirtyBits != 0) {
delegate.rollback(); // 归还前先回滚未提交的事务
}
resetConnectionState(this, delegate, dirtyBits); // 重置状态
} finally {
delegate = ClosedConnection.CLOSED_CONNECTION;
poolEntry.recycle(lastAccess); // 归还连接到池
}
}
}
}四、生产级代码实现:自定义对象池
4.1 通用对象池实现
/**
* 通用对象池(享元模式的生产级实现)
* 适用于:创建开销大、可复用的对象(如数据库连接、HTTP客户端、大型字典等)
*/
@Slf4j
public class ObjectPool<T> implements Closeable {
private final BlockingDeque<T> available; // 可用对象队列
private final Set<T> allObjects; // 所有对象(用于检测泄漏)
private final Supplier<T> factory; // 对象工厂
private final Consumer<T> resetter; // 对象归还时的重置操作
private final Predicate<T> validator; // 对象有效性检查
private final Consumer<T> destroyer; // 对象销毁操作
private final int maxSize;
private final long acquireTimeoutMs;
private final AtomicInteger totalCreated = new AtomicInteger(0);
private volatile boolean closed = false;
public ObjectPool(PoolConfig config, Supplier<T> factory,
Consumer<T> resetter, Predicate<T> validator,
Consumer<T> destroyer) {
this.maxSize = config.getMaxSize();
this.acquireTimeoutMs = config.getAcquireTimeoutMs();
this.factory = factory;
this.resetter = resetter;
this.validator = validator;
this.destroyer = destroyer;
this.available = new LinkedBlockingDeque<>(maxSize);
this.allObjects = Collections.newSetFromMap(new ConcurrentHashMap<>());
// 预创建最小数量的对象
for (int i = 0; i < config.getMinSize(); i++) {
T obj = createObject();
if (obj != null) {
available.offer(obj);
}
}
log.info("ObjectPool initialized: minSize={}, maxSize={}", config.getMinSize(), maxSize);
}
/**
* 从池中获取对象(享元工厂方法)
*/
public T acquire() throws InterruptedException {
if (closed) throw new IllegalStateException("Pool is closed");
// 先从可用队列取
T obj = available.poll();
if (obj != null) {
// 检查对象是否仍然有效(比如连接是否还活着)
if (validator.test(obj)) {
return obj;
} else {
// 无效对象,销毁并尝试创建新的
destroyObject(obj);
obj = null;
}
}
// 队列为空,如果未达到最大数量则创建新对象
if (totalCreated.get() < maxSize) {
obj = createObject();
if (obj != null) return obj;
}
// 等待可用对象(带超时)
obj = available.poll(acquireTimeoutMs, TimeUnit.MILLISECONDS);
if (obj == null) {
throw new RuntimeException("Object pool exhausted, timeout after " + acquireTimeoutMs + "ms");
}
// 再次验证
if (!validator.test(obj)) {
destroyObject(obj);
throw new RuntimeException("Pool returned invalid object");
}
return obj;
}
/**
* 归还对象到池中
*/
public void release(T obj) {
if (obj == null) return;
if (closed) {
destroyObject(obj);
return;
}
try {
resetter.accept(obj); // 重置对象状态
available.offer(obj); // 归还到可用队列
} catch (Exception e) {
log.warn("Error resetting pooled object, destroying it: {}", e.getMessage());
destroyObject(obj);
}
}
/**
* try-with-resources 支持(借用模式)
*/
public PooledObject<T> borrow() throws InterruptedException {
T obj = acquire();
return new PooledObject<>(obj, this);
}
private T createObject() {
try {
T obj = factory.get();
if (obj != null) {
allObjects.add(obj);
totalCreated.incrementAndGet();
log.debug("Created pooled object #{}", totalCreated.get());
}
return obj;
} catch (Exception e) {
log.error("Failed to create pooled object: {}", e.getMessage());
return null;
}
}
private void destroyObject(T obj) {
allObjects.remove(obj);
totalCreated.decrementAndGet();
try {
destroyer.accept(obj);
} catch (Exception e) {
log.warn("Error destroying pooled object: {}", e.getMessage());
}
}
@Override
public void close() {
closed = true;
// 销毁所有对象
T obj;
while ((obj = available.poll()) != null) {
destroyObject(obj);
}
log.info("ObjectPool closed. Created: {}", totalCreated.get());
}
public int getAvailableCount() { return available.size(); }
public int getTotalCount() { return totalCreated.get(); }
/**
* 可借用对象包装(支持 try-with-resources)
*/
public static class PooledObject<T> implements AutoCloseable {
private final T object;
private final ObjectPool<T> pool;
PooledObject(T object, ObjectPool<T> pool) {
this.object = object;
this.pool = pool;
}
public T get() { return object; }
@Override
public void close() {
pool.release(object);
}
}
}
/**
* 使用示例:重用 Elasticsearch RestHighLevelClient
*/
@Configuration
public class EsClientPoolConfig {
@Bean
public ObjectPool<RestHighLevelClient> esClientPool() {
PoolConfig config = PoolConfig.builder()
.minSize(5)
.maxSize(20)
.acquireTimeoutMs(5000)
.build();
return new ObjectPool<>(
config,
() -> {
// 工厂:创建ES客户端
RestClientBuilder builder = RestClient.builder(
new HttpHost("localhost", 9200, "http")
);
return new RestHighLevelClient(builder);
},
client -> {
// 重置:ES客户端无状态,无需重置
},
client -> {
// 验证:检查ES连接是否正常
try {
return client.ping(RequestOptions.DEFAULT);
} catch (Exception e) {
return false;
}
},
client -> {
// 销毁:关闭客户端
try {
client.close();
} catch (IOException e) {
// ignore
}
}
);
}
}
// 使用示例:借用ES客户端执行查询
@Service
public class EsSearchService {
@Autowired
private ObjectPool<RestHighLevelClient> esClientPool;
public SearchHits search(SearchRequest request) {
try (ObjectPool.PooledObject<RestHighLevelClient> pooled = esClientPool.borrow()) {
SearchResponse response = pooled.get().search(request, RequestOptions.DEFAULT);
return response.getHits();
} catch (Exception e) {
throw new SearchException("ES search failed", e);
}
}
}五、踩坑实录
坑一:Integer 的 == 比较陷阱
最常见的坑,已经在开篇讲了。工程建议:永远用 equals() 比较 Integer 值,不要依赖缓存对象的 == 相等性。== 对引用类型是比较引用地址,不是比较值。
坑二:String.intern() 的滥用
String.intern() 可以减少重复字符串的内存占用,但滥用会造成 JVM 方法区(Metaspace)内存溢出。常量池的大小是有限制的,如果把大量动态生成的字符串(如用户输入、数据库读出的数据)都放进常量池,会把常量池撑爆。
intern() 只适用于那些数量有限且高度重复的字符串,比如状态码、枚举值字符串、固定格式的 key。
坑三:线程池忘记 shutdown() 导致 JVM 无法退出
线程池中的非 Daemon 线程会阻止 JVM 正常退出。如果在应用代码里创建了线程池,忘记在应用关闭时 shutdown(),JVM 进程会一直挂起。
在 Spring 应用里,推荐使用 ThreadPoolTaskExecutor(它实现了 DisposableBean,Spring 关闭时会自动 shutdown()),或者手动注册 ShutdownHook:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}));六、总结
享元模式在 Java 世界无处不在:
Integer.valueOf()的 -128~127 缓存String常量池- 各种连接池、线程池
Boolean.TRUE/Boolean.FALSE的两个单例Byte,Short,Long的缓存机制
核心原则:识别哪些状态可以共享(内部状态),哪些状态需要外部传入(外部状态),然后把可共享的状态提取到享元对象中统一管理。
在自己的业务代码中,当发现有大量相似对象时,可以考虑用享元模式:维护一个对象池(Map 或队列),按 key 缓存对象,避免重复创建。但要注意:享元对象必须是线程安全的(因为会被多个线程共享),如果有状态,要么是只读状态,要么做好同步。
