线程安全和共享资源
能够同时被多个线程安全地调用的代码,就是线程安全。如果一段代码是线程安全的,就说明它没有竞态。竞态只会在多线程更新共享资源的时候出现。所以,知道 Java 在执行的时候是共享了什么资源是很重要的。
局部变量
局部变量存储在每个线程自己的栈中。也就是说局部变量不会再多个线程中共享。也意味着本地的基本类型的变量是线程安全的。
public void someMethod() {
long threadSafeInt = 0;
threadSafeInt++;
}
局部对象引用
局部对象引用有些不同。尽管引用没有被共享,但是引用指向的对象不是存储在线程的栈中,而是存放在共享堆中。
如果一个对象没有从创建该对象的方法逃离,它就是线程安全的。实际上,你可以把这个对象传递给其他方法或者对象使用,只要这个对象没有被传递到其他线程就行。
下面是一个线程安全的局部对象的例子
public void someMethod() {
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject) {
localObject。setValue("value");
}
对象的成员变量
对象的成员变量和对象一起存放在堆中。因此,如果两个县城调用了同一个对象中的方法,并且该方法改变了成员变量的值,那么这个方法就不是线程安全的。
下面例子中的方法不是线程安全的
public class NotThreadSafe {
StringBuilder builder = new StringBuilder();
public add(String text) {
this.builder.append(text);
}
}
如果两个线程同时调用了同一个NotThreadSafe实例的 add()
方法,就会导致竞态。比如
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(shareInstance)).start();
new Thread(new MyRunnable(shareInstance)).start();
public class MyRunnable implements Runnable {
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance) {
this.instance = instance;
}
public void run() {
this.instance.add("some text");
}
}
可以看到,两个线程共享的是同一个 NotThreadSafe 实例,因此当它们调用 add()
方法时,就导致了竞态。
如果两个线程调用的是不同的实例的话,即使这两个线程是同时调用,就不会产生竞态了,因此上面的代码可以这么修改
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
所以,即使一个对象不是线程安全的,也可以换一种方法来避免竞态。
线程控制逃逸规则
如果你想判断你的代码访问资源的时候是否是线程安全的,可以借助县城控制逃逸规则:
如果资源的创建、使用和销毁都是在同一个线程内,并且从不脱离线程的控制。那么对该资源的使用就是线程安全的。
不过,即使对象的使用时线程安全的,但是如果对象指向的是一个共享资源,比如文件或者数据库,那么你的应用整体上来说就不是线程安全了。比如线程1和线程2都创建了一个数据库连接:connection 1 和 connection 2。connection 1 和 connection 2 的使用时线程安全的,但是connection 1 和 connection 2 指向的数据库也许不是线程安全的。比如两个线程都执行如下操作
检查记录 X 是否存在
如果不存在,就插入记录 X
如果两个线程同时执行,就有能出现同时插入的隐患
线程 1 检查记录 X 是否存在:不存在
线程 2 检查记录 X 是否存在:不存在
线程 1 插入记录 X
线程 2 插入记录 X
这种情况也有可能发生在线程操作文件或者其他共享资源的时候。所以,区分对象代表的是资源本身,还是指向资源的引用(比如数据库连接这样的对象)。