本系列译自jakob jenkov的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作为其他方法的参数使用,在某种程度上来说,它是可以被其他线程访问。
对象的成员变量
对象的成员变量是和对象一起存储在堆里的,因此,如果两个线程同时访问一个方法的相同对象并且这个方法会更新这个成员变量时,这个方法就不是线程安全的。下面是是一个线程不安全的方法。
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{
NotThreadSate instance = null;
public MyRunable(NotThreadSafe instance){
this.instance = instance;
}
@override
public void run(){
this.instance.add("some text");
}
}
注意,两个MyRunnable线程实例共享一个NotThreadSafe实例,因此,当他们NotThreadSafe实例上调用add()方法时,会导致竞态条件。
然而,当两个线程同时在不同的NotThreadSafe实例上调用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 whthin the control of the same thread,and never escapes the control of this thread,the use of that resource is thread safe.
这里共享资源可以是一个对象、数组、文件、数据库连接、socket等,在java言中,你不可能清楚的知道对象是否销毁,销毁意味着失去对象的引用或是对象为null.
即使对象的引用是线程安全的,但是如果这个对象指向的是共享资源如文件或是数据库,你的应用有可能也不是线程不安全的。例如:线程1和线程2都各自己创建他们的数据库连接,他们各自的数据库连接是线程安全的,但是使用数据库的连接可能不是线程安全的。例如:如果两个线程如下面的代码一样执行。
check if record X exists
if not insert record x
如果两个线程同时执行,record x是检测的是同一条记录,这里有个风险就是两个线程都插入了record x
Thread 1 checks if record x exists. Result = no
Thread 2 checks if record x exists. Result = no
Thread1 insert record x
Thread2 insert record x
这种情况也可能发生在线程操作文件或是其他的共享资源,因此,区分线程控制对象,还是仅仅引用对象是非常重要的。