第二章-线程安全性

  • 线程安全核心:
对状态访问的控制,特别是共享(shared)和可变(Mutable)状态的访问
  • 共享:
由多个线程同时访问
  • 可变:
在变量的生命周期内可发生变化
  • 同步:
包括voliatile变量、synchronized、显示锁(Explicit Lock)、原子变量
  • 如何修复多个线程访问可变状态是发生的错误:
1.不在线程间共享该变量
2.将状态变量变为不可变的变量
3.在访问状态变量时使用同步

2.1 什么是线程安全性

  • 无状态对象一定是线程安全的
    • 线程访问无状态对象的行为不会影响其他线程中操作的正确性
@ThreadSafe
public class StatelessFactorizer implements Servlet{
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        encodeIntoResponse(factors,resp);
    }
}

这个例子中StatelessFactorizer就是无状态的,他既不包含任何域,也不包含对其他类中域的引用,。计算中的临时状态仅存于线程栈(线程独立)上的局部变量表中,并且只能由正在执行的线程访问

2.2 原子性

2.2.1 竞态条件
  • 竞态条件
当某个计算结果的正确性取决于多线程交替执行的时序时,就会发生竞态条件,简而言之就是靠运气。
最常见的就是“先检查后执行(Check-Then-Act)”
2.2.2 示例:延迟初始化时的竞态条件
@NotThreadSafe
public class LazyInitRace{
    private ExpenciveObject instance=null;
    
    public ExpenciveObject getInstance(){
        if(instance==null){
            instance=new ExpenciveObject(); 
        }
        return instance;
    }
}

假设有两个线程A和B同时访问getInstance,A看到instance对象为空创建一个ExpenciveObject对象,B线程同样需要判断instance对象是否为空,但是instance是否为空需要取决于不可预测的时序,包括线程的调度方式,A线程初始化ExpenciveObject并设置instance所耗费的时间。如果当B线程判断instance为空,则会新建一个ExpenciveObject对象,那么这两个线程调用getInstance时得到的就是两个不同的结果。

2.2.3 复合操作
  • 避免竞态条件
在某个线程修改变量时,通过某种方式阻止其他线程使用这个变量。从而确保其他线程只能在修改之前或者修改之后读取和修改状态,而不是在修改状态的过程中
  • 原子操作
对于访问同一个状态的所有操作(包括该操作本身),这个操作是以原子方式执行的操作。
假定线程A和B,如果从执行A线程的角度来说,当另一个线程B执行时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说都是原子的
@ThreadSafe
public class CountingFactorizer 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();
        encodeIntoResponse(factors,resp);
    }
}

AtomicLong是一个原子变量类,在java.concurrent.atomic包中,该包还含有其他一些原子变量类,用于实现在数值和对象引用上的原子状态转换,上面代码用例通过AtomicLong代替long类型计数器,能够确保所有对计数器的访问都是原子的,由于上面代码Servlet状态就是计数器的状态,并且计数器是线程安全的,因此Servlet也是线程安全的

2.3 加锁操作

  • 对多个线程安全状态变量操作,并不能保证线程安全。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
    private final AtomicReference lastNumber=new AtomicReference();
    
    private final AtomicReference lastFactors=new AtomicReference<>();
    
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        if(i.equals(lastNumber.get())){
            encodeIntoResponse(factors,resp);
        }else{
            BigInteger[] factors=factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(factors,resp);
        }
    }
}

上方代码中,在使用原子引用的情况下,尽管对set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastFactors。如果只修改了其中一个变量,那么在这两次修改操作之间,其他线程将发现不变性条件被破坏了。同样,也不能保证会同时获取两个值:在线程A获取这两个值得过程中,线程B可能修改了它们,这样线程A发现不变性条件被破坏了

2.3.1 内置锁
  • 同步代码块(Synchronized Block称为内置锁或监视器锁)
    • 锁的对象引用
    • 由这个锁保护的代码块(静态的synchronized方法以class对象作为锁)
  • 进入同步代码块自动获得锁,退出同步代码块(无论是正常退出还是抛出异常)自动释放锁
  • JAVA内置锁相当于一种互斥锁,最多只有一个线程能持有这种锁,其他未持有锁的线程必须等待或阻塞,直到持有这个锁的线程释放锁,否则将一直等待下去。
  • 由这个锁保护的同步代码块会以原子方式执行
@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
    private final AtomicReference lastNumber=new AtomicReference();
    
    private final AtomicReference lastFactors=new AtomicReference<>();
    
    public synchronized void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        if(i.equals(lastNumber.get())){
            encodeIntoResponse(factors,resp);
        }else{
            BigInteger[] factors=factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(factors,resp);
        }
    }
}

上面代码在service方法上加了synchronized后线程安全了,但是假如多个请求同时访问这个方法,由于同一时刻只有一个线程可以执行该方法,会导致性能很低。

2.3.2 重入
  • 内置锁是可重入的
当一个线程试图获得一个已经由他自己持有的锁,这个请求可以成功。
  • 获取锁的粒度是“线程”不是“调用”
  • 实现方式
为每个锁关联一个获取计数值和所有者线程,当计数值为0则认为该锁没有被任何线程持有。
当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并加计数值加设为1。
如果这个线程再次获得锁,计数值递增。
当线程退出同步代码块时,计数值递减,当计数值到0时,这个锁将被释放。

2.4 用锁来保护状态

  • 锁保护状态
对于可能被多个线程访问的可变状态变量,在访问它时都需要持有同一个锁。
  • 加锁约定
将所有的可变状态变量(前提是需要被多个线程同时访问)都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问
  • 对于每个包含多个变量的不变性条件,其中涉及的所有变量都要由同一把锁保护

2.5 活跃性与性能

  • 不良并发
可同时调用的数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制
  • 避免不良并发
尽量将不影响共享状态且执行时间较长的操作从同步代码中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态
  • 示例:
public class CachedFactorizer implements Servlet{
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    
    public synchronized long getHits(){return hits;}

    public synchronized double getCacheHitRatio(){
        return (double)cacheHits/(double)hits;
    }
    
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=null;
        synchronized (this){
            ++hits;
            if(i.equals(lastNumber)){
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if(factors==null){
            factors=factor(i);
            synchronized (this){
                lastNumber=i;
                lastFactors=factors.clone();
            }
        }
        encodeIntoResponse(resp);
    }
}

你可能感兴趣的:(第二章-线程安全性)