02-线程的安全性

一、概述

  1.1 要编写线程安全的代码,核心在于要对状态访问操作进行管理,尤其是对共享可变状态的访问。

    共享:变量可由多个线程访问

    可变:变量的值在其生命周期内可以发生变化

  1.2 当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。

    Java中的主要同步机制是关键字synchronizedvolatile显示锁以及原子变量

  1.3 如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误

    修复方式:

    1. 不在线程之间共享该状态变量
    2. 将状态变量修改为不可变的变量
    3. 在访问状态变量时使用同步

  1.4 一开始就设计一个线程安全的类,后续更改该类为线程安全类更加容易

       首先使代码正确运行再提高代码速度。

  1.5 线程安全类和线程安全程序的区别

    1. 线程安全程序并非完全由线程安全类构成
    2. 完全由线程安全类构成的程序不一定就是线程安全的
    3. 在线程安全类中也能包含非安全类

  

二、什么是线程安全性

  2.1 线程安全性的定义中核心概念是正确性

    正确性:某个类的行为与其规范完全一致。

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

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

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

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

    一个无状态servlet

@ThreadSafe
public class StatelessFactorizer implements Servlet {

    @Override
    public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}

  

三、原子性

  3.1 如果在无状态的对象中增加一个状态时,就会导致线程不安全的问题发生

  3.2 在无状态servlet中增加一个命中计数器

    count++包括:读取count - 修改即count+1 - 写入count,其结果状态依赖于之前的状态

  3.3 在并发编程中,这种由于不恰当的执行时序而出现不正确的结果叫竞态条件

public class UnsafeCountingFactorizer implements Servlet {
    private long count = 0;
    public long getCount(){
        return count;
    }
    @Override
    public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count = getCount();
        ++count;
        encodeIntoResponse(resp, factors);
    }
}

  3.4 竞态条件

    3.4.1 当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞争条件。即当正确的结果取决于运气时,就属于竞态条件。

    3.4.2 最常见的类型就是“先检查后执行”:

    当你迈出前门时,你在星巴克A的观察结果变得无效,你的朋友可能从后门进来了而你不知道。这种观察结果的失效就是大多数竞态条件的本质--基于一种可能失效的观察结果来做出判断或执行某个计算。这种类型竞态条件称为“先检查后执行”。

3.4.3 延迟初始化中的竞态条件

  延迟初始化:将对象的初始化操作推迟到实际使用时才进行,同时要确保只被初始化一次

  先检查后执行常见情况就是延迟初始化,如下代码,getInstance()判断是否已创建instance实例,没创建则创建一个实例。但多线程同时操作时可能创建多个不同实例..

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

    3.4.4 复合操作

  假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作来说,这个操作是一个原子操作
      使用AtominLong来保证原子操作
    
@ThreadSafe
public class CountingFactorizer implements Servlet {
    private final AtomicInteger count = new AtomicInteger(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(resp, factors);
    }
}

四、加锁机制

  4.1 假设我们希望提升Servlet的性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无须重新计算。

@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber.get()))
            encodeIntoResponse(resp, lastFactors.get());
        else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}

  以上这种方式并不正确。

  UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的数值。

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

  4.2 内置锁

    Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)

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

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

    Java的内置锁相当于一种互斥体(或互斥锁),最多只有一个线程能持有这种锁。
  由于每次只能有一个线程执行内置锁保护的代码块,所以可以用这种机制确保因数分解Servlet的线程安全性。然而,这种方法过于极端,服务的响应性非常低。

4.3 重入

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

  4.3.2 重入意味着获取锁的操作的粒度是“线程”,而不是“调用”。

  具体实现:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
  重入进一步提升了加锁行为的封装性,简化了面向对象并发代码的开发。如下代码,子类重写父类方法,若内置锁不是可重入的则子类调用父类方法将产生死锁。
public class Widget {
      public synchronized void doSomething(){
       ....
      }          
}
public class LoggingWidget extends Widget{
      public synchronized void doSomething(){
         ...
         super.doSomething();
      }  
}

 

  
 
 
 
 
 
 

五、活跃性与性能

  程序清单2-6中给出的代码,每次只有一个线程可以执行。背离了Servlet框架的初衷,需要同时处理多个请求。如果Servlet在对某个大数值进行因数分解时需要很长的执行时间,那么其他客户端就一直等待,知道Servlet处理完当前的请求,才能开始另一个新的因数分解运算。

应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

@ThreadSafe
public class CachedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") 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(req, resp);
    }
}

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

 

 
posted @ 2021-10-19 23:14  微~粒  阅读(50)  评论(0)    收藏  举报