线程安全和资源共享(Thread Safety and Shared Resources)

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

可以被多个线程同时调用的安全代码叫做线程安全。如果一段代码是线程安全的,它是不会包含竞争条件的。竞争条件只发生在多个线程更改共享资源的时候。因此,了解 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");
    }

在上面例子中的局部对象的实例没有被方法 method2 返回,也没有传递给在 someMethod() 方法之外可以访问这个局部对象的其他对象。每个执行 someMethod() 方法的线程都会创建一个它自己的局部对象实例然后分配给局部对象引用。因此,在这儿使用局部对象 localObject 是线程安全的。事实上,整个 someMethod() 方法是线程安全的。即使局部对象的实例 localObject 被当作参数传递给同一个类的其他方法内或者其他类来使用它,都是线程安全的。唯一例外的是,如果调用方法的时候局部对象 localObject 作为参数被保存了下来,其他线程可以通过某种手段访问到它,这种情况下该局部对象不是线程安全的。

成员对象

成员对象随着对象被保存到共享的堆中。因此,如果两个线程调用了同一个对象实例上的同一个方法并且更改了成员对象,这个方法就不是线程安全的。下面这个例子中的方法就不是线程安全的:

    public class NotThreadSafe {
        StringBuilder builder = new StringBuilder();
        public void 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 类的同一个实例的方式共享了对象。因此,两个线程在 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.

    如果一个资源的创建、使用、释放都在同一个线程的控制下并且这个资源不会逃逸出这个线程的控制范围,那么对这个资源的使用就是线程安全的。

可以是任何共享的资源,比如对象、数组、文件、数据库连接、 socket 等。在 Java 中不需要程序员显式的释放对象,所以“被释放(disposed)”的意思是最终没有地方使用这个对象或者是把这个对象上的引用置为 null 。

即使对一个对象的使用是线程安全的,如果这个对象又指向了一个共享的资源,比如一个文件或数据库,作为一个整体,应用程序未必就是线程安全的。例如,线程 1 和线程 2 各自都创建了它们自己的数据库连接 1 和数据库连接 2 ,每个线程使用自己的数据库连接本身是线程安全的。但是,对数据库连接指向资源的使用未必是线程安全的。例如,两个线程都执行了如下的代码:

    check if record X exists
    if not, insert record X
    检查记录 X 存不存在,如果不存在的话插入记录 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

这种情况也可能发生在多个线程操作共享的文件或其他资源上。因此,能够分辨出被一个线程控制的对象是资源本身还是被控制的对象仅仅是资源的引用是很重要的。

Next:线程安全和不可变性

你可能感兴趣的:(线程安全,资源共享)