什么是线程安全?

Posted

tags:

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

线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

当对一个复杂对象进行某种操作时,从操作开始到操作结束,被操作的对象往往会经历若干非法的中间状态。调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。

如果其他线程企图访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避免这种情况发生是线程安全性的核心问题。

扩展资料:


简单类比

线程安全性问题跟外科医生做手术有点象,尽管手术的目的是改善患者的健康,但医生把手术过程分成了几个步骤,每个步骤如果不是完全结束的话,都会严重损害患者的健康。

想想看,如果一个医生切开患者的胸腔后要休三周假会怎么样?然而单线程的程序中是不存在这种问题的,因为在一个线程更新某对象的时候不会有其他线程也去操作同一个对象。

(除非其中有异常,异常是可能导致上述问题的。当一个正在更新某对象的线程因异常而中断更新过程后,再去访问没有完全更新的对象,会出现同样的问题)

参考技术A 线程安全是指如果在进程中有多个线程同时运营,而这些线程同时运行一段代码,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不使得每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的。 参考技术B “线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。” 参考技术C 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
线程安全就是说多线程访问同一代码,不会产生不确定的结果。编写线程安全的代码是低依靠线程同步。
在接口方式中,线程有一个共享的数据成员,即:
private int count =10;
而在继承方式中,线程之间没有共享的成员,而是各线程各自有一个私有成员,即: private
int count
=10;
在多线程环境中,当各线程不共享数据的时候,那么一定是线程安全的。问题是这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized就是一段代码同时只能有一个线程来操作,不然中间过程可能会产生不可预制的结果。

什么是线程安全性?

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

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

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

 

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

实例:一个无状态的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),一定不要持有锁。

 

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

什么是线程安全与非线程安全?

什么是线程安全?

为什么会有多线程?什么是线程安全?如何保证线程安全?(带详细例子)

什么是线程安全?

什么是线程安全,实现线程安全都有哪些方法

什么是线程安全与线程不安全