【学习笔记】【Java并发编程实战】第二章 线程安全性

三种方式修复“多个线程访问同一个可变的状态变量时没有使用合适的同步”:

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

什么是线程安全性

  • 线程安全类: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为。
  • 在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。
  • 无状态(既不包含任何域,又不包含任何对其他类中域的引用)对象一定是线程安全的。

原子性

  • 原子性:一组语句作为一个不可分割的单元被执行。
  • 竞态条件(race condition):某个计算的正确性取决于多个线程的交替执行时序。最常见的竞态条件类型就是“先检查后执行”操作,通过一个可能失效的观测结果来决定下一步的动作。
  • 要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其它线程使用这个变量。
  • 复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性,如“先检查后执行”或“读取-修改-写入”。
  • 应尽可能地使用现有的线程安全对象来管理类的状态。

加锁机制

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); // 此时其它线程可能会读取lastNumber和lastFactors,出现lastNumber和lastFactors不对等的情况。
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}
  • 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
  • 以synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,以方法调用所在的对象为锁,线程进入同步代码块后会自动获得锁,并在退出(正常退出或抛出异常)时自动释放锁。
  • 内置锁(监视器锁):每个Java对象都可以用做一个实现同步的锁。内置锁相当于一种互斥体,最多只有一个线程能持有这种锁。
  • 重入:为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者并且将获取计数值置为1。如果同一线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数器将递减。当计数值为0是,这个锁将被释放。
public class Widget {
	public synchronized void doSomething() {
	}
}

public class LoggingWidget extends Widget {
	public synchronized void doSomething() { // 获取LoggingWidget类的某对象的锁。
	System.out.println(toString() + ":calling doSomething");
	super.doSomething(); // synchronized方法,需要再次获取LoggingWidget类的该对象的锁,如果内置锁不是可重入的那么会一直等待,发生死锁。
	}
}

用锁来保护状态

  • 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。
  • 每个共享和可变的变量都应该只由一个锁来保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
  • 在不变性条件中的每个变量都必须由同一个锁来保护。
  • 不加区别的滥用synchronized不足以保证复合操作是原子的,还可能导致活跃性问题或性能问题。

活跃性与性能

public class CachedFactorizer implements Servlet {
    @GuardeBy("this") private BigInteger lastNumber;
    @GuardeBy("this") private BigInteger[] lastFactors;
    @GuardeBy("this") private long hits;
    @GuardeBy("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(resp, factors);
    }
}
  • 通过缩小同步代码块的作用范围,可以确保并发性并维护线程安全线。将不影响共享状态且执行时间较长的操作从同步代码块中分离出去。
  • 获取与释放锁等操作都需要一定的开销,但是不要盲目地为了性能而牺牲简单性,这可能会破坏安全性。
  • 当执行时间较长的计算或者可能无法快速完成的操作时,一定不要持有锁。

你可能感兴趣的:(Java)