前言

  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(); 的实现。

@Overridepublic 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是一个重量级锁,实现依赖于JVMmonitor 监视器锁。次要应用monitorentermonitorexit指令来实现办法同步和代码块同步。在编译的是时候,会将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。

结尾

  我是不一样的科技宅,每天提高一点点,体验不一样的生存。咱们下期见!

  如果感觉对你有帮忙,能够多多评论,多多点赞哦,也能够到我的主页看看,说不定有你喜爱的文章,也能够顺手点个关注哦,谢谢。