`线程安全性

对象的状态指存储在状态变量(例如实例或者静态域)中的数据,对象的状态可能包括其他依赖对象的域。

要编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。“共享”意味着变量可以由多个线程同时访问,“可变”意味着变量的值在其生命周期内可以发生变化

什么是线程安全性?

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的!

无状态的对象一定是线程安全的!

 

什么叫无状态的对象:无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有成员变量和其它类中成员对象的引用的对象。

下面的这个类的对象就是无状态对象。这个类主要用来对请求中的参数进行因式分解,计算过程中的临时状态仅存在线程栈上的局部变量中,并且只能由正在执行的线程访问。

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

  

原子性

一个操作是不可分割的,就是原子性的操作,假如一个操作能够分成几个独立的操作,就不是原子性的。

需要增加命中计数器来统计所处理的请求数量。可以在Servlet中增加一个long类型的成员变量,每处理一个请求这个值就加1,但这不是线程安全的。因为++count虽然只有1行,但包含了三个独立的操作:读取count的值,将值加1,再将值的结果写入count。这是一个“读取--修改--写入”的操作序列,并且其结果状态依赖于之前的状态。如果计数器的初始值是9,而碰巧在某些情况下,两个线程读到的值都是9,那么最后计数器的赋值为10。与实际值偏差1。

@NotThreadSafe  
public class UnsafeCountingFactorizer extends GenericServlet 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;  
        encodeIntoResponse(resp, factors);  
    }  
}  

  

在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,这种情况叫做:

竞态条件


当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。

举例:

你和朋友约定好了在一条街上的咖啡馆见面,但是这条街上有两家同样的咖啡馆,你不知道要碰面的是哪家,十二点十分,你在咖啡馆A没有看到朋友,就去咖啡馆B,十二点十五,你到达了咖啡馆B,也没有发现朋友,但可能你朋友在这段时间内也到过了这两家咖啡馆,只是你到的时候他没在,你不在的时候他在了。在这个“你要看看他在不在另一家店的问题里”,问题在于首先看了看A,发现不在,就要去B找,在B中可以做出同样的选择,但是A和B之间需要一段路程,在十二点十分到十二点十五这段时间内可能系统的状态已经发生了变化,当你刚迈出A的前门,朋友从A的后门进来了......

最常见的竞态条件类型就是“先检查后执行”,基于一种可能失效的观察结果来做出判断或者执行某个计算:一个线程观察到某个结果为真,进行下一步操作,但事实上可能另一个线程再次之前已经执行了这个操作,这个观察结果已经无效了,从而导致各种问题的出现

竞态条件容易与数据竞争混淆:数据竞争是指访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现数据竞争。当一个线程写入一个变量而另一个线程接下来读取这个变量,或者读取一个之前有另一个线程写入的变量时,并且在这两个线程之间没有使用同步,那么可能出现数据竞争。

示例:延迟初始化中的竞态条件

public class LazyInitRace{
 private ExpensiveObject instance=null;
 public ExpensiveObject getInstance(){
   if(instance==null){
      instance=new EExpensiveObject;
    }
     return instance
  }
}

延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次,在LazyInitRace中存在一个竞态条件。假定线程A和线程B同时执行getInstance。A看到instance为空,创建一个新的实例,B同样需要判断instance是否为空,此时的instance是否为空,取决于不可预测的时序,包括线程的调度方式,以及A需要花多长时间来初始化ExpensiveObject并设置instance。如果当B检查时instance为空,那么在两次调用getInstance时可能会得到不同的结果,即使getInstance通常被认为是返回相同的实例

复合操作:

包含了一组必须以原子方式执行的操作以确保线程安全性

比如“读取-修改-写入”和“先检查后执行”等操作,为了保证线程安全,必须保证操作是原子性的,这样的操作统称为复合操作

加锁机制

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

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

synchronized(lock){

//访问或修改由锁保护的共享状态

}

每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程再进入同步代码块的时候会自动获得锁,出来的时候自动释放锁,获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法

原文地址:https://www.cnblogs.com/wxw7blog/p/7145162.html