多线程安全
2020-02-01 本文已影响0人
二狗不是狗
一.JMM内存模型
1、下图描述了一个多线程执行场景, 线程 A 和线程 B 分别对主内存的变量进行读写操作。
2、其中主内存中的变量为共享变量,也就是说此变量只此一份,多个线程间共享。
3、线程不能直接读写主内存的共享变量,每个线程都有自己的工作内存,线程需要读写主内存的共享变量时需要先将该变量拷贝一份副本到自己的工作内存,然后在自己的工作内存中对该变量进行所有操作,线程工作内存对变量副本完成操作之后需要将结果同步至主内存。
二.线程安全的本质
线程安全问题都是由全局变量及静态变量引起的,若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能导致数据不一致。
// 多线程x变量是安全的,两个线程执行完后x=10000
Runnable runnable = new Runnable() {
@Override
public void run() {
int x = 0; // 变量存储在栈中
for (int i=1; i<=10000; i++) { x++; }
System.out.println(x);
}
};
// 多线程x变量是不安全的,两个线程执行完后x不可预知
Runnable runnable = new Runnable() {
int x = 0; // 变量存储在堆中
@Override
public void run() {
for (int i=1; i<=10000; i++) { x++; }
System.out.println(x);
}
};
new Thread(runnable).start();
new Thread(runnable).start();
// 两个线程执行完后x=10000
new Thread(new Runnable() {
int x = 0;
@Override
public void run() {
for (int i=1; i<=10000; i++) { x++; }
System.out.println(x);
}
}).start();
new Thread(new Runnable() {
int x = 0;
@Override
public void run() {
for (int i=1; i<=10000; i++) { x++; }
System.out.println(x);
}
}).start();
在多线程环境下就会出现在执行完 int tmp = x + 1; 这行代码时就发生了线程切换,当线程再次切回来的时候,x 就会被重复赋值,导致出现上面的运行结果,2个线程都无法输出 2000000。
image.png三.为什么StringBuilder线程不安全
- 下面的代码两个线程输出的长度并不是1000;输出字符串也不是1000个a。
- 有时还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)
Runnable runnable = new Runnable() {
StringBuilder stringBuilder = new StringBuilder();
@Override
public void run() {
for (int n = 1; n<=1000; n++) { stringBuilder.append("a"); }
System.out.println(stringBuilder.length());
System.out.println(stringBuilder.toString());
}
};
new Thread(runnable).start();
new Thread(runnable).start();
1.为什么输出值跟预期值不一样
- 我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
}
- 再看StringBuilder的append()方法
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence
{
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
- StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法
abstract class AbstractStringBuilder implements Appendable, CharSequence {
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;
}
}
- 我们先不管代码的第6行和第7行干了什么,直接看第8行,count += len不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到了第8行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的长度介于1000到2000之间。
2.为什么会抛出ArrayIndexOutOfBoundsException异常
char[] src = new String("hellow").toCharArray();
char[] dest = new String("12345789").toCharArray();
/*
* 开始执行数组复制操作
* 将源数组['h','e','l','l','o','w']从数组下标0开始的4位长度的数组['h','e','l','l']
* 复制到目标数组['1','2','3','4','5','6','7','8'],从下标为3的位置开始
* 如果这个3是大于等于6的数字则会报ArrayIndexOutOfBoundsException异常
*/
System.arraycopy(src, 0 , dest, 3, 4);
- 我们看回AbstractStringBuilder的append()方法源码的第6行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
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;
}
}
- AbstractStringBuilder的append()方法源码的第7行,是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
}
- 假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了第6行的ensureCapacityInternal()方法,此刻count=5。
- 这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了。
- 线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。
四.几个编程例子
// 因为需要通过futureTask来获取多个结果,故一般是一个Callable、多个FutureTask、多个Thread;
// 而不是一个Callable、一个FutureTask、多个Thread;
FutureTask<Integer> futureTask1 = new FutureTask<>(callable);
FutureTask<Integer> futureTask2 = new FutureTask<>(callable);
new Thread(futureTask1).start();
new Thread(futureTask2).start();
System.out.println("futureTask1:" + futureTask1.get());
System.out.println("futureTask2:" + futureTask2.get());
// sum 和 i 都在堆上分配,这段代码线程不安全,输出结果不确定
Callable<Integer> callable = new Callable<Integer>() {
int i = 1;
int sum = 0;
@Override
public Integer call() throws Exception {
for (; i<=100000; i++) { sum++; }
System.out.println(sum);
return sum;
}
};
// sum 和 i 都在栈上分配,这段代码线程安全,输出结果确定等于100000
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i<=100000; i++) { sum++; }
System.out.println(sum);
return sum;
}
};
// str 和 i 都在堆上分配,这段代码线程不安全,输出结果不确定
// StringBuilder换成StringBuffer也是一样线程不安全
Callable<Integer> callable = new Callable<Integer>() {
int i = 1;
StringBuilder str = new StringBuilder();
@Override
public Integer call() throws Exception {
for (; i<=1000; i++) { str.append("a"); }
System.out.println(str);
System.out.println(str.length());
return str.length();
}
};
// str和 i 都在栈上分配,这段代码线程安全,输出结果确定等于1000个a
// StringBuilder换成StringBuffer也是一样
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
StringBuilder str = new StringBuilder();
for (int i = 1; i<=1000; i++) { str.append("a"); }
System.out.println(str);
System.out.println(str.length());
return str.length();
}
};
public class Person {
public int sum;
public StringBuilder str = new StringBuilder();
}
// person在函数内部分配在栈上,故person.sum和person.str线程安全输出结果确定
Runnable runnable = new Runnable() {
@Override
public void run() {
Person person = new Person();
for (int i=1; i<=10000; i++) {
person.sum++;
person.str.append("a");
}
System.out.println("person.sum = " + person.sum);
System.out.println("person.str.length = " + person.str.length());
System.out.println(person.str);
}
};
// person是对象变量分配在堆上,故person.sum和person.str线程不安全输出结果不确定
Runnable runnable = new Runnable() {
Person person = new Person();
@Override
public void run() {
for (int i=1; i<=10000; i++) {
person.sum++;
person.str.append("a");
}
System.out.println("person.sum = " + person.sum);
System.out.println("person.str.length = " + person.str.length());
System.out.println(person.str);
}
};
new Thread(runnable).start();
new Thread(runnable).start();