Jakob Jenkov 2015-10-01
代码如果能够被多个线程同时调用而不会引发问题,就称为线程安全。如果一段代码是线程安全的,那么它不包含竞态条件(race conditions)。竞态条件只会在多个线程同时更新共享资源时发生。因此,了解 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() 方法外部访问的其他对象。每个执行 someMethod() 方法的线程都会创建自己的 LocalObject 实例,并将其赋值给 localObject 引用。因此,此处对 LocalObject 的使用是线程安全的。
事实上,整个 someMethod() 方法都是线程安全的。即使将 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 实例。因此,当它们调用该实例的 add() 方法时,就会产生竞态条件。
然而,如果两个线程分别在不同的实例上调用 add() 方法,则不会导致竞态条件。以下是稍作修改的示例:
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
现在,两个线程各自拥有自己的 NotThreadSafe 实例,因此它们对 add 方法的调用互不干扰,不再存在竞态条件。所以,即使一个对象本身不是线程安全的,只要使用方式得当(比如每个线程使用独立实例),仍然可以避免竞态条件。
线程控制逃逸规则
在判断某段代码对特定资源的访问是否线程安全时,可以使用以下线程控制逃逸规则:
如果一个资源在其同一个线程的控制下被创建、使用和销毁,
并且从未逃逸出该线程的控制范围,
那么对该资源的使用就是线程安全的。
这里的“资源”可以是任何共享资源,例如对象、数组、文件、数据库连接、套接字等。在 Java 中,我们通常不会显式地“销毁”对象,因此“销毁”在这里指的是失去对该对象的引用或将引用置为 null。
需要注意的是:即使某个对象的使用是线程安全的,但如果该对象指向一个共享资源(如文件或数据库),那么整个应用程序可能仍然不是线程安全的。
例如,线程 1 和线程 2 各自创建了自己的数据库连接(connection1 和 connection2),每个连接本身的使用是线程安全的。但它们所指向的数据库的使用却可能不是线程安全的。比如,如果两个线程都执行如下逻辑:
检查记录 X 是否存在
如果不存在,则插入记录 X
如果两个线程同时执行这段代码,并且它们检查的是同一条记录 X,就有可能出现两个线程都插入同一条记录的情况。过程如下:
- 线程 1 检查记录 X 是否存在 → 结果:不存在
- 线程 2 检查记录 X 是否存在 → 结果:不存在
- 线程 1 插入记录 X
- 线程 2 插入记录 X
这种情况也可能发生在操作文件或其他共享资源的线程中。因此,重要的是要区分清楚:线程控制的对象本身是否就是资源,还是仅仅是对资源的引用(例如数据库连接只是对数据库的引用)。