乐趣区

Java必修课Stringintern原来还能这么用原理与应用

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 有帮助。


欢迎关注公众号 <南瓜慢说>,将持续为你更新 …

多读书,多分享;多写作,多整理。

退出移动版