什么是线程安全性

来源:互联网 发布:网络十大科幻小说 编辑:程序博客网 时间:2024/06/06 20:48

线程安全性是多线程开发中经常讨论的一个概念,是一个基本且很重要的概念,掌握了这个概念才能为后续沟通与开发铺扫除障碍。本人初学多线程作下总结,难免有误,还望本文能抛砖引玉。
1.什么是线程安全性?
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
定义的核心就是“正确的行为”。
下面通过几个例子来理清线程安全的概念,首先看看下面的例子是否是线程安全的,如果不是线程安全那么为什么不安全。
例1:
public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}
例2:
public class Counting{
private long count=0;
public long increment() {
 ++count;
}
}
例3:
public class Singleton {
  private static Singleton instance=null;
  private Singleton(){}
  public static Singleton getInstance() {
      if(instance==null)
      instance=new Singleton();
      return instance;
  }
}

例1中当客户端多个线程访问StatelessFactorizer 的同一个实例的service方法时,service能否表现正确的行为?
答案是能,因为多个线程访问service方法时,方法里面用到的临时变量,只有每个线程自己能访问到,也就是说线程之间根本没有发生数据共享,所以这个类是线程安全。为什么方法里面用到的临时变量只有每个线程自己能访问到,关于线程存储可以参考查阅java内存模型与线程。
这里StatelessFactorizer类是没有状态的,总结就是无状态对象一定是线程安全的。

例2中当客户端多个线程访问Counting的同一个实例的increment方法时,返回结果是否是正确的呢?
这里就不一定了,虽然++count看起来是一条很简单的语句,但是这个操作并非原子的,实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。假设两个线程都做了前面两步还未做第三步,那么都做完第三步后结果肯定是不正确的。

例3大家应该不陌生,就是一个懒汉式的单例类。如果两个线程都调用了Singleton.getInstance(),返回的是否是同一个实例呢?答案是不一定。getInstance方法有三行条语句,假设有两个线程A、B。如果线程A执行了前两行后,这时CPU被线程B抢占了,因为此时instance==null仍然成立,那么线程B也能执行到第二行语句,这样最终两个线程返回的就不是同一个实例了,那么这个单例类在多线程环境下是错误的。

上面例2例3,在多线程环境下都不能表现出正确的行为,那么就不是线程安全的类。

举完几个例子,相信大家对多线程安全概念应该清晰些了。

2.如果你写的代码不是线程安全的,那么将造成难以预料的结果。那么如何能写出线程安全的代码呢?下面我作简单总结。
方式一就是加锁。加锁是最常用的方式,在java中Synchronized就是一个加锁的关键字。加了锁后,只有一把钥匙,哪个线程拿到了钥匙就能访问被锁住的代码。下面我们修改下例2:
public class Counting{
private long count=0;
//这里给increment整个方法加上锁,钥匙就是this对象.
public Synchronized long increment() {
 ++count;
}
}
在修改后的例2中,一次只能有一个线程进入increment方法,也就是说++count里面的三步操作会一次性完成,这样该方法就始终能返回正确的结果了。
方式二线程封闭。这是实现线程安全性的最简单方式之一。如果仅在单线程内访问数据,就不需要同步。线程封闭技术的一种常见应用是JDBC的Connection对象。JDBC规范并不要求Connection对象必须是线程安全的。在典型的服务器应用程序中,线程从连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。由于大多数请求(例如Servlet请求或EJB调用等)都是由单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中。维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

方式三是使用不可变对象。不可变对象一定是线程安全的。如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一。什么是不可变对象?当满足一下条件时,对象才是不可变的:
·对象创建以后起状态就不能修改。
·对象的所有域都是final类型。
·对象是正确创建的(在对象的创建期间,this引用没有逸出)。
所以能用final就用,这也是我们编写代码的基本规范。即使对象是可变的,通过将对象的某些域声明为final类型,仍然可以简化对状态的判断,因此限制对象的可变性也就相当于限制了该对象可能的状态集合。仅包含一个或两个可变状态的“基本不可变”对象仍然比包含多个可变状态的对象简单。通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。

线程安全性就总结这么多了,欢迎大家指正与讨论。