线程安全和共享资源

时间:2020-01-09 10:35:53  来源:igfitidea点击:

多个线程可以同时安全调用的代码称为线程安全。如果一段代码是线程安全的,则它不包含竞争条件。竞争条件仅在多个线程更新共享资源时发生。因此,重要的是要知道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

对于在文件或者其他共享资源上运行的线程,也可能发生这种情况。因此,区分由线程控制的对象是资源还是仅引用资源(就像数据库连接一样)是很重要的。