本文节选自《设计模式就该这样学》之享元模式(Flyweight Pattern)

1 故事背景

一个程序员就因为改了生产环境上的一个办法参数,把int型改成了Integer类型,因为波及到钱,后果上线之后公司损失惨重,程序员被解雇了。信不信持续往下看。先来看一段代码:

public static void main(String[] args) {        Integer a = Integer.valueOf(100);        Integer b = 100;        Integer c = Integer.valueOf(129);        Integer d = 129;        System.out.println("a==b:" + (a==b));        System.out.println("c==d:" + (c==d));}

大家猜它的运行后果是什么?在运行完程序后,咱们才发现有些不对,失去了一个意想不到的运行后果,如下图所示。

看到这个运行后果,有人就肯定会问,为什么是这样?之所以失去这样的后果,是因为Integer用到的享元模式。来看Integer的源码,

public final class Integer extends Number implements Comparable<Integer> {        ...        public static Integer valueOf(int i) {                        if (i >= IntegerCache.low && i <= IntegerCache.high)                                return IntegerCache.cache[i + (-IntegerCache.low)];                        return new Integer(i);        }        ...}

再持续进入到IntegerCache的源码来看low和high的值:

private static class IntegerCache {  // 最小值  static final int low = -128;  // 最大值,反对自定义  static final int high;  // 缓存数组  static final Integer cache[];  static {    // 最大值能够通过属性配置来扭转    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);        // 最大数组大小为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;    // 将low-high范畴内的值全副实例化并存入数组中当缓存应用    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() {}}

由上可知,Integer源码中的valueOf()办法做了一个条件判断,如果目标值在-128 - 127,则间接从缓存中取值,否则新建对象。其实,Integer第一次应用的时候就会初始化缓存,其中范畴最小值为-128,最大值默认是127。接着会把low至high中所有的数据初始化存入数据中,默认就是将-128 - 127总共256个数循环实例化存入cache数组中。精确的说应该是将这256个对象在内存中的地址存进数组中。这里又有人会问了,那为什么默认是-128 - 127,怎么不是-200 - 200或者是其余值呢?那JDK为何要这样做呢?

在Java API 中是这样解释的:

Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range

大抵意思是:

128~127的数据在int范畴内是应用最频繁的,为了缩小频繁创建对象带来的内存耗费,这里其实是用到了享元模式,以进步空间和工夫性能。

JDK减少了这一默认的范畴并不是不可变,咱们在应用前能够通过设置-Djava.lang.Integer.IntegerCache.high=xxx或者设置-XX:AutoBoxCacheMax=xxx来批改缓存范畴,如下图:

起初,我又找到一个比拟靠谱的解释:

实际上,在Java 5中首次引入此性能时,范畴固定为-127到+127。 起初在Java 6中,范畴的最大值映射到java.lang.Integer.IntegerCache.high,VM参数容许咱们设置高位数。 依据咱们的利用用例,它能够灵便地调整性能。 应该从-127到127抉择这个数字范畴的起因应该是什么。这被认为是宽泛应用的整数范畴。 在程序中首次应用Integer必须破费额定的工夫来缓存实例。

Java Language Specification 的原文解释如下:

Ideally, boxing a given primitive value p, would always yield an identical reference. In practice, this may not be feasible using existing implementation techniques. The rules above are a pragmatic compromise. The final clause above requires that certain common values always be boxed into indistinguishable objects. The implementation may cache these, lazily or eagerly. For other values, this formulation disallows any assumptions about the identity of the boxed values on the programmer's part. This would allow (but not require) sharing of some or all of these references.
This ensures that in most common cases, the behavior will be the desired one, without imposing an undue performance penalty, especially on small devices. Less memory-limited implementations might, for example, cache all char and short values, as well as int and long values in the range of -32K to +32K.

2 对于Integer和int的比拟

1) 因为Integer变量实际上是对一个Integer对象的援用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

Integer i = new Integer(100);Integer j = new Integer(100);System.out.print(i == j); //false

2) Integer变量和int变量比拟时,只有两个变量的值是向等的,则后果为true(因为包装类Integer和根本数据类型int比拟时,java会主动拆包装为int,而后进行比拟,实际上就变为两个int变量的比拟)

Integer i = new Integer(100);int j = 100;System.out.print(i == j); //true

3) 非new生成的Integer变量和new Integer()生成的变量比拟时,后果为false。(因为 ①当变量值在-128 - 127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值在-128 - 127之间时,非new生成Integer变量时,java API中最终会依照new Integer(i)进行解决(参考上面第4条),最终两个Interger的地址同样是不雷同的)

Integer i = new Integer(100);Integer j = 100;System.out.print(i == j); //false

4) 对于两个非new生成的Integer对象,进行比拟时,如果两个变量的值在区间-128到127之间,则比拟后果为true,如果两个变量的值不在此区间,则比拟后果为false

3 扩大常识

在JDK中,这样的利用不止int,以下包装类型也都利用了享元模式,对数值做了缓存,只是缓存的范畴不一样,具体如下表所示:

根本类型大小最小值最大值包装器类型缓存范畴是否反对自定义
boolean---Bloolean--
char6bitUnicode 0Unic ode 2(16)-1Character0~127
byte8bit-128+127Byte-128~127
short16bit-2(15)2(15)-1Short-128~127
int32bit-2(31)2(31)-1Integer-128~127反对
long64bit-2(63)2(63)-1Long-128~127
float32bitIEEE754IEEE754Float-
double64bitIEEE754IEEE754Double-
void---Void--
大家感觉这个锅背得值不值?

4 应用享元模式实现数据库连接池

再举个例子,咱们常常应用的数据库连接池,因为应用Connection对象时次要性能耗费在建设连贯和敞开连贯的时候,为了进步Connection对象在调用时的性能,将Connection对象在调用前创立好并缓存起来,在用的时候间接从缓存中取值,用完后再放回去,达到资源重复利用的目标,代码如下。

public class ConnectionPool {    private Vector<Connection> pool;    private String url = "jdbc:mysql://localhost:3306/test";    private String username = "root";    private String password = "root";    private String driverClassName = "com.mysql.jdbc.Driver";    private int poolSize = 100;public ConnectionPool() {        pool = new Vector<Connection>(poolSize);        try{            Class.forName(driverClassName);            for (int i = 0; i < poolSize; i++) {                Connection conn = DriverManager.getConnection(url,username,password);                pool.add(conn);            }        }catch (Exception e){            e.printStackTrace();        }    }    public synchronized Connection getConnection(){        if(pool.size() > 0){            Connection conn = pool.get(0);            pool.remove(conn);            return conn;        }        return null;    }    public synchronized void release(Connection conn){        pool.add(conn);}}

这样的连接池,广泛利用于开源框架,能够无效晋升底层的运行性能。

【举荐】Tom弹架构:珍藏本文,相当于珍藏一本“设计模式”的书

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我高兴!
如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源。关注微信公众号『 Tom弹架构 』可获取更多技术干货!