对象池
原文:https://blog.csdn.net/efine_dxq/article/details/70230383
单例模式是限制了一个类只能有一个实例,对象池模式则是限制一个类实例的个数。对象池类就像是一个对象管理员,它以Static列表(也就是装对象的池子)的形式存存储某个实例数受限的类的实例,每一个实例还要加一个标记,标记该实例是否被占用。当类初始化的时候,这个对象池就被初始化了,实例就被创建出来。然后,用户可以向这个类索取实例,如果池中所有的实例都已经被占用了,那么抛出异常。用户用完以后,还要把实例“还”回来,即释放占用。对象池类的成员应该都是静态的。用户也不应该能访问池子里装着的对象的构造函数,以防用户绕开对象池创建实例。数据库连接的管理就是对象池的应用。比如,每个用户的连接数是有限的,这样每个连接就是一个池子里的一个对象,“连接池”类就可以控制连接数了。
Java对象的生命周期分析
Java对象的生命周期大致包括三个阶段:对象的创建,对象的使用,对象的清除。因此,对象的生命周期长度可用如下的表达式表示:T = T1 + T2 +T3。其中T1表示对象的创建时间,T2表示对象的使用时间,而T3则表示其清除时间。由此,我们可以看出,只有T2是真正有效的时间,而T1、T3则是对象本身的开销。下面再看看T1、T3在对象的整个生命周期中所占的比例。
我们知道,Java对象是通过构造函数来创建的,在这一过程中,该构造函数链中的所有构造函数也都会被自动调用。另外,默认情况下,调用类的构造函数时,Java会把变量初始化成确定的值:所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和double变量设置成0.0,逻辑值设置成false。所以用new关键字来新建一个对象的时间开销是很大的,如表1所示。
表1 一些操作所耗费时间的对照表
运算操作 | 示例 | 标准化时间 |
---|---|---|
本地赋值 | i = n | 1.0 |
实例赋值 | this.i = n | 1.2 |
方法调用 | Funct() | 5.9 |
新建对象 | New Object() | 980 |
新建数组 | New int[10] | 3100 |
从表1可以看出,新建一个对象需要980个单位的时间,是本地赋值时间的980倍,是方法调用时间的166倍,而若新建一个数组所花费的时间就更多了。
再看清除对象的过程。我们知道,Java语言的一个优势,就是Java程序员勿需再像C/C++程序员那样,显式地释放对象,而由称为垃圾收集器(Garbage Collector)的自动内存管理系统,定时或在内存凸现出不足时,自动回收垃圾对象所占的内存。凡事有利总也有弊,这虽然为Java程序设计者提供了极大的方便,但同时它也带来了较大的性能开销。这种开销包括两方面,首先是对象管理开销,GC为了能够正确释放对象,它必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。其次,在GC开始回收“垃圾”对象时,系统会暂停应用程序的执行,而独自占用CPU。
因此,如果要改善应用程序的性能,一方面应尽量减少创建新对象的次数;同时,还应尽量减少T1、T3的时间,而这些均可以通过对象池工作来实现。
池工作是典型的空间换时间
Java中常见的池有对象池、线程池、连接池。
为什么要使用池
无论对象池、线程池还是连接池,它们大都先将一些创建好的对象缓存起来,放入池(容器)中。等到要使用的时候,是从池中取出,而不是新创建。这样,很大程度上能减少创建对象和销毁对象的时间开销,同时能使对象得到复用,提高了应用程序性能。
哪些对象需要使用池存储
随着计算机性能在各方面的提高,创建一个新的对象已经不像过去那样昂贵了。然而,有些对象,还是需要创建管理这些对象的池,使得这些对象能够动态的重用,而客户端代码也不用关心它们的生命周期,这样对提高应用程序性能还是有很大改善的。
-
创建的开销还是十分大的对象,比如线程、网络连接(数据库连接或socket连接)等一些重量级的对象。
-
使用过于频繁的对象,比如包装类对象。
1 2 3 4 5 6 7 8 9 10
class Demo { public static void main(String[] args) { System.out.println(Integer.valueOf(127) == Integer.valueOf(127)); System.out.println(Integer.valueOf(128) == Integer.valueOf(128)); } } // 运行结果 true false
Java中常见的池
- 对象池:包装类、Netty的缓冲区的创建
- 连接池:数据库连接、客户端创建socket连接,一般为长连接
- 线程池:JDK中线程的创建
对象池
-
包装类对象的缓存原理,以Integer类型对象的实现说明 在Java中,数据类型可以分为两大类,基本数据类型和引用数据类型,基本数据类型的数据不是对象,所以对于要将基本数据类型作为对象来使用的情况,需要使用其相对应的包装类。基础数据类型与包装数据类型相互转换通过包装类的自动装箱和拆箱机制来实现的。JDK5引入的自动装箱和拆箱就是编译器来依据我们编写的代码,决定是否进行装箱和拆箱动作,即无需使用valueOf()和intValue()等方法。自动装箱过程其实时调用了valueOf()的方法。
-
valueOf()方法的源码
1 2 3 4 5 6
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
从上面源代码可知:Java内部为了节省内存,IntegerCache类中有一个数组缓存了值从-128到127之间的对象。当我们调用Integer.valueOf(int i)的时候,如果i的值是介于-128和127之间的,会直接从这个缓存中返回一个对象,否则就new一个新的Integer对象。其他包装类原理类似,就不一一说明了。
使用过程中的注意事项
使用过程中的注意事项
- JDK版本,不要使用JDK5以下的版本,因为包装类的自动装箱和拆箱机制是JDK5才引进的,低版本的JDK创建一个包装类对象还是需要通过new Integer(int i)方法创建。
- 缓存对象的范围,Integer类只缓存了-128到127之间的Integer对象,其他数据范围是通过new Integer(int i)方法创建的。
- 对象回收
Integer i = 100, i = null;
这里的代码不会有对象符合垃圾回收器的条件,这儿的i虽然被赋予null,但是它之前指向的是cache中的Integer对象,而cache没有被赋null,所以Integer(100)这个对象依然存在。而如果在-128和127之外,则它所指向的对象将符合垃圾回收的条件。
连接池
目前常用的数据库连接池有阿里的Druid、SpringBoot中默认的Tomcat连接池、高性能的HikariCP、Apache的DBCP、Hibernate开发组推荐C3P0、还有部分公司使用的开源连接池Proxool,虽然连接池种类众多,但大部分原理都是类似的,以我们常用的阿里Druid使用来说明:
配置考虑
- 初始化连接:系统启动时创建连接个数,可根据DB规模来考虑
- 最小连接数:可考虑该值的设置和初始化连接保持一致
- 最大连接数:对于有较大DB规模的,最大连接不要设置过大,避免本地维护的db太大。如果对应的数据源并发数过高,可考虑增大最大连接的数量。
- 连接的超时时间:如果连接全部被占用,需要等待的时间。可以根据当前系统的响应时间判定,如果容忍度较高,可以大一点。容忍度较低,则设置小一点
- 连接有效性检测时间:该值需要结合数据库的wait timeout, interactive_timeout值进行设置。加入数据库为120s,则心跳检测时间在120s以内越大越好。如果太小,心跳检测时间会比较频繁。建议设置为90s
- 最大空闲时间:如果连接超过该时间没有使用过,则会进行close。该值不要太小,避免频繁的建立和关闭连接。也不要设置太大,导致一直无法关闭。
driud具体配置
介绍: https://github.com/alibaba/druid
推荐配置:
initialSize | 10 | 初始化配置 |
---|---|---|
minldle | 10 | 最小连接数 |
maxActive | 20 | 最大连接数 |
maxWait | 2000 | 连接超时时间(ms) |
timeBetweenEvictionMillis | 90000 | 连接有效性检测时间(ms) |
testOnBorrow | False | 获取连接检测 |
testOnReturn | False | 归还连接检测 |
minEvictableTimeMillis | 1800000 | 最大空闲时间(ms) |
testWhileldle | True | 在获取连接后,确定是否要进行连接空闲时间的检查 |
配置说明
- minEvictableTimeMillis最大空闲时间:默认时30分钟,配置里面不需要设置
- testOnBorrow和testOnReturn默认为关闭,可以不配置
- testWhileldle在获取连接后,确定是否要进行连接空闲时间的检查。默认为True,配置里面不需要进行设置
流程说明
- 在第一次调用connection的时候,才会进行initialSize的初始化
- 心跳检测时间线程,会休眠timeBetweenEvictionRunsMillis时间,然后只对没有borrow的线程减去mindle的线程进行检查,如果空闲时间大于minEvictableTimeMillis则进行close
- testWhileldle必须设置为True,在获取连接后,先检查testOnBorrow,然后再判定testWhileldle,如果连接空闲时间大于timeBetweenEvictionMillis,则会进行心跳检测
- 不需要配置validationQuery,如果不配置的情况下会走ping命令,性能更高
- 连接保存在数组里面,获取连接的时候,获取数组的最后一位。在timeBetweenEvictionMillis时是从前往后检查连接的有效性
注意事项
-
使用了连接,一旦释放掉,这里的释放指的不是关闭数据库连接,而是归还到池中。在某个子项目中就遇到类似问题,由于没释放,导致连接数不断增大,出现内存溢出的现象
-
尽量使用连接池中的连接,不要通过new Connection()方法创建连接,这样会有内存溢出的风险
-
数据库事务时间不要过长,不仅会造成事务执行时间超长,而且也会严重降低并发能力。那么我们在用事务的时候,遵循的原则是快进快出,事务代码尽量小
1 2 3 4 5 6 7 8 9 10
public void test() { Transaction.begin();//开启事务 try { do.insert(); //插入一条记录 httpClient.query();//请求访问 Transaction.commit(); //事务提交 } catch (Exception e) { Transaction.rollback(); //事务回滚 } }
其中使用httpClient组件可能导致事务执行时间过长,应该独立出来使用。
线程池
线程池的创建
JDK中提供了工具类Executors用来创建不同类型的线程池
- 固定大小的线程池
|
|
特性:
- 线程池中核心线程个数和最大线程个数一致,队列使用的是LinkedBlockingDeque,那么大小没有限制。
- 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值,新的任务将会放在队列里面
-
单一线程的线程池
1 2 3 4 5 6
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService( new ThreadPoolExecutor(1, 1, 0l, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>()) ); }
- 线程池中核心线程个数和最大线程个数都为1,队列使用的是LinkedBlockQueue,那么大小没限制
- 此线程池保证所有任务的执行顺序按照任务的提交顺序执行
-
可缓存的线程池
- 如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60s不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程类处理任务
- 此线程池不会对线程池的大小做限制,线程池大小完全依赖于JVM能够创建的最大线程数
-
从上面可以看出,线程池创建都是依赖于其核心类ThreadPoolExecutor
1 2 3 4 5 6 7 8
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
创建一个线程池需要输入几个参数:
- corePoolSize 核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,及时出现了线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的空闲线程处理。
- maximumPoolSize 当线程数大于或者等于核心线程数,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maximumPoolSize,如果线程数已等于该值,且任务队列已满,则已经超出线程池的处理能力,线程池会拒绝处理任务而抛异常
- keepAliveTime 线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize
- TimeUnit 线程活动保持的时间单位,可选分钟,毫秒,秒等
- workQueue 当线程个数达到核心线程个数时,用于保存等待执行的任务的阻塞队列,可以选择以下几个阻塞队列: 1)ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序 2)LinkedBlockingDeque:一个基于链表结构的阻塞队列,此队列按FIFO原则对元素进行排序,吞吐量通常要高于ArrayBlockingQueue 3)SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作
- RejectedExecutionHandler 拒绝策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务
线程池的主要处理流程
注意事项
- 尽量使用线程池创建线程,不要使用工具类创建线程池,而使用其核心类ThreadPoolExecutor创建线程池
- 线程池中队列尽量不要使用无界队列LinkedBlockingDeque,可以看到采用无界队列,也就是说队列可以无限的存放可执行的任务,造成大量对象无法释放和回收,在某应用子系统中遇到过队列大小没限制,导致内存溢出现象
对象池工作的示例代码
将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。 并非所有对象都适合拿来池化――因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。但是对于生成时开销可观的对象,池化工作就是提高性能的有效策略了。下面是构建对象池的一个例子:
|
|
commons-pool
ref:https://blog.csdn.net/liuxiao723846/article/details/78881040
ObjectPool定义了一个简单的池化接口,有三个对应实现 GenericObjectPool:实现了可配置的后进先出或先进先出(LIFO/FIFO)行为,默认是作为一个后进先出队列,这意味当对象池中有可用的空闲对象时,borrowObject 将返回最近的对象实例,如果将lifo 属性设置为false,则按FIFO行为返回对象实例。 StackObjectPool :实现了后进先出(LIFO)行为。 SoftReferenceObjectPool: 实现了后进先出(LIFO)行为。另外,对象池还在SoftReference 中保存了每个对象引用,允许垃圾收集器针对内存需要回收对象。
KeyedObjectPool定义了一个以任意的key访问对象的接口(可以池化对种对象),有两种对应实现。 GenericKeyedObjectPool :实现了先进先出(FIFO)行为。 StackKeyedObjectPool : 实现了后进先出(LIFO)行为。
PoolableObjectFactory 定义了池化对象的生命周期方法,我们可以使用它分离被池化的不同对象和管理对象的创建,持久,销毁。 BasePoolableObjectFactory这个实现PoolableObjectFactory 接口的一个抽象类,我们可用扩展它实现自己的池化工厂。
一个对象池使用的简单例子:
|
|