所遇问题
最近在做项目遇到了一个问题,具体场景是多线程下共享资源的问题,这里模拟一个具体场景:
有一个启动线程的类,这个类中存放线程的共享变量,在这个类中新建多个线程,并将共享变量以构造器的参数引入,以此实现多线程间的数据共享。
例如:
有两个线程A和B,将list传给两个线程A和B:
A线程负责打印list的大小,B线程负责在list中加入数据:
运行程序后,如下:
从结果可以看出,list变量是在连个线程共享的。
但是我犯了一个基本的错误。。。。。。就是把这里的list换成了基本数据类型,比如int、long等,得出的结果是这些基本数据类型不会共享。这里的问题就在于Java传参的方式,有传值和传引用,这里先讲讲java的传参问题。
传值和传引用
针对于不同的数据类型,java分为传值和传引用,先说一下啥是传值和传引用
关于传值
传值是指传递过去的对象实际上是拷贝出一个同样值的新对象传递过去,传递过去后,原来的参数和传递过去的参数对应的是两个对象,指向的是两个不同的地址空间。例如下面这个图:
上图中,B方法将a传递给A方法,并赋值给b,相当于在内存中新开辟的了一个空间,此时A和B都等于2,但实际上他们指向的是两个不同的地址空间,他们之间的改动并不会相互影响。
关于传引用:
传引用是将对象的引用地址传递给另一个值,两个参数共同指向同一个地址空间的对象,对两个参数任意的修改,实际上修改的是同一个地址空间,所以这两个参数相当于共享一个对象,如下图:
在java中,对于基本数据类型(8种)的参数传递是值传递,对于其他对象类型的擦混地是引用传递,下面用一个小demo测试一下:
同样是两个线程,A和B,只是在构造其中传入ArrayList对象和int类型的数据:
线程A进行展示,线程B对list和count进行改变(这里先不考虑线程安全问题):
运行后结果如下:
运行结果也如我们分析的一样,对于list因为指向同一个变量,所以是两个线程共享的,而count为基本数据类型,指向的是两个地址空间的对象所以B线程count和A中的count是两个不同对象,B修改的只是自己指向的地址空间中的值,A中的值一直不变。
String类型:
刚才说到,java对于基本数据类型来说是传值,对于其他对象类型是传引用。但是String例外,String不是基本数据类型,但是它的传递是按值传递的。原因在于String类型适用final关键字修饰的,代表了他是一个不可变对象。不可变对象决定了每一次对string的改变,都不是在原有的地址空间中的对象进行修改,而是新开辟出一个地址空间后指向这个新开辟的地址空间。
例如下面的图:
如图,在对S进行赋值以后,指向了一个地址A中的对象,在对S进行修改后,由于String类型是不可变的,并不是在原有的A中进行修改,而是新开辟一个空间B,指向这个空间中的“ab”。
所以将此应用到上面线程A、B中的例子,假如传入A、B线程中的是一个String对象,B线程中对String进行修改后,实际上对象的引用地址已经换了,线程A和B中的两个String是不同的对象,所以不能达到共享的目的。
运行结果为:
可以看出,String类型的str在A中并没有改变,印证了之前说的。
stringbuffer/stringbuilder
因为string类型是不可变量,在线程中无法进行共享,这个时候就可以使用Stringbuffer和Stringbuilder了,这两个类就为可变对象,对其所有的修改、删除、更新操作都是在同一个地址空间进行(String是新开辟空间,上面提到过)。基于这一点,这两个类的性能要比String类要高很多,在使用字符串类型频率较高的场景下,都推荐用这两个类。
Stringbuffer:线程安全,适用于多线程对字符串操作场景。
Stringbuilder:线程不安全,用于单线程对字符串的操作,也因此性能高于Stringbuffer。
针对与StringBuffer,有一系列对其操作的方法,append/delete/reverse/insert等等,这里说一下针对于StringBuffer清空,主要有两种方法:
sb.delete(0,sb_length); //删除字符串从0~sb_length-1处的内容
sb.setLength(0); //设置StringBuffer变量的长度为0
第二种的效率要高于第一种。