被多个线程同时安全调用的代码称为线程安全。如果一段代码是线程安全的,那么它就不包含竞态条件。竞态条件只出现在多个线程更新共享资源时。因此,了解 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");
}
这个例子中的 LocalObject
实例没有被方法返回,也没有传递给从 someMethod()
方法外面可以访问的任何其它对象。每个执行 someMethod()
方法的线程会创建它自己的 LocalObject
实例,并将其赋值给 localObject
引用。因此,这里 LocalObject
的使用是线程安全的。
实际上,整个 someMethod()
方法都是线程安全的。即使 LocalObject
实例是被作为参数传递给同一个类或者其它类中的其它方法,它的使用也是线程安全的。
当然,唯一的例外是,如果用 LocalObject
作为参数调用的方法之一,以允许从其它线程访问它的方式来存储 LocalObject
实例。
对象成员变量
对象成员变量(字段)与对象一起存储在堆上。因此,如果两个线程在同一个对象实例上调用一个方法,而这个方法会更新对象成员变量,那么该方法就不是线程安全的。如下是一个不是线程安全的方法的示例:
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(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();
public class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
public void run(){
this.instance.add("some text");
}
}
请注意,两个 MyRunnable
实例共享同一个 NotThreadSafe
实例。因此,当它们在 NotThreadSafe
实例上调用 add()
方法时,就会导致竞态条件。
不过,如果两个线程同时调用的是不同实例上的 add()
方法,那么就不会导致竞态条件。以下是对上例稍作修改后的例子:
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
现在这两个线程都有它们各自的 NotThreadSafe
实例,所有它们对 add()
方法的调用不会相互干扰。代码也不会再有竞态条件。所以,即使一个对象不是线程安全的,它依然可以以不会导致竞态条件的方式来使用。
线程控制逃逸规则
当试图确定访问某个资源的代码是否是线程安全时,可以用线程控制逃逸规则:
如果一个资源是在同一个线程的控制内被创建、使用和销毁的,并且从未逃脱这个线程的控制,那么该资源的是使用就是线程安全的。
资源可以是任何共享资源,比如对象、数组、文件、数据库链接、socket 等。在 Java 中,我们并不总是显式销毁对象,所以“销毁”就是指丢失对对象的应用(设置为 null)。
即使一个对象的使用是线程安全的,如果该对象指向文件或者数据库这样的共享资源,那么整个应用程序可能都不是线程安全的。比如,如果线程 1 和线程 2 各自都创建了自己的数据库连接 connection 1 和 connection 2,每个连接本身的使用都是线程安全的。但是连接指向的数据库的使用可能不是线程安全的。比如,如果两个线程都执行像下面这样的代码:
检查记录 X 是否存在
如果不存在,就插入记录 X
如果两个线程同时执行这段代码,而它们要检查的记录 X 恰好是同一条记录,那么就有这两个线程最终都插入它的风险。比如:
线程 1 检查记录 X 是否存在。结果为 no
线程 2 检查记录 X 是否存在。结果为 no
线程 1 插入记录 X
线程 2 插入记录 X
线程操作文件或者其它共享资源的时候也有可能发生这种情况。因此,重要的是,要区分被一个线程控制的对象是资源,还是仅仅是资源的引用(就像数据库连接所做的那样)。