线程安全和共享资源
多个线程可以同时安全调用的代码称为线程安全。如果一段代码是线程安全的,则它不包含竞争条件。竞争条件仅在多个线程更新共享资源时发生。因此,重要的是要知道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方法的调用不会相互干扰。该代码不再具有竞争条件。因此,即使一个对象不是线程安全的,它仍然可以以不会导致竞争状态的方式使用。
线程控制转义规则
尝试确定代码对特定资源的访问是否是线程安全的时,可以使用线程控制转义规则:
If a resource is created, used and disposed within the control of the same thread, and never escapes the control of this thread, the use of that resource is thread safe.
资源可以是任何共享资源,例如对象,数组,文件,数据库连接,套接字等。在Java中,我们并不总是显式地布置对象,因此" disposed"意味着丢失或者取消对对象的引用。
即使使用对象是线程安全的,但是如果该对象指向文件或者数据库之类的共享资源,则整个应用程序可能也不是线程安全的。例如,如果线程1和线程2各自创建自己的数据库连接,即连接1和连接2,则每个连接本身的使用都是线程安全的。但是使用连接指向的数据库可能不是线程安全的。例如,如果两个线程都执行如下代码:
check if record X exists if not, insert record X
如果两个线程同时执行此操作,并且它们正在检查的记录X恰好是同一条记录,则存在两个线程最终都将其插入的风险。这是这样的:
Thread 1 checks if record X exists. Result = no Thread 2 checks if record X exists. Result = no Thread 1 inserts record X Thread 2 inserts record X
对于在文件或者其他共享资源上运行的线程,也可能发生这种情况。因此,区分由线程控制的对象是资源还是仅引用资源(就像数据库连接一样)是很重要的。