本文参考
此片文章思路再次分析以加固理解、记忆~感谢~
三个方面来分析三者区别:
一,增加字符串速度;
二,源码分析;
三,线程相关。
一,通过实例进行三者增加字符串的速度分析:
代码:
public class Demo8 {
public static void main(String[] args) {
// TODO Auto-generated method stub
string();
stringbuilder();
stringbuffer();
}
public static void string() {
String str=new String("This is a String:");
//String str="This is a String:";
long begintime=System.currentTimeMillis();
for(int i=0;i<100000;i++) {
str+="a";
}
long endtime=System.currentTimeMillis();
System.out.println("String执行时间为"+(endtime-begintime));
}
public static void stringbuilder() {
StringBuilder strbd=new StringBuilder("This is a StringBuilder:");
long begintime=System.currentTimeMillis();
for(int i=0;i<100000;i++) {
strbd.append("a");
}
long endtime=System.currentTimeMillis();
System.out.println("StringBuilder执行时间为"+(endtime-begintime));
}
public static void stringbuffer() {
StringBuffer strbf=new StringBuffer("This is a StringBuffer:");
long begintime=System.currentTimeMillis();
for(int i=0;i<100000;i++) {
strbf.append("a");
}
long endtime=System.currentTimeMillis();
System.out.println("StringBuffer执行时间为"+(endtime-begintime));
}
}
运行结果:
一些想法:
①:因为String的初始化过程有两种(如上代码部分),一种是new,一种是直接赋值,builder和buffer都是通过new进行创建对象,故开始测试时,string也采用new创建。但是最后发现,string的两种创建方式对于结果影响不大。最后查找资料发现:
String str = "abc";
相当于:
char data[] = {'a', 'b', 'c'};
String str = new String(data);
②:string的速度与其他两者速度差别较大,而builder和buffer速度相差无异,同时builder和buffer两者速度builder较快(就目前而言是这样,假装不知道结论)。
结论:String<StringBuffer<StringBuilder
二,源码分析:
String:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage.*/
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in*/
private final byte coder;
/** Cache the hash code for the string */
private int hash; // Default to 0
}
value值是byte数组,是字符串中最重要的值,且有final修饰,表示不可修改。故此分析,在增加字符串的操作中,是不断通过创建对象来进行的。所以在10W次的不断添加中,就产生了10W+的字符串新对象。
(脑洞大开点:在没有gc机制的C++中,这样的操作对其来说是不是非常难受?)
因为StringBuilder和StringBuffer类继承的AbstractStringBuilder的源码(builder和buffer都是继承了这个.....builder类的,且StringBuilder和StringBuffer类的源码中除了方法,没什么看的 - -)。
AbstractStringBuilder的源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**The value is used for character storage.*/
byte[] value;
/**The id of the encoding used to encode the bytes in {@code value}.*/
byte coder;
/**The count is the number of characters used.*/
int count;
}
其中的value仍然一个byte数组,但已没有final修饰,所以这就是StringBuilder、StringBuffer可以改变的原因。
三,线程相关
参考文章中所说“
String作为不可变类,是明显线程安全的,Java中所有不可变类都是线程安全的。
StringBuffer类是可变类,但是StringBuffer类中实现的方法都是被Sychronized关键字所修饰的,因此它靠锁实现了线程安全。
Stringbuilder类是可变类,并且方法没有被Sychronized修饰,因此它是线程不安全的。
关于线程安全:线程安全,是指每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
关于
Sychronized:java语言的关键字,修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
我们可以看到
Sychronized刚好解决了线程安全这个问题。
再看StringBuilder、StringBuffer的append方法:
public StringBuilder append(String str) {
super.append(str);
return this;
}
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
再看append方法的说明:Appends the specified string to this character sequence.将指定的str添加到之前的字符队列中去,关于如何添加我们不再关心(- -看了,看不懂)。
关于String,线程肯定是安全的,不可变的。所以现在只讨论Buffer和Builder的线程安全问题。
测试思路:
参考文章的测试代码,个人存疑有一定的问题,随之又重新找了一份
新代码。(感谢~)
对于原文章中的测试代码存疑部分:①thread.sleep()方法,查找资料可知sleep会使该线程放弃cpu,此时CPU可以进行其他调度。我们把这一思路放到buffer里来进行思考,buffer线程安全是因为加了锁,使其他线程不再访问,但是sleep之后,会使该线程让出CPU,换言之,执行了sleep之后,CPU可以“毫无忌惮的”去执行其他线程,此时的加锁已经没有任何意义。②:多线程的执行顺序本就是由CPU自己决定的,执行顺序不能说明是否线程安全,我们应关注在全局变量原址性的一些操作上的线程安全问题。
测试代码:
/*
* 测试builder和buffer的线程安全
* 实现:
* 1.创建一个StringBuilder对象sb传入"0000011111"
* 2.多个线程同时调用sb对象的reverse
* 3.当结果出现01混合状态时,则验证了线程不安全
*/
public class Demo9 {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder("0000011111");
//StringBuffer sb=new StringBuffer("0000011111");
for(int i=0;i<100;i++){
new Thread(new MyThread(sb)).start();
}
}
private static class MyThread implements Runnable{
private StringBuilder sb;
private MyThread(StringBuilder sb){
this.sb=sb;
}
/* private StringBuffer sb;
private MyThread(StringBuffer sb){
this.sb=sb;
}*/
public void run() {
for(int i=0;i<100;i++){
sb.reverse();
}
System.out.println(sb);
}
}
}
测试结果:
再执行有关于buffer的相关代码:
我们来看看测试代码中的reverse方法源码:
public AbstractStringBuilder reverse() {
byte[] val = this.value;
int count = this.count;
int coder = this.coder;
int n = count - 1;
if (COMPACT_STRINGS && coder == LATIN1) {
/****************************************
for (int j = (n-1) >> 1; j >= 0; j--) {
int k = n - j;
byte cj = val[j];
val[j] = val[k];
val[k] = cj;
}
****************************************/
} else {
StringUTF16.reverse(val, count);
}
return this;
}
首先reverse方法就是将前和其对应的后面字符进行交换。
在上面代码的注释处,个人觉得这个就是产生线程不安全的根本原因,两个字符会借助一个中间变量cj来进行交换。多个线程会同时使用三个变量(cj、val[i]、vaj[j])产生同步问题。
而且除了原序列0000011111有次序问题,还会发生个数变化的问题(上面的执行结果也可以看出来,如原本5个1+5个0变成4个1+6个0)。
谈谈最近收获吧:
1,编程基础还是很差以至于敲测试代码还要参考别人的代码,自己能敲出来,但是实现方面还是有很大问题;
2,多看源码,多练习,多思考。