【Java必修课】String.intern()原来还能这么用(原理与应用)
1 简介
string.intern()
是jdk一早就提供的native方法,不由java实现,而是底层jvm实现,这让我们对它的窥探提高了难度。特别是在oracle收购了sun公司后,源代码不开源了,更无法深入研究了。但我们还是有必要尽量地去探索。
本文将主要讲解一下string.intern()
方法的原理、特点,并介绍一个新奇的应用。
2 string的池化
方法intern()
的作用就是将string池化,这个池是string的常量池。不同版本的jdk有不同的实现。
2.1 不同实现与不同内存空间
- jdk 6:
intern()
方法会把首先遇到的字符串复制一份到永久代中,然后返回永久中的实例引用;如果不是首次,说明常量池中已经有该字符串,直接返回池中的引用。常量池在永久代(permgen)中。 - jdk 7:
intern()
方法首次遇到字符串时,不会复制实例,而是直接把该字符串的引用记录在常量池中,并返回该引用;如果不是首次,则直接返回池中引用。jdk 7常量池在堆中。 - jdk 8:功能与jdk 7类似。常量池在元空间metaspace中,元空间不在虚拟机内存中,而是使用本地内存。
2.2 常量池大小差异
这个所谓的string常量池,其实就是一张哈希表,跟hashmap
类似,所以也是有大小限制和哈希冲突可能。常量池越大,哈希冲突可能性越小。
jdk 6早期版本,池大小为常量1009,后期变得可配置,通过参数
-xx:stringtablesize=n
指定。大小也会受限于永久代的大小,建议避免使用intern()
方法,防止造成permgen内存溢出。jdk 7将常量池移到堆后,可以存放更多常量,也一样通过参数可配置大小。在java 7u40后,常量池默认大小增加到了60013。
jdk 8默认大小一开始就是60013,依旧支持参数配置。
总的来说,-xx:stringtablesize的默认值在java 7u40以前为1009,java 7u40以后改为60013。
3 例子分析
通过例子,来理解一下就更清晰了。jdk 7和8应该表现一致,本文使用jdk 8。
3.1 jdk 8
先演示jdk 8的情况:
例子1
string str1 = new string("pkslow"); system.out.println(str1.intern() == str1);
结果:false
分析:因为使用了字面量,在编译期就会把字符串放到常量池,当使用new string()
时,会创建新的对象。所以常量池中的引用与创建的对象引用不同。
例子2
string str1 ="pkslow"; system.out.println(str1.intern() == str1);
结果:true
分析:与上个例子对比,将常量池的地址赋值给了str1变量,所以相等。
例子3
string str1 = new stringbuilder("pk").append("slow").tostring(); system.out.println(str1.intern() == str1); string str2 = new stringbuilder("pk").append("slow").tostring(); system.out.println(str2.intern() == str2);
结果:true false
分析:
(1)第一句创建了一个新的字符串对象,str1为其引用,调用str1.intern()
时会把它的引用放到常量池中并返回,所以是同一个引用。
(2)在(1)中已经放在常量池了,所以str2.intern()
返回的是str1,与str2不相等。
例子4
string str = new stringbuilder("ja").append("va").tostring(); system.out.println(str.intern() == str);
结果:false
分析:按理说与上个例子的(1)一样,应该为true才对。问题在于java它是一个比较特殊的字符串,已经在常量池中存在了,所以不相等。至于为何会存在,我的猜想是有两种可能:其它jdk的java代码有该常量;jvm代码直接把某些特殊字符串放到了常量池。这个没有深究了。
3.2 jdk 6的不同
当我们知道了原理后,不同表现就可以很容易判断出来了。如下例子:
string str1 = new stringbuilder("pk").append("slow").tostring(); system.out.println(str1.intern() == str1);
jdk 6结果:false
jdk 8结果:true
因为jdk 6对于首次遇到的字符串,会复制一份到常量池并返回其引用,与str1的引用不是同一个,所以为false。而jdk 8只是将str1的引用在常量池记录然后返回,还是同一个,所以为true。
知道了基本原理,更多情况就可以具体分析了,不再一一赘述。
4 一种少见的应用
之前已经说过,string.intern()
方法本质就是维持了一个string的常量池,而且池里的string应该都是唯一的。这样,我们便可以利用这种唯一性,来做一些文章了。我们可以利用池里string的对象来做锁,实现对资源的控制。比如一个城市的某种资源同一时间只能一个线程访问,那就可以把城市名的string对象作为锁,放到常量池中去,同一时间只能一个线程获得。
具体代码如下:
import java.util.concurrent.executorservice; import java.util.concurrent.executors; public class stringinternmultithread { private string city; public stringinternmultithread(string city) { this.city = city; } public void handle() { synchronized (this.city.intern()) { system.out.println(city + ":fetched the lock"); try { thread.sleep(1000); } catch (interruptedexception e) { e.printstacktrace(); } system.out.println(city + ":release the lock"); } } public static void main(string[] args) { executorservice executorservice = executors.newfixedthreadpool(6); stringinternmultithread guangzhou = new stringinternmultithread("guangzhou"); stringinternmultithread shenzhen = new stringinternmultithread("shenzhen"); stringinternmultithread beijing = new stringinternmultithread("beijing"); executorservice.execute(guangzhou::handle); executorservice.execute(guangzhou::handle); executorservice.execute(guangzhou::handle); executorservice.execute(shenzhen::handle); executorservice.execute(shenzhen::handle); executorservice.execute(beijing::handle); executorservice.shutdown(); } }
运行结果如下:
guangzhou:fetched the lock shenzhen:fetched the lock beijing:fetched the lock beijing:release the lock shenzhen:release the lock guangzhou:release the lock shenzhen:fetched the lock guangzhou:fetched the lock shenzhen:release the lock guangzhou:release the lock guangzhou:fetched the lock guangzhou:release the lock
可以看出,同一时间同一个城市不会同时获得资源,而不同城市可以同时获得资源来处理。这种案例其实有其它更优雅的方案,这不是本文的重点,就不赘述了。
5 总结
本文介绍了string.intern()
方法的原理和不同jdk版本的表现,并通过多个例子与一个应用加深理解。希望对大家理解string和jvm有帮助。
欢迎关注公众号<南瓜慢说>,将持续为你更新...
多读书,多分享;多写作,多整理。