Java并发编程(Java Concurrency)(9)- 线程安全与共享资源(Thread Safety and Shared Resources)

时间:2022-09-16 06:58:51

原文链接:http://tutorials.jenkov.com/java-concurrency/thread-safety.html

摘要:这是翻译自一个大概30个小节的关于Java并发编程的入门级教程,原作者Jakob Jenkov,译者Zhenning Lang,转载请注明出处,thanks and have a good time here~~~(希望自己不要留坑)

一段代码如果可以被多线程同时安全访问,那么就说这段代码是线程安全的,也就意味着不存在竞争现象。由于竞争仅发生在多线程写入共享资源的情况,所以知道并把握 Java 线程执行时到底共享哪类资源是至关重要的。

1 函数内部本地(局部)变量(Local Variables)

局部变量保存在每个线程自己的栈中。这意味着局部变量永远不会被其他线程访问到,这也意味着所有局部的简单变量(local primitive variable)都是线程安全的。下面是一个示例:

public void someMethod(){
long threadSafeInt = 0;
threadSafeInt++;
}

2 函数内部对象的局部引用变量(Local Object References)

对于对象的局部引用变量的情况稍有不同。引用变量本身并不是线程共享的,然而对象本身并不是存在每个线程自己的栈中,JAVA 中所有的对象都保存在共享的堆(heap)之中。

如果一个对象一直运行在创建这个对象的方法内部使用,那么这种情况是线程安全的。事实上,即便该对象的引用被传递给了其他函数,只要不被传递到其他线程,就一直是安全的。

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

public void someMethod(){

LocalObject localObject = new LocalObject();

localObject.callMethod();
method2(localObject);
}

public void method2(LocalObject localObject){
localObject.setValue("value");
}

上例中的 LocalObject 的实例即没有从 method2 方法返回给 someMethod 方法,也没有被传递给可以被外界访问的任何对象。每个执行 someMethod 方法的线程都将创建属于自己的 LocalObject 实例并且将其引用赋值给 localObject。因此这一切都使其是线程安全 。

但是,一旦 LocalObject 实例的引用可以被其他线程获取,就将变得线程不安全。

3 对象的成员变量(Object Member Variables)

一个对象的成员变量和对象本身一起都被存储在堆中。因此如果两个线程的方法都对同一个对象的实例进行(写)操作时,就会导致线程的不安全。如下例所示:

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");
}
}

请注意同一个 NotThreadSafe 实例是如何被传递给两个 MyRunnable 实例并被执行的。因此,当两个线程的 add() 方法被执行时会引发竞争。

然而另一种情况下,如果两个线程的 add() 方法处理了不同的 NotThreadSafe 实例,并不会引发竞争,修改后的代码如下所示:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

如此一来两个线程的 add() 方法仅处理其自己的 NotThreadSafe 实例,也就不会相互影响,竞争就不会发生了。

4 线程控制逃离准则(The Thread Control Escape Rule)

为了确定获取特定资源的一段代码是否是线程安全的,可以使用“线程控制逃离准则”:

如果一个资源的创建、使用和回收都在同一个线程内完成的,并且从来没有逃离这个线程的控制域,那么该资源就是线程安全的。

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 中并不显式地回收资源,所以这里的“回收”意味着对对象引用的不再使用或者置为 null。

即便一个对象是线程安全的,如果这个对象指向了一个另外的共享资源(如文件或数据库),那么整个程序很可能并不是线程安全的。例如,如果线程 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

这种情况也可能发生在对文件或者其他共享资源的操作上。因此,一定要区分一个线程所控制的对象到底是资源本身还是指向资源的一个引用