被多个线程同时安全调用的代码称为线程安全。如果一段代码是线程安全的,那么它就不包含竞态条件。竞态条件只出现在多个线程更新共享资源时。因此,了解 Java 线程在执行时共享哪些资源是很重要的。

局部变量

局部变量存储在每个线程自己的栈中。也就是说,局部变量不会在线程之间共享。这也意味着所有局部基础类型变量都是线程安全的。如下是线程安全局部基础类型变量的一个示例:

  1. public void someMethod(){
  2. long threadSafeInt = 0;
  3. threadSafeInt++;
  4. }

局部对象引用

局部对象引用有点不同。引用本身是不共享的。不过,被引用的对象不是存储在每个线程的本地栈中。所有对象都是存储在共享的栈中。

如果一个在本地创建的对象从没逃出创建它的方法,那么它就是线程安全的。实际上,还可以把这个对象传给其它方法和对象,只要这些方法或者对象都不让传进来的对象对其它线程可用。

如下是一个线程安全的局部对象的实例:

  1. public void someMethod(){
  2. LocalObject localObject = new LocalObject();
  3. localObject.callMethod();
  4. method2(localObject);
  5. }
  6. public void method2(LocalObject localObject){
  7. localObject.setValue("value");
  8. }

这个例子中的 LocalObject 实例没有被方法返回,也没有传递给从 someMethod() 方法外面可以访问的任何其它对象。每个执行 someMethod() 方法的线程会创建它自己的 LocalObject 实例,并将其赋值给 localObject 引用。因此,这里 LocalObject 的使用是线程安全的。

实际上,整个 someMethod() 方法都是线程安全的。即使 LocalObject 实例是被作为参数传递给同一个类或者其它类中的其它方法,它的使用也是线程安全的。

当然,唯一的例外是,如果用 LocalObject 作为参数调用的方法之一,以允许从其它线程访问它的方式来存储 LocalObject 实例。

对象成员变量

对象成员变量(字段)与对象一起存储在堆上。因此,如果两个线程在同一个对象实例上调用一个方法,而这个方法会更新对象成员变量,那么该方法就不是线程安全的。如下是一个不是线程安全的方法的示例:

  1. public class NotThreadSafe{
  2. StringBuilder builder = new StringBuilder();
  3. public add(String text){
  4. this.builder.append(text);
  5. }
  6. }

如果两个线程同时调用同一个 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

线程操作文件或者其它共享资源的时候也有可能发生这种情况。因此,重要的是,要区分被一个线程控制的对象是资源,还是仅仅是资源的引用(就像数据库连接所做的那样)。