开发者社区> 不一样的科技宅> 正文

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

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

pexels-pixabay-60504

前言

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

前菜

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

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

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

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

20180719930824_lrEKIh

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

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

开胃小菜

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

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

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

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

75f3cd331b7ab4ca4552847a746952da

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

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

image-20200714151350294

面试官:刚才说到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的原因了。

20181119593911_rYDslC

面试官:刚才你说到了synchronized关键字 ,那能讲讲synchronized的表现形式嘛?

小宅

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

面试官:能讲讲synchronized的原理嘛?

小宅synchronized是一个重量级锁,实现依赖于JVMmonitor 监视器锁。主要使用monitorentermonitorexit指令来实现方法同步和代码块同步。在编译的是时候,会将monitorexit指令插入到同步代码块的开始位置,而monitorexit插入方法结束处和异常处,并且每一个monitorexit都有一个与之对应的monitorexit

  任何对象都有一个monitor与之关联,当一个monitor被持有后,它将被处于锁定状态,线程执行到monitorenter指令时间,会尝试获取对象所对应的monitor的所有权,即获取获得对象的锁,由于在编译期会将monitorexit插入到方法结束处和异常处,所以在方法执行完毕或者出现异常的情况会自动释放锁。

硬菜来了

面试官:前面你提到synchronized是个重量级锁,那它的优化有了解嘛?

006qir4oly1g0qzhmov3tj30gw0cgjte

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

  首先是一个无锁的状态,当线程进入同步代码块的时候,会检查对象头内和栈帧中的锁记录里是否存入存入当前线程的ID,如果没有使用CAS 进行替换。以后该线程进入和退出同步代码块不需要进行CAS 操作来加锁和解锁,只需要判断对象头的Mark word内是否存储指向当前线程的偏向锁。如果有表示已经获得锁,如果没有或者不是,则需要使用CAS进行替换,如果设置成功则当前线程持有偏向锁,反之将偏向锁进行撤销并升级为轻量级锁。

  轻量级锁加锁过程,线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word复制到锁记录(Displaced Mark Word)中,然后线程尝试使用CAS 将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,反之表示其他线程竞争锁,当前线程便尝试使用自旋来获得锁。

  轻量级锁解锁过程,解锁时,会使用CAS将Displaced Mark Word替换回到对象头,如果成功,则表示竞争没有发生,反之则表示当前锁存在竞争锁就会膨胀成重量级锁。

升级过程流程图

533411-20200423173203871-980115964

白话一下:

  可能上面的升级过程和升级过程图,有点难理解并且还有点绕。我们先可以了解下为什么会有锁升级这个过程?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的轻量级锁给抢走了,这个时候就会升级成重量级锁了。

u=628528004,774370142&fm=26&gp=0

吃完撤退

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

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

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

timg

面试题解析

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。

结尾

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

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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
19030 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
18704 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
21619 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
25056 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
20361 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
18643 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
13913 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
14713 0
使用NAT网关轻松为单台云服务器设置多个公网IP
在应用中,有时会遇到用户询问如何使单台云服务器具备多个公网IP的问题。 具体如何操作呢,有了NAT网关这个也不是难题。
35104 0
+关注
不一样的科技宅
每天进步一点点,体验不一样的生活。
21
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载