静态变量、实例变量、局部变量等在多线程下的安全问题
静态变量:线程非安全
静态变量:使用static关键字定义的变量。static可以修饰变量和方法,也有static静态代码块。被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。
用public修饰的static成员变量和成员方法本质是全局变量和全局方法,当声明它的类的对象时,不生成static变量的副本,而是类的所有实例共享同一个static变量。
静态变量也称为类变量,属于类对象所有,位于方法区,为所有对象共享,共享一份内存,一旦值被修改,则其他对象均对修改可见,故线程非安全。
实例变量:单例时线程非安全,非单例时线程安全
实例变量:实例变量属于类对象的,也就是说,属于对象实例私有,在虚拟机的堆中分配。
实例变量是实例对象私有的,系统只存在一个实例对象,则在多线程环境下,如果值改变后,则其它对象均可见,故线程非安全;如果每个线程都在不同的实例对象中执行,则对象与对象间的修改互不影响,故线程安全。
局部变量:线程安全
局部变量:定义在方法内部的变量。 每个线程执行时都会把局部变量放在各自的帧栈的内存空间中,线程间不共享,故不存在线程安全问题
静态方法的线程安全性
静态方法中如果没有使用静态变量,则没有线程安全的问题;
静态方法内的变量,每个线程调用时,都会新创建一份,不会共用一个存储单元,故不存在线程冲突的问题。
为什么string不可变就是线程安全的?
string是内容不可变,但是引用可变,那么如果某个线程修改了字符串的引用地址,其他线程不就读到了不一样的内容吗?这样可以说是线程不安全吗?
string不可变”指的是堆区不可变, 但string指针是可变的,一旦共享就存在"安全发布"的问题
例如,以下代码中线程A先执行update,之后线程B执行read,B并不一定能看到线程A的修改,可能读到更新前的值
String s=new String("abc"); //s指针指向堆1
void update(){
s=s.replace("b","d"); //线程A: s指针指向堆2
}
void read(){
System.out.println(s); //线程B: 可能读到堆1
}
我们在说String类是线程安全的时候,是在说其采用了"栈封闭"的手段,每当修改时就返回一个新的对象,这个新对象在发布以前(也就是方法返回以前)其指针只存在于线程私有栈中, 此时还不是"共享的", 当然是线程安全的。至于当从String的方法域内发布以后, 其指针已经逸出, 这个指针可以被并发读写, 同时具有共享性和可变性,存在安全发布的问题,不是线程安全的。 但这个锅已经不能给String类去背了, 因为已经出了String的域. 更进一步说, 把 String 换成任何线程安全的类, 都一样会存在指针安全发布问题。
StringBuffer、StringBuilder线程安全问题
StringBuffer是线程安全,而StringBuilder不是线程安全的(原因是StringBuffer中的方法都加了synchronized关键字)。所以网上很多资料都说,多线程不要用StringBuilder,否则会出现问题。
/**
* StringBuffer 线程安全
* StringBuilder 线程非安全
* @author 909974
*
*/
public class Thread4 {
public static void main(String[] argaa) {
MyString sb = new MyString();
StringBuilder sbBuilder = new StringBuilder();
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
sb.append(1);
sbBuilder.append("1");
stringBuffer.append("1");
System.out.println(sb.getNum() + "-" +
sbBuilder.length() + "-" + stringBuffer.length());
}
}
}).start();
}
}
}
class MyString {
private Integer num = 0;
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
public synchronized void append(Integer num) {
this.num = this.num + num;
}
}
上面的代码中,用了自己定义的MyString类与StringBuffer以及StringBuilder。启用了1000个线程,每个线程都进行“累加”操作,并打印结果。
结果显示,StringBuffer是正确答案,StringBuilder少于正确答案,而MyString,会根据append方法是否加上synchronized关键字而显示不同结果。
所以StringBuilder不能用于对同一对象的多线程操作。
stringbuffer固然是线程安全的,所以stringbuffer固然是比stringbuilder更慢
不过一般对字符串的操作,并不会用到多线程,而且stringbuffer的线程安全,仅仅是保证jvm不抛出异常顺利的往下执行而已,它可不保证逻辑正确和调用顺序正确。大多数时候,我们需要的不仅仅是线程安全,而是锁。所以绝大多数时候,用StringBuilder即可。
所以在jdk1.5的时候,javac把所有用加号连接的string运算都隐式的改写成stringbuilder,也就是说,从jdk1.5开始,用加号拼接字符串已经没有任何性能损失了("用加号拼接字符串已经没有任何性能损失了"并不严谨,严格的说,如果没有循环的情况下,单行用加号拼接字符串是没有性能损失的,java编译器会隐式的替换成stringbuilder,但在有循环的情况下,编译器没法做到足够智能的替换,每次都new了一个stringBuilder,然后把string转成stringBuilder在执行append方法,因此,用循环拼接字符串的时候,还是老老实实的用stringbuilder吧)
循环使用+拼接
//源码
String str = "hello";
for(int i=0;i<1000;i++){
str+=i;
}
//反编译后的
String str = "hello";
for(int i=0;i<1000;i++){
str = (new StringBuilder()).append(str).append(i).toString();
}