Java Concurrency in Practice 读书笔记 第二章

第二章  线程安全

2.1  什么是线程安全

定义1:无状态的对象一定是现成安全的。

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

如上所示,无状态是指:状态的瞬间转移全部蕴涵于Local变量中(只存在于本线程的堆栈中),且只能由当前线程来获取。
因此,如果只是Req<----->Resp方式的话,Servlet一般都是线程安全的。但是如果要恢复/记录状态的话,就有可能导致线程安全问题。

2.2  原子性

定义2:对有状态类,如含类变量(Field变量)而言,如果不加控制的话,一般不是线程安全的。

例如下面代码不是线程安全的:

@NotThreadSafe
public class UnsafeCountingFactorizer 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);
    }

}

上述代码的问题并不在于使用了Field变量,而是使用了Filed变量却不加同步控制。因为++count不是原子操作,这会导致在某些杯具时间条件下产生的竞争条件

这一类竞争条件叫做check-then-act:例如检查某个变量的数值,然后准备对其进行修改,然而在你检查好之后、修改之前,这个数值却发生了变化,这就可能导致很多问题。

再来看一种lazy-load导致的竞争条件,这是基本所有人都会写错的单件类,因为它不是线程安全的:

@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

如上所示,如果线程A在new对象的过程中,尚未赋值给instance之前,B来检查instance。B发现instance是null,也初始化了对象。然后单件类的功能就废了。

对于读-写-读的竞争条件,只要将读取i、自增i都做成原子操作就可以解决线程安全隐患了。

Java提供了AtomLong,即原子封装的Long来解决上述问题,如下述代码所示:

@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(resp, factors);
    }
}

使用原子操作对象,是一种很好的线程安全解决办法。但是对包含两个及以上导致状态转移变量情况来说并不成立。

2.3  锁

如果同时有两个变量影响状态的变化,则不一定总是正确的,例如对于一个Cache因式分解的例子:

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

     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);
         }
     }
}

尽管lastNumber和lastFactors分别都是原子操作,但无法保证在一个原子操作内同时更新这两个数值。因此仍然不是线程安全的。

Java提供了synchronized关键字来完成同步工作。

synchronized (lock) {
    // Access or modify shared state guarded by lock
}

在synchronized中包含的代码在同一时间只能被一个线程执行。

这种锁机制是可重入的:如果某线程尝试获取已经获得的锁,则它将直接获得,而非等待释放。

如果没有重入,下面这种代码将死锁:

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}

如果同一线程中调用了子类的someThing,就会死锁。

2.4  用锁守护状态

如果使用synchronized同步变量,则必须在使用该变量的每一处添加synchronized。常见的错误认为:只需要在写的时候同步即可。

另一条规则:如果某一状态改变由多个变量共同作用,则为了同步它们,必须使用同一个锁。即将整体的状态改变放在同一个原子操作内。

此时,函数方式的synchronized能更好地解决这个问题:对于函数式synchronized来说,它使用的锁就是这个Object,因此所有同一对象内的synchronized函数使用同一个锁

然而,并非把所有函数都声明为synchronized就可以解决线程同步问题。

2.5  活跃度和性能

有的时候,往往要在性能和安全性之间选择做出平衡,如果对service全部synchronized,并发性会非常差尽管它是线程安全的,主要通过缩小synchronized的方法在保证线程安全的基础上提升并发性能,如下:

@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(resp, factors);
    }
}

在上面的代码中,lastNumber、lastFactors、hints、cacheHits共同导致状态的变化,因此使用同一个锁:this(该对象)将他们保护起来。这样例如在修改lastNumber的时候,其他线程也不可能接触到其余变量。

保证线程安全的覆盖标准是:对于会导致状态变化的变量,在读、写他们的地方都要加上保护。

Leave a Reply

Your email address will not be published. Required fields are marked *