共享资源能够被多个线程访问且不会形成竟态条件即为线程安全的代码。所以分清哪些资源为共享资源,对于区分代码是否为线程安全至关重要。
线程安全与共享资源
局部变量
基础数据类型
局部变量基础数据类型仅会存储在线程栈中,供本线程使用,所以局部变量基础数据类型是线程安全的。
public void safeMethod() {
int threadSafeInt = 10;
System.out.println(++threadSafeInt);
}
复制代码
对象引用
虽然引用类型本身不会被共享,但引用类型指向的对象是存储在共享堆中的,因此需要判断堆中的对象是否会逃逸出本线程,被其他线程访问到,若该对象仅会在本线程被访问,那么它是线程安全的,若它能够被其他线程所访问那么它将不是线程安全的。
public void method0(){
LocalObject localObject = new LocalObject();
localObject.add(2);
method1(localObject);
}
public void method1(LocalObject localObject){
localObject.add(5);
}
复制代码
method0即使被多个线程调用也不会产生竞态条件,因为局部对象仅会在线程内部创建和访问,即使在method0将该局部对象传递给本对象的其他方法或其他对象的方法也是如此。
对象成员
若多个线程访问多一个对象的成员,将会产生竟态条件。
public class UnSafeObject {
private int count = 0;
public void add(int val) {
int result = this.count + val;
this.count = result;
}
public int getCount() {
return this.count;
}
public static void main(String[] args) {
final UnSafeObject unSafeObject = new UnSafeObject();
Runnable runnable = () -> {
unSafeObject.add(2);
};
IntStream.range(1, 3)
.forEach(i -> new Thread(runnable).start());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
该实例中,两个线程并发访问了UnSafeObject
对象的成员方法add()
。因此该成员变量是线程不安全的。
对以上实例稍加修改:
public static void main(String[] args) {
Runnable runnable = () -> {
final UnSafeObject unSafeObject = new UnSafeObject();
unSafeObject.add(2);
};
IntStream.range(1, 3)
.forEach(i -> new Thread(runnable).start());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
复制代码
让UnSafeObject分别在不同的线程中创建新的实例,这样就会是线程安全的。
事实证明,只要措施得当就能让本身不安全的资源变成安全的。
线程控制逃逸规则
如果一个资源的创建使用和销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则这个资源永远是线程安全的。
资源可以是对象,数组,文件,数据库,套接字等。Java对象的销毁可以指没有任何引用指向该对象。
就算一个对象本身是线程安全的,但是该对象中包含其他不安全资源(文件,数据库连接),则整个对象都不再是线程安全的。
如多个线程创建的数据库连接指向同一个数据库,则有可能多个线程对同一条记录作出更新或插入,此时该数据库资源是线程不安全的。
运行轨迹如下:
线程1: 检查记录x是否存在,记录x不存在
线程2: 检查记录x是否存在,记录x不存在
线程1: 插入记录x
线程2: 插入记录x
复制代码
最后数据库会产生两条一样的记录,不符合预期。
线程安全与不可变性
之所以会产生竞态条件是因为一到多个线程同时访问了相同的对象,且至少有一个线程进行了写操作。多个线程访问不会变化的对象并不会产生线程安全问题。因此可以利用对象的不可变性来规避线程安全问题。不可变性仅能保证对象在多个线程间传递是安全的。但凡有线程对传递的对象进行操作,都会产生新的对象。
done like this:
public class ImmutableExample {
private int val;
public ImmutableExample(int val) {
this.val = val;
}
public int getVal() {
return this.val;
}
public ImmutableExample add(int val) {
return new ImmutableExample(this.val + val);
}
}
复制代码
不可变对象仅能通过构造函数注入数值,没有更新入口,当需要更新数据时,仅能通过构造新对象来实现。
不可变类型的引用
public class Calculator {
private ImmutableExample immutableExample;
public Calculator(ImmutableExample immutableExample) {
this.immutableExample = immutableExample;
}
public ImmutableExample getImmutableExample() {
return immutableExample;
}
public void add(ImmutableExample immutableExample) {
this.immutableExample = this.immutableExample.add(immutableExample.getVal());
}
}
复制代码
实例中Calculator内部引用了一个不可变对象,虽然引用的对象是不可变的。但引用本身却是可变的,因此在多线程环境下,整个Calculator仍然是线程不安全的。使用不可变类型来规避线程安全问题需要牢记这点。
总结
线程安全与共享资源息息相关,所以确定哪些资源是线程不安全的,对于区分代码是否是线程安全的至关重要。
虽然不可变类型可以规避线程安全问题,但是要小心不可变类型的引用,引用不是线程安全的,仍然会导致整个代码是线程不安全的。
该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial