什么是线程安全性?

Posted 玉曲风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是线程安全性?相关的知识,希望对你有一定的参考价值。

定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么久称这个类时线程安全的。

解释:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,

并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

 

  • 在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施。

实例:一个无状态的Servlet

@ThreadSafe
public class SafeThreadTest implements Servlet{
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        encodeIntoRespose(resp,factors);      
    }
}

这个Servlet将请求参数经过提取数值,然后转换后再封装到Servlet的响应中。

这个Servlet与大多数的Servlet相同,SafeThreadTest是无状态的,它不包含包域,也不包含对其他类的域的引用。

计算的过程中的局部变量只存在执行的线程的栈上,访问SafeThreadTest不会影响同一访问该Servlet的另一个线程

的计算结果。所以无状态的对象一定是线程安全的。

 

实例:一个有状态的Servlet

@NotThreadSafe
public class UnsafeThreadTest implements Servlet{
    private long count=0;

    public long getCount(){return count;}

    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        count++;
        encodeIntoRespose(resp,factors);      
    }
}

   我们增加一个统计请求数量,给这个Servlet增加了一个状态,UnsafeThreadTest并非是一个线程安全的,尽管它在单线程中能正确运行。

这个类很可能丢失一些更新操作,看上去这是一个操作,但是这个操作丢失了原子性。实际上它包含了三个独立的操作:读取count,将值+1,

然后将结果写入count,这是一个“读取-->修改-->写入”的操作序列,并且其结果取决于之前的状态 。

  如果在某些情况下,多个线程同时读取到值为10,当都加1,写入11.这样数值将会偏差好多。在并发编程中,这种由于不恰当的执行时序而出现

不正确的结果是一种非常重要的情况,它有一个正式的名字:竞争条件(Race Codition)

竞争条件:当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞争条件(就是说正确的结果要取决于运气)。最常见的竞争类型就是

“先检查后执行(Check-Then-Act)操作”,即通过一个可能失效的观测的结果来决定于下一步动作。

 

实例:延迟初始化中的竞争条件

@NotThreadSafe
public class LazyInitRace{
  private ExprensiveObject instance =null;

  public   ExprensiveObject getInstance(){
    if(instance==null)
        instance=new ExprensiveObject();
        return instance;
    }

}    

如果两个线程同时执行,都产生一个实例,这样实例完全不同了。与大多数并发错误一样,竞争条件并不是总是产生错误的,还需要某种不恰当的执行时序。

 

复合操作:以上的两个含有竞争条件的实例,都需要包含一组需要以原子方式执行(或者说不可分割)的操作。

原子性操作:操作A和B,如果从执行A的线程来看,当另一个线程执行B,要么将B完全执行完,要么完全不执行B,那么A和B对彼此来说是原子的。

 

我们使用AtomicLong类型变量来统计已经处理请求数量

@ThreadSafe
public class CountSafeThreadTest implements Servlet{
    private final AtomicLong count=new AtomicLong(0);

    public long getCount(){return count.get();}

    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        count.incrementAndGet();
        encodeIntoRespose(resp,factors);      
    }
}

AtomicLong以用原子方式更新的 long 值。有关原子变量属性的描述,请参阅 java.util.concurrent.atomic 包规范。

在实际情况中,应该尽可能使用现有的线程安全类来管理类的状态。

 

加锁机制:当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理Servlet的状态来维护Servlet的线程安全性,如果想在Servlet添加更多的状态,那么是否只需要

添加更多的线程安全状态变量就足够了?

  要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

 

内置锁:java提供了一种内置的锁机制来支持原子性:同步代码块。

(1)以synchronized来修饰的方法就是横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在对象。

(2)静态的synchronized方法以Class对象作为锁。(在类加载时,HotSpot虚拟机是在方法区产生java.lang.Class对象)

  synchronized(lock){//访问或修改由锁保护的共享的状态}

每个java对象都可以用作一个实现同步锁,这些锁被称为内置锁或监视器锁。线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。

获取内置锁的唯一途径就是进入这个锁保护的同步代码块或同步方法。java的内置锁相当于一种互斥体,最多只有一个线程持有这个锁。由于每次只有一个

线程执行内置锁保护的代码块。因此,由这个锁保护的同步代码块会以原子性方式执行。并发环境中的原子性和事务应用程序中的原子性有着相同的含义:

一组语句作为一个不可分割的单元被执行。

 

重入:当某个线程请求一个由其它线程持有的锁时,发出请求的线程的就会阻塞。然而,由于内置锁是可以重入的,因此如果某个线程试图获得一个已经有它自己持有

的锁,那么这个请求就会成功。“重入”意味着获取锁的操作粒度是“线程”,而不是调用。

 

于可能被多个线程同时访问的可变状态变量时,在访问它时需要持有同一个锁,在这种情况下,我们称这个状态变量时由这个锁保护的。

每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要同一个锁来保护。

当执行较长时间的计算或者无法快速完成的操作时(例如:网络I/O或者控制台I/O),一定不要持有锁。

 

以上是关于什么是线程安全性?的主要内容,如果未能解决你的问题,请参考以下文章

HashMap 和 ConcurrentHashMap 的区别

什么是线程安全?

什么是线程安全?

什么是线程安全?

什么是线程安全?

第十次总结 线程的异步和同步