本篇内容主要讲解“String、String Builder、String Buffer的区别是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“String、String Builder、String Buffer的区别是什么”吧!
小宅:这个太简单了吧,这是看不起我?
从可变性来讲String的是不可变的,StringBuilder,StringBuffer的长度是可变的。
从运行速度上来讲StringBuilder > StringBuffer > String。
从线程安全上来StringBuilder是线程不安全的,而StringBuffer是线程安全的。
所以 String:适用于少量的字符串操作的情况,StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况,StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。
面试官:为什么String的是不可变的?
小宅:因为存储数据的char数组是使用final进行修饰的,所以不可变。
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类使用final关键字进行修饰,所以不能被继承,并且StringBuilder,StringBuffer也是如此都被final关键字修饰。
小宅:这是因为在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。
到此,相信大家对“String、String Builder、String Buffer的区别是什么”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。