前言
String,StringBuilder,StringBuffer 的区别是啥?这个面试题预计每个 JAVA 都应该碰到过吧。依稀记得第一次面试的时候,面试官问我这个问题时,心想着能有啥区别不都是拼接字符串嘛。深刻理解这个问题后,发现并不简略?
前菜
面试官:你好,你是不一样的科技宅是吧?
小宅:面试官你好,我是不一样的科技宅。
面试官:你好,麻烦做一个简略的自我介绍吧。
小宅:我叫不一样的科技宅,来自 xxx,做过的我的项目次要有 xxxx 用到 xxx,xxx 技术。
面试官:好的,对你的的履历有些根本理解了,那咱们先聊点基础知识吧。
小宅:心田 OS(放马过来吧)
开胃小菜
面试官:String,StringBuilder,StringBuffer 的区别是啥?
小宅:这个太简略了吧,这是看不起我?
- 从可变性来讲 String 的是不可变的,StringBuilder,StringBuffer 的长度是可变的。
- 从运行速度上来讲 StringBuilder > StringBuffer > String。
- 从线程平安上来 StringBuilder 是线程不平安的,而 StringBuffer 是线程平安的。
所以 String:实用于大量的字符串操作的状况,StringBuilder:实用于单线程下在字符缓冲区进行大量操作的状况,StringBuffer:实用多线程下在字符缓冲区进行大量操作的状况。
面试官:为什么 String 的是不可变的?
小宅:因为存储数据的 char 数组是应用 final 进行润饰的,所以不可变。
面试官:方才说到 String 是不可变,然而上面的代码运行完,却发生变化了,这是为啥呢?
public class Demo {public static void main(String[] args) {
String str = "不一样的";
str = str + "科技宅";
System.out.println(str);
}
}
很显著下面运行的后果是:不一样的科技宅。
咱们先应用javac Demo.class
进行编译,而后反编译javap -verbose Demo
失去如下后果:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc #2 // String 不一样的
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String 科技宅
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
咱们能够发现,在应用 +
进行拼接的时候,实际上 jvm 是初始化了一个StringBuilder
进行拼接的。相当于编译后的代码如下:
public class Demo {public static void main(String[] args) {
String str = "不一样的";
StringBuilder builder =new StringBuilder();
builder.append(str);
builder.append("科技宅");
str = builder.toString();
System.out.println(str);
}
}
咱们能够看下builder.toString();
的实现。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
很显著 toString
办法是生成了一个新的 String
对象而不是更改旧的 str
的内容,相当于把旧 str
的援用指向的新的 String
对象。这也就是 str
发生变化的起因。
分享我碰到过的一道面试题,大家能够猜猜答案是啥?文末有解析哦
public class Demo {public static void main(String[] args) {
String str = null;
str = str + "";
System.out.println(str);
}
}
面试官:String 类能够被继承嘛?
小宅:不能够,因为 String 类应用 final 关键字进行润饰,所以不能被继承,并且 StringBuilder,StringBuffer 也是如此都被 final 关键字润饰。
面试官:为什么 String Buffer 是线程平安的?
小宅 :这是因为在StringBuffer
类内,罕用的办法都应用了 synchronized
进行同步所以是线程平安的,然而StringBuilder
并没有。这也就是运行速度 StringBuilder
> StringBuffer
的起因了。
面试官:方才你说到了 synchronized
关键字,那能讲讲 synchronized
的表现形式嘛?
小宅:
- 对于一般同步办法,锁是以后实例对象。
- 对于动态同步办法,锁是以后类的 class 对象。
- 对于同步办法块,锁是 Synchonized 括号配置的对象。
面试官 :能讲讲synchronized
的原理嘛?
小宅 :synchronized
是一个重量级锁,实现依赖于 JVM
的 monitor
监视器锁。次要应用monitorenter
和monitorexit
指令来实现办法同步和代码块同步。在编译的是时候,会将 monitorexit
指令插入到同步代码块的开始地位,而 monitorexit
插入方法完结处和异样处,并且每一个 monitorexit
都有一个与之对应的monitorexit
。
任何对象都有一个 monitor
与之关联,当一个 monitor
被持有后,它将被处于锁定状态,线程执行到 monitorenter
指令工夫,会尝试获取对象所对应的 monitor
的所有权,即获取取得对象的锁,因为在编译期会将 monitorexit
插入到办法完结处和异样处,所以在办法执行结束或者出现异常的状况会主动开释锁。
硬菜来了
面试官 :后面你提到synchronized
是个重量级锁,那它的优化有理解嘛?
小宅:为了缩小取得锁和和开释锁带来的性能损耗引入了偏差锁、轻量级锁、重量级锁来进行优化,锁降级的过程如下:
首先是一个无锁的状态,当线程进入同步代码块的时候,会查看对象头内和栈帧中的锁记录里是否存入存入以后线程的 ID,如果没有应用 CAS
进行替换。当前该线程进入和退出同步代码块不须要进行CAS
操作来加锁和解锁,只须要判断对象头的Mark word
内是否存储指向以后线程的偏差锁。如果有示意曾经取得锁,如果没有或者不是,则须要应用 CAS
进行替换,如果设置胜利则以后线程持有偏差锁,反之将偏差锁进行撤销并降级为轻量级锁。
轻量级锁加锁过程,线程在执行同步块之前,JVM 会在以后线程的栈帧中创立用于存储锁记录的空间,并将对象头的 Mark Word
复制到锁记录 (Displaced Mark Word
) 中,而后线程尝试应用 CAS
将对象头中的Mark Word
替换为指向锁记录的指针。如果胜利,以后线程取得锁,反之示意其余线程竞争锁,以后线程便尝试应用自旋来取得锁。
轻量级锁解锁过程,解锁时,会应用 CAS 将 Displaced Mark Word
替换回到对象头,如果胜利,则示意竞争没有产生,反之则示意以后锁存在竞争锁就会收缩成重量级锁。
降级过程流程图
文言一下:
可能下面的降级过程和降级过程图,有点难了解并且还有点绕。咱们先能够理解下为什么会有锁降级这个过程?HotSpot 的作者通过钻研发现,大多数状况下锁不仅不存在多线程竞争,而且总是由同一个线程屡次取得。为了防止取得锁和和开释锁带来的性能损耗引入锁降级这样一个过程。了解锁降级这个流程须要明确一个点:产生了竞争才锁会进行降级并且不能降级。
咱们以两个线程 T1,T2 执行同步代码块来演示锁是如何膨胀起来的。咱们从无锁的状态开始,这个时候 T1 进入了同步代码块,判断以后锁的一个状态。发现是一个无锁的状态,这个时候会应用 CAS
将锁记录内的线程 Id 指向 T1 并从无锁状态变成了偏差锁。运行了一段时间后 T2 进入了同步代码块,发现曾经是偏差锁了,于是尝试应用 CAS
去尝试将锁记录内的线程 Id 改为 T2,如果更改胜利则 T2 持有偏差锁。失败了阐明存在竞争就降级为轻量级锁了。
可能你会有疑难,为啥会失败呢?咱们要从 CAS
操作动手,CAS
是 Compare-and-swap(比拟与替换)的简写,是一种有名的无锁算法。CAS 须要有 3 个操作数,内存地址 V,旧的预期值 A,行将要更新的目标值 B,换句话说就是,内存地址 0x01 存的是数字 6 我想把他变成 7。这个时候我先拿到 0x01 的值是 6,而后再一次获取 0x01 的值并判断是不是 6,如果是就更新为 7,如果不是就再来一遍之道胜利为止。这个次要是因为 CPU 的工夫片起因,可能执行到一半被挂起了,而后别的线程把值给改了,这个时候程序就可能将谬误的值设置进去,导致后果异样。
简略理解了一下 CAS
当初让咱们持续回到锁降级这个过程,T2 尝试应用 CAS
进行替换锁记录内的线程 ID,后果 CAS
失败了这也就意味着,这个时候 T1 抢走了本来属于 T2 的锁,很显著这一刻产生了 竞争 所以锁须要降级。在降级为轻量级锁前,持有偏差锁的线程 T1 会被暂停,并查看 T1 的状态,如果 T1 处于 未流动的状态 / 曾经退出同步代码块 的时候,T1 会开释偏差锁并被唤醒。如果未退出同步代码块,则这个时候会降级为轻量级锁,并且由 T1 取得锁,从平安点继续执行,执行完后对轻量级锁进行开释。
偏差锁的应用了呈现竞争了才开释锁的机制,所以当其余线程尝试竞争偏差锁时,持有偏差锁的线程才会开释锁。并且偏差锁的撤销须要期待全局平安点(这个工夫点没有任何正在执行的字节码)。
T1 因为没有人竞争通过一段时间的安稳运行,在某一个工夫点时候 T2 进来了,产生应用 CAS
取得锁,然而发现失败了,这个时候 T2 会期待一下(自旋取得锁),因为竞争不是很强烈所以等 T1 执行完后,就能获取到锁并进行执行。如果长时间获取不到锁则就可能产生竞争了,可能呈现了个 T3 把本来属于 T2 的轻量级锁给抢走了,这个时候就会升级成重量级锁了。
吃完撤退
面试官:心田 OS:居然没问倒他,看来让他培训是没啥心愿了,让他回去等告诉吧。
小宅是吧,你的程度我这边根本理解了,我对你还是比较满意的,然而咱们这边还有几个候选人还没面试,没方法间接给你回答,你先回去等告诉吧。
小宅:好的好的,谢谢面试官,我这边先回去了。多亏我筹备的充沛,全答复上来了,应该能收到 offer 了吧。
面试题解析
public class Demo {public static void main(String[] args) {
String str = null;
str = str + "";
System.out.println(str);
}
}
答案是 null,从之前咱们理解到应用 +
进行拼接实际上是会转换为 StringBuilder
应用 append
办法进行拼接。所以咱们看看 append
办法实现逻辑就明确了。
public AbstractStringBuilder append(String str) {if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
从代码中能够发现,如果传入的字符串是 null
时,调用 appendNull
办法,而 appendNull
会返回 null。
结尾
我是不一样的科技宅,每天提高一点点,体验不一样的生存。咱们下期见!
如果感觉对你有帮忙,能够多多评论,多多点赞哦,也能够到我的主页看看,说不定有你喜爱的文章,也能够顺手点个关注哦,谢谢。