String对象底层是一个char[],并且是final修饰不可继承。为什么不可继承?
因为String对象是JVM中最常使用的一种数据类型,运行时大量的创建和销毁需要耗费时间和空间。所以为了提高效率节约空间,就有了字符串常量池的存在。JDK7之后字符串常量池从永久代移到堆区实现。JDK7后开始移除永久代,在8后用元空间完全替代。
一般的使用为String a = "hello",这种方式为声明字面量,在编译时期将hello放到字符串常量池中。
String a = new String("hello") 这种创建对象的方式不建议,会在堆区创建一个包含字符串hello内容的实例对象。
String.intern()方法是返回字符串池中的一个相同内容的实例引用。如果字符串池中没有相同内容的字符串,就把当前字符串内容放到池中并返回这个字符串在池中的地址。
String s1 = "古时的风筝";// 在字符串池中创建 “古时的风筝”字符串,返回地址
String s2 = "古时的风筝";// 池中已有相同内容的字符串,直接返回地址
String a = "古时的";// 在池中创建 “古时的”字符串,返回地址
String s3 = new String(a + "风筝");// 在堆中创建“古时的风筝”,返回堆地址1
String s4 = new String(a + "风筝");// 在堆中创建“古时的风筝”,返回堆地址2
System.out.println(s1 == s2); // 【1】 true 2个引用指向字符串池中相同地址
System.out.println(s2 == s3); // 【2】 false 2个引用分别指向常量池和堆,是不同地址
System.out.println(s3 == s4); // 【3】 false 2个引用指向堆中不同地址
s3.intern();// 在池中查找“古时的风筝”内容的字符串,已有所以返回地址,但是没有变量接收
System.out.println(s2 == s3); // 【4】 false 2个引用指向不同地址,s2是常量池地址,s3是堆地址
s3 = s3.intern();// s3指向了常量池的地址
System.out.println(s2 == s3); // 【5】 true 2个引用都指向常量池地址
s4 = s4.intern();// s4指向常量池地址
System.out.println(s3 == s4); // 【6】 true 2个引用都指向常量池地址
所以回到问题,String是final不可继承以及不可被改变的,String内部的char[]数组也是final的。
字符串常量池的基础就是字符串的不可变性,如果字符串是可变的,那想一想,常量池就没必要存在了。假设多个变量都指向字符串常量池的同一个字符串,然后呢,突然来了一行代码,不管三七二十一,直接把字符串给变了,那岂不是 jvm 世界大乱。
字符串不可变的根本原因应该是处于安全性考虑。
我们知道 jvm 类型加载的时候会用到类名,比如加载 java.lang.String 类型,如果字符串可变的话,那我替换成其他的字符,那岂不是很危险。项目中会用到比如数据库连接串、账号、密码等字符串,只有不可变的连接串、用户名和密码才能保证安全性。字符串在 Java 中的使用频率可谓高之又高,那在高并发的情况下不可变性也使得对字符串的读写操作不用考虑多线程竞争的情况。
最后一点就是上面提到的,字符串对象的频繁创建会带来性能上的开销,所以,利用不可变性才有了字符串常量池,使得性能得以保障。
那String.intern()可以用来干点什么?
有一个使用场景,缓存没有数据时异步查询DB数据,为了避免大流量查询DB,需要在JVM中保证一个活动只放一个线程去查,这样并发查询的数量,对于一个活动来说,就是JVM实例的数量,一般也就十几台就是十几个并发。所以这个锁对象该怎么构造?
直接用活动id?不行,new Integer就是一个新的对象,锁不住。new String同理。
这个锁对象要求所有的活动id使用同一个,刚好符合String常量池。只要创建String对象时先去常量池找,就可以保证所有活动id使用同一个String对象。
经过下面的实践证明这样的方案可行。
private void internCompare() {
int activityId = 1;
String a = (ASYNC_QUERY_ACTIVITY_PREFIX + activityId).intern();
String b = (ASYNC_QUERY_ACTIVITY_PREFIX + activityId).intern();
// 输出true,同一个对象
System.out.println(a == b);
}
private void concurrent() {
int t = 20;
for (int i = 0; i < t; i++) {
Thread w = new Thread(() -> sync(130));
w.setName("Worker"+i);
w.start();
}
try {
Thread.sleep(200001);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
20个线程竞争,只会有一个线程进入且睡眠10秒,按照设想应该每隔10秒输出一个线程
private void sync(Integer activityId) {
String lock = (ASYNC_QUERY_ACTIVITY_PREFIX + activityId);
// lock.intern总是返回常量池中的同个String对象
synchronized (lock.intern()) {
System.out.println(new Date() + " " +Thread.currentThread().getName());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" leaving!");
}
}
用visualVM看也是符合预想,其他线程都block住,每10秒只有一个线程在睡眠