[享学Eureka] 三十三、Eureka内置公用小工具:StringCache及详解String#intern()

上坡的路都是累的设置难受的,要控制好自己的情绪。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

上篇文章分享了Eureka内置的一个小工具RateLimiter限流器后,本文继续分享其另外一个实用小工具:StringCache。这两工具均是与业务代码无关(也就是和Eureka本身无关),仅依赖于JDK,因此具有最好的移植性(代码复制一下就可以开干),可以任意地方随意使用。

关于字符串缓存,一直就是个热门以及高性能下必须考虑的问题,如String#intern()的使用可不能太过于随意,本文会一并尝试讨论。


正文

StringCache是Eureka 自己实现的对字符串的缓存,在这之前我们先来认识认识String#intern()


详解String#intern()

在 JAVA 语言中有8种基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。

8种基本类型(byte、short、int、long、float、double、boolean、char)的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中

String#intern()源码:

String:

	// 它是个native方法 -> 用c/c++实现
	public native String intern();

这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。

小总结一下,String#intern()方法能有如下特点:

  • 字符串常量池能带来速度更快更节省内存的好处(不用一个字符串存储多份了嘛)
  • 非双引号声明的 String 对象,需要使用String#intern()方法,将字符串存储到字符串常量池

既然已经有这个系统级的缓存可用了,那为何Eureka还要多此一举,自己搞个StringCache呢?这就需要继续深入String#intern(),了解它的不足了。


源码(native代码)参阅

因为它的源码是c编写,因此以下部分引用自“美团技术团队”,欢迎关注其官方公众号,内容非常的干。

... // 假设有源码

细节说明:在 jdk7后,oracle 接管了 JAVA 的源码后就不对外开放了,根据 jdk 的主要开发人员声明 openJdk7 和 jdk7 使用的是同一分主代码,只是分支代码会有些许的变动。所以可以直接跟踪 openJdk7 的源码来探究 intern 的实现。

它的大体实现结构就是:JAVA 使用 jni 调用c++实现的StringTable(说明:此类理应是每个Java Coder多少应该了解一些的哦)的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的,只是不能自动扩容,默认大小是1009

到这:似乎已经找到了StringTable/String#intern()的不足之处了吧~

要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。

在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:

-XX:StringTableSize=99991

代码示例

相信每个人都做过这样一道面试题:String s = new String("abc")这个语句创建了几个对象?这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。

@Test
public void fun1() {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

请注意这个结果在JDK6和JDK7中的结果是不一样的。其实关于intern()这部分代码最初来自于美团点评,后来天下被各种抄袭,此处我不重复了,请您参考美团点评的原著:深入解析String#intern

这里我只提醒各位自己做试验的时候,在切换JDK时候的注意事项:

在这里插入图片描述
如果你项目应用的JDK是1.8,但你只是在这调整了语言级别,那么你可能是看不到效果的,因为它只管编译,运行实际还是JDK8。正确做法应该是:

在这里插入图片描述
在这里插入图片描述
只有这么做,才能确保结果是你想要的。


Eureka中的StringCache

知晓了String#intern()的StringTable不能动态扩容的局限性之后,不难理解Eureka为何要搞个StringCache了吧。官方Javadoc解释:用于替代String#intern(),它没有容量限制(with no capacity constraints)。

StringCache顾名思义它表是字符串缓存,对于那些并不需要动态性的字符串值,就可以使用该缓存,以避免每次都新开辟空间而浪费内存。比如eureka里的appName、appGroupName、vipAddress、secureVipAddress等等都会使用它来缓存。

public class StringCache {
	
	// 缓存的字符串的最大长度:超过38个我就不缓存你了
	public static final int LENGTH_LIMIT = 38;
	
	// 懒汉式
	private static final StringCache INSTANCE = new StringCache();

	// 读写锁
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    // 底层使用的WeakHashMap做的缓存,value是个WeakReference若引用
    private final Map<String, WeakReference<String>> cache = new WeakHashMap<String, WeakReference<String>>();
    // 默认值是LENGTH_LIMIT 
    private final int lengthLimit;

	
	// 虽然提供了一个INSTANCE实例,但是它的构造器仍旧是公开的
    public StringCache() {
        this(LENGTH_LIMIT);
    }
    public StringCache(int lengthLimit) {
        this.lengthLimit = lengthLimit;
    }

}

该字符串缓存底层使用的是WeakHashMap作为存储,因此它可以动态扩容,是没有存储限制的。


使用方法

它既提供了一个static的INSTANCE实例,并且构造器却又都是public的,因此可想而知它是支持两种使用方式的,但底层归口是同一个。

StringCache:

    public String cachedValueOf(final String str) {
    	// 若字符串超过长度了(默认38),就不予以缓存了
    	// 当然若你设置lengthLimit < 0的值,代表缓存所有的串
    	// 注意:""  "  "这种空串也都是会缓存的哦
        if (str != null && (lengthLimit < 0 || str.length() <= lengthLimit)) {
            try {
            	// 上读锁,看看缓存里是否有当前字符串,有就直接return
                lock.readLock().lock();
                WeakReference<String> ref = cache.get(str);
                if (ref != null) {
                    return ref.get();
                }
            } finally {
                lock.readLock().unlock();
            }

            // 若缓存里没有,就写进缓存里去
            // 注意:每个value都是一个new WeakReference<>(str)
            // 弱引用:当gc时,无论内存是否充足,都会回收被弱引用关联的对象
            try {
                lock.writeLock().lock();
                WeakReference<String> ref = cache.get(str);
                if (ref != null) {
                    return ref.get();
                }
                cache.put(str, new WeakReference<>(str));
            } finally {
                lock.writeLock().unlock();
            }
            return str;
        }
        return str;
    }

	// 缓存大小
    public int size() {
        try {
            lock.readLock().lock();
            return cache.size();
        } finally {
            lock.readLock().unlock();
        }
    }

逻辑非常的简单,优点是利用了弱引用,这样只要GC发生时,无论内存是否充足都会回收,对内存友好。

参考文章:Java中的引用类型(强引用、软引用、弱引用、虚引用)介绍,示例WeakHashMap的使用【享学Java】

另外StringCache还提供了一个静态方法,方便使用(大部分情况均是使用此静态方法完成缓存工作):

StringCache:

    public static String intern(String original) {
        return INSTANCE.cachedValueOf(original);
    }

代码示例

@Test
public void fun1() {
    String str1 = new String("YoutBatman");
    String str2 = new String("YoutBatman");
    System.out.println(str1 == str2);


    StringCache cache = new StringCache();
    String cachedStr1 = cache.cachedValueOf(str1);
    String cachedStr2 = cache.cachedValueOf(str2);
    System.out.println(cachedStr1 == cachedStr2);
}

运行程序,控制台输出:

false
true

在实际使用过程中,对于不怎么动态变化的我们一般这么赋值:

@Test
public void fun2() {
    String appName = StringCache.intern("YoutBatman");
}

对比Hystrix里的InternMap缓存

不仅是Eureka里,在Hystrix里也是有一个类似的用户缓存字符串的类的,它叫InternMap

InternMap:

	private final ConcurrentMap<K, V> storage = new ConcurrentHashMap<K, V>();
	...

    public V interned(K key) {
        V existingKey = storage.get(key);
        V newKey = null;
        if (existingKey == null) {
            newKey = valueConstructor.create(key);
            existingKey = storage.putIfAbsent(key, newKey);
        }
        return existingKey != null ? existingKey : newKey;
    }

    public int size() {
        return storage.size();
    }

它直接使用ConcurrentMap进行缓存来使用,相对来说并没有StringCache那样使用WeakHashMap + 读写锁ReadWriteLock的方式来得那么高效和对内存友好,属于低配版。


总结

关于Eureka内置公用小工具:StringCache及详解String#intern()就先介绍这,本文其实是有难点的,那边是对String#intern()的深入理解,建议有兴趣者看美团点评的那篇技术博客,并且一定一定要自己敲代码做试验,否则你一定扭头就忘。

使用建议:建议使用StringCache代替String#intern()InternMap(提示:若你工程没有引入Eureka Client做依赖,你大可以把这个类拷贝到你项目内来使用它)。
分隔线

声明

原创不易,码字更不易,感谢关注。分享本文到你的朋友圈是被授权的,但拒绝抄袭。【左边扫码加我wx / wx号:fsx641385712】,邀你加入 【Java高工、架构师】 系列纯纯纯技术群,亦可扫码加入我的知识星球【BAT的乌托邦】。
往期精选

发布了403 篇原创文章 · 获赞 923 · 访问量 57万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/105503957