多线程系列:竞态临界区、共享资源

竞态临界区

在同一程序中运行多个线程本身不会导致问题,问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

多线程同时执行下面的代码可能会出错:

1 public class Counter {
2     protected long count = 0;
3     public void add(long value){
4         this.count = this.count + value;  
5     }
6 }

想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:

从内存获取 this.count 的值放到寄存器
将寄存器中的值增加value
将寄存器中的值写回内存
观察线程A和B交错执行会发生什么,两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。

竞态条件(Race Condition):当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上面的add方法就是临界区。可以这样理解:临界区的访问需要的是互斥。

共享资源:

局部变量

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。

局部的对象引用

局部的对象引用

对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(译者注:即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例:

01 public void someMethod(){
02    
03   LocalObject localObject = new LocalObject();
04  
05   localObject.callMethod();
06   method2(localObject);
07 }
08  
09 public void method2(LocalObject localObject){
10   localObject.setValue("value");
11 }

样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了.

对象成员

对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:

1 public class NotThreadSafe{
2     StringBuilder builder = new StringBuilder();
3      
4     public add(String text){
5         this.builder.append(text);
6     }  
7 }

如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如:

01 NotThreadSafe sharedInstance = new NotThreadSafe();
02  
03 new Thread(new MyRunnable(sharedInstance)).start();
04 new Thread(new MyRunnable(sharedInstance)).start();
05  
06 public class MyRunnable implements Runnable{
07   NotThreadSafe instance = null;
08    
09   public MyRunnable(NotThreadSafe instance){
10     this.instance = instance;
11   }
12  
13   public void run(){
14     this.instance.add("some text");
15   }
16 }

注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。

当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子:

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

现在两个线程都有自己单独的NotThreadSafe对象,调用add()方法时就会互不干扰,再也不会有竞态条件问题了。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。

总结:

1.局部变量中的基本数据类型(8种)永远是线程安全的。

2.局部变量中的对象类型只要不会被其他线程访问到,也是线程安全的。
3.一个对象实例被多个线程同时访问时,他的成员变量就可能是线程不安全的。


原文地址:

http://ifeve.com/race-conditions-and-critical-sections/  

http://ifeve.com/thread-safety/

你可能感兴趣的:(多线程系列:竞态临界区、共享资源)