回顾
上一篇中我们主要介绍了并发编程的成本:上下文切换导致的资源消耗、代码结构变得更为复杂,以及三种并发编程模型:并行工作者模型、流水线模型、函数式并行,我们详细介绍了三种模型的优缺点,但目前为止都还十分的抽象。
本篇我们主要会介绍临界区、竞态以及线程安全。
1.竞态条件&临界区
public class Counter {
public long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
在以上代码中,同一对象的add()方法可以被多个线程同时调用,这里出现了多个线程竞争修改同一资源——成员变量counter
的情况,这种情况可以称为竞态条件,而add方法代码块以内称之为临界区。
2.线程安全
允许多个线程同时执行的代码称之为线程安全的代码,线程安全的代码内部不会产生竞态条件。由于竞态条件是因为对共享资源的竞争引发的,所以我们有必要了解Java线程执行时共享了什么资源。
2.1 局部变量
局部变量存储在线程自己的栈中,这意味着局部变量永远不会被多线程共享,所以,基础数据类型的局部变量是线程安全的。
这里我们特别强调基础数据类型的局部,因为引用类型的局部变量存储在堆中,和基础类型有点不同。
2.2 引用类型的局部变量
尽管引用类型的变量本身没有被共享,但是引用所指向的对象并没有被存储在线程的栈内,所有的对象都存储在共享的堆内存中。只要在方法中创建的变量没有逃逸出该方法(即该对象不会被其他方法获得,也不会被其他非局部变量引用),那么这个引用就是线程安全的。实际上,即使将它作为参数传递给其它方法,只要别的线程获取不到这个对象,那么它就依然是线程安全的。
public void someMethod(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
上面的代码中是一个典型的局部引用的案例,localObject 没有被返回,也没有被传递给someMethod()方法以外的任何对象,每个执行someMethod()的线程都会创建自己的localObject,因此localObject是线程安全的,事实上整个someMethod()都是线程安全的。
2.3 对象成员
对象成员存储在堆上,如果两个线程同时修改一个对象的成员变量,那么这个代码就是线程不安全的,看实例:
public class NotSafe {
//一个成员变量
private StringBuilder builder=new StringBuilder();
public void add(String str){
builder.append(str);
}
}
多线程修改成员变量,我们先实现一个自己的线程:
public class MyRunnable implements Runnable {
private NotSafe notSafe=null;
public MyRunnable(NotSafe notSafe) {
this.notSafe = notSafe;
}
@Override
public void run() {
notSafe.add("some str");
}
}
NotSafe shared=new NotSafe();
Thread t7=new Thread(new MyRunnable(shared),"t7");
Thread t8=new Thread(new MyRunnable(shared),"t8");
t7.start();
t8.start();
这里我们人为地制造了竞态条件,这就出现了两个线程修改对象成员变量的场景。
当然我们还可以通过新建两个NotSafe对象分别传入到MyRunnable中,这样就可以避免多个线程修改同一对象的成员,如下:
NotSafe shared1=new NotSafe();
NotSafe shared2=new NotSafe();
Thread t7=new Thread(new MyRunnable(shared1),"t7");
Thread t8=new Thread(new MyRunnable(shared2),"t8");
t7.start();
t8.start();
2.4 线程控制逃逸规则
线程控制逃逸规则可以帮助我们更好地判断某些资源的访问是否是线程安全的:
如果一个资源的创建、使用、销毁都是在一个线程内完成的,且永远不会脱离线程的控制,那么它就是线程安全的。
这里资源可以是:对象,数组,文件,数据库连接,套接字等等。Java中我们无需手动销毁资源,所以这里指的是不再有引用指向该资源即可。
即使对象本身线程安全,但如果该对象中包含了其他资源,整个应用也许就不再是线程安全的了。例如,两个线程各自持有一个数据库的连接,连接本身是线程安全的,但是当它们同时对数据库进行操作时,如果操作的是同一行记录,那么依然是线程不安全的。
2.5 不可变性
我们再次强调竞态条件是多个线程同时对同一资源进行写操作,多个线程同时对一个资源进行读操作并不构成竞态。我们可以通过创建不可变的对象来保证它在多线程,但不可变和只读是有区别的,我们设计一个Person
类,那么当它的birthday
字段设置好之后,那么它的age
字段就是只读的,但随着时间的推移,age依然会发生变化,所以它是可变的。
不可变对象则是通过构造方法赋值,但是只有get方法而没有set方法。
public class ImmutableValue {
private int value;
public ImmutableValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public ImmutableValue add(int addV){
return new ImmutableValue(this.value+addV);
}
}
在上面的不可变类型中,我们依然可以添加方法对其进行操作,但其实返回的都是一个新建的ImmutableValue
实例对象,而不是原对象。
我们需要注意,即使一个对象是不可变对象,但持有这个对象引用的类不是不可变的,那么这个类不是线程安全的,这一点需要铭记。