天天看点

由String, String Builder, String Buffer 引起的面试惨案

由String, String Builder, String Buffer 引起的面试惨案

前言

  String,StringBuilder,StringBuffer的区别是啥?这个面试题估计每个JAVA都应该碰到过吧。依稀记得第一次面试的时候,面试官问我这个问题时,心想着能有啥区别不都是拼接字符串嘛。深入了解这个问题后,发现并不简单?

前菜

面试官:你好,你是不一样的科技宅是吧?

小宅:面试官你好,我是不一样的科技宅。

面试官:你好,麻烦做一个简单的自我介绍吧。

小宅:我叫不一样的科技宅,来自xxx,做过的项目主要有xxxx用到xxx,xxx技术。

由String, String Builder, String Buffer 引起的面试惨案

面试官:好的,对你的的履历有些基本了解了,那我们先聊点基础知识吧。

小宅:内心OS(放马过来吧)

开胃小菜

面试官:String,StringBuilder,StringBuffer的区别是啥?

小宅:这个太简单了吧,这是看不起我?

  • 从可变性来讲String的是不可变的,StringBuilder,StringBuffer的长度是可变的。
  • 从运行速度上来讲StringBuilder > StringBuffer > String。
  • 从线程安全上来StringBuilder是线程不安全的,而StringBuffer是线程安全的。

  所以 String:适用于少量的字符串操作的情况,StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况,StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。

由String, String Builder, String Buffer 引起的面试惨案

面试官:为什么String的是不可变的?

小宅:因为存储数据的char数组是使用final进行修饰的,所以不可变。

由String, String Builder, String Buffer 引起的面试惨案

面试官:刚才说到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

的原因了。

由String, String Builder, String Buffer 引起的面试惨案

面试官:刚才你说到了

synchronized

关键字 ,那能讲讲

synchronized

的表现形式嘛?

小宅:

  • 对于普通同步方法 ,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的class对象。
  • 对于同步方法块,锁是Synchonized括号配置的对象。

面试官:能讲讲

synchronized

的原理嘛?

synchronized

是一个重量级锁,实现依赖于

JVM

monitor

监视器锁。主要使用

monitorenter

monitorexit

指令来实现方法同步和代码块同步。在编译的是时候,会将

monitorexit

指令插入到同步代码块的开始位置,而

monitorexit

插入方法结束处和异常处,并且每一个

monitorexit

都有一个与之对应的

monitorexit

  任何对象都有一个

monitor

与之关联,当一个

monitor

被持有后,它将被处于锁定状态,线程执行到

monitorenter

指令时间,会尝试获取对象所对应的

monitor

的所有权,即获取获得对象的锁,由于在编译期会将

monitorexit

插入到方法结束处和异常处,所以在方法执行完毕或者出现异常的情况会自动释放锁。

硬菜来了

面试官:前面你提到

synchronized

是个重量级锁,那它的优化有了解嘛?

由String, String Builder, String Buffer 引起的面试惨案

小宅:为了减少获得锁和和释放锁带来的性能损耗引入了偏向锁、轻量级锁、重量级锁来进行优化,锁升级的过程如下:

  首先是一个无锁的状态,当线程进入同步代码块的时候,会检查对象头内和栈帧中的锁记录里是否存入存入当前线程的ID,如果没有使用

CAS

进行替换。以后该线程进入和退出同步代码块不需要进行

CAS

操作来加锁和解锁,只需要判断对象头的

Mark word

内是否存储指向当前线程的偏向锁。如果有表示已经获得锁,如果没有或者不是,则需要使用

CAS

进行替换,如果设置成功则当前线程持有偏向锁,反之将偏向锁进行撤销并升级为轻量级锁。

  轻量级锁加锁过程,线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的

Mark Word

复制到锁记录(

Displaced Mark Word

)中,然后线程尝试使用

CAS

将对象头中的

Mark Word

替换为指向锁记录的指针。如果成功,当前线程获得锁,反之表示其他线程竞争锁,当前线程便尝试使用自旋来获得锁。

  轻量级锁解锁过程,解锁时,会使用CAS将

Displaced Mark Word

替换回到对象头,如果成功,则表示竞争没有发生,反之则表示当前锁存在竞争锁就会膨胀成重量级锁。

升级过程流程图

由String, String Builder, String Buffer 引起的面试惨案

白话一下:

  可能上面的升级过程和升级过程图,有点难理解并且还有点绕。我们先可以了解下为什么会有锁升级这个过程?

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的轻量级锁给抢走了,这个时候就会升级成重量级锁了。

由String, String Builder, String Buffer 引起的面试惨案

吃完撤退

面试官:内心OS:竟然没问倒他,看来让他培训是没啥希望了,让他回去等通知吧 。

  小宅是吧,你的水平我这边基本了解了,我对你还是比较满意的,但是我们这边还有几个候选人还没面试,没办法直接给你答复,你先回去等通知吧。

小宅:好的好的,谢谢面试官,我这边先回去了。多亏我准备的充分,全回答上来了,应该能收到offer了吧。

由String, String Builder, String Buffer 引起的面试惨案

面试题解析

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。

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!