java 并发实践 - Chapter 2(Thread Safety) 笔记

Thread Safety


要做到线程安全,核心是控制对状态(state)的访问。
(对象的)状态:通常是指它那些共享的(shared)、可变的(not final)的成员变量。

我们知道,线程之间是共享内存的(成员变量都分配在内存中)。所以它们有能力同时访问同一个 state ,这将破坏线程安全。我们需要某种机制进行访问的同步。
相比之下,由于线程之间各自持有堆栈,这些堆栈不是共享的。因此,当不同的线程访问同一个函数的局部变量时(局部变量都分配在堆栈中),是线程安全的。

2.1 What is thread safety?

来看书中的一个例子:

// 一个大整数提取因子,并返回
// 只访问了局部变量,所以是线程安全的
@ThreadSafe
public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}

这是一个无状态(stateless)的例子:
不同的线程访问 service(req, resp) 时,在各自堆栈中生成一个 i 和 factors。正如之前所说,这些堆栈是相互隔离的(线程A的修改不会影响到线程B),是线程安全的。
也就是说,无状态的对象总是线程安全的

2.2 Atomicity(原子性)

原子性的必要性 (read-modify-write)

再来看一个相近的例子:

// 一个大整数提取因子,并返回
// 访问了成员变量 count,存在隐患
@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);
    }
}

在该例子中,仅仅是对成员变量 count进行修改,便不再是线程安全的。
原因在于,count++并非原子操作
事实上,count ++ 可以分解为三条指令:

1. 读取到寄存器(cache): MOV cache, count;
2. 在寄存器中计算:       ADD cache, 13. 写回内存(count):    MOV count,cache

这3步应该保持原子性,不可被打断。一旦被线程切换打断,将会得到不可预测的错误答案。
这种读取-修改-写回的操作经常出现,书中称之为 read-modify-write

原子性的保持

我们已经知道保持原子性的必要性,但是我们应该怎么做呢?
好在 java.util.concurrent.* 中,为我们提供了解决方案。
从名称上看,这是一个关于并发的包,里面封装了一些线程安全的类。
让我们用线程安全的 AtomicLong 来替换原来的 long:

// 一个大整数提取因子,并返回
// 访问了线程安全的成员变量 count,保持了原子性
@ThreadSafe
public class CountingFactorizer implements Servlet {
    //private long count = 0;
    private final AtomicLong count = new AtomicLong(0);

    //public long getCount() { return count; }
    public long getCount() { return count.get(); }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        //++count; 
        count.incrementAndGet(); // 增加的计数变量
        encodeIntoResponse(resp, factors);
    }
}

感想

好奇地点开了 AtomicLong ,看了一下实现,有两点值得注意的:
1.它使用了 volatile 关键字:(对它比较陌生,有空可以了解一下)

    private volatile long value;

2.从头至尾没有发现 synchronized 关键字。

那我就疑惑了,它是怎么保持同步的呢?
后来搜到了这篇文章:http://www.cnblogs.com/Mainz/p/3556430.html
里面提到了“CAS(Compare and Swap)无锁算法”,听上去好像不用锁的,先mark一下。
AtomicLong 确实调用了一个 compareAndSwapLong(),不过是 native 方法,暂时看不到源码。

2.3 Locking(上锁)

注意到BigInteger[] factors = factor(i);这行,其中的factor()可能是一个比较耗时的操作。很自然的,我们想到用缓存来优化,保存上一次的结果lastNumber&&lastFactors。于是,我们写出了如下代码:

@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())) // 1. 从缓存取 lastNumber
            encodeIntoResponse(resp,  lastFactors.get() ); // 2. 从缓存取 lastFactors

        else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);          // 3. 缓存 lastNumber
            lastFactors.set(factors);   // 4. 缓存 lastFactors
            encodeIntoResponse(resp, factors);
        }
    }
}

To preserve state consistency, update related state variables in a single atomic operation.

很遗憾,尽管lastNumber&&lastFactors都是原子类,但是组合在一起却保证不了原子性。注意到它们须满足不变式(invariant)lastFactors = factor(lastNumber),这就要求对这两个变量的读和写必须各自原子的。也就是说,步骤 1、2 是原子的(一起读),不允许切换线程。步骤 3、4 同理,否则就会破坏不变式。 那么如何解决呢,请听下文分解。

2.3.1. Intrinsic Locks (固有锁)

java 在语言层面上提供了锁机制:synchronized关键字。该关键字可以修饰一个代码块(the synchronized block),以保证该 block 的原子性:

synchronized (lock) { // 这里的 lock 可以是任意的 Object
    // synchronized block
    // Access or modify shared state guarded by lock
}

顺便一提,synchronized可重入锁(Reentrant Lock),这点后面会详细解释。
一个小的变种是,synchronized可以修饰函数,此时函数体即作为代码块。值得注意的是,此时被上锁的对象默认为函数调用者,即我们常见的synchronized (this){}中的this;静态函数则对应class


答案已经浮出水面了,我们加上 synchronized ,即可解决之前的同步问题:

@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    // use synchronized
    public synchronized void service(ServletRequest req,
                                     ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber))
            encodeIntoResponse(resp, lastFactors);
        else {
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            encodeIntoResponse(resp, factors);
        } 
    }
}


2.5. Liveness and Performance(性能)

回顾上边的例子,尽管线程安全了,但是性能却变得很差。无脑使用同步块,导致不同的 client 无法同时访问这个service()。每个线程排队等候,完全不是并发了!!!
注意到不是所有的步骤都需要同步,我们做了如下调整:

@ThreadSafe
public class CachedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) {
            if (i.equals(lastNumber)) 
                factors = lastFactors.clone();  // 1. 深拷贝, 防止调用 encodeIntoResponse 时, 数据已变
        }
        if (factors == null) {
            factors = factor(i);    // 2. factor 是耗时操作, 不要包在 synchronized 里
            synchronized (this)  {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }
}


注意点

  1. 1处的深拷贝有点ThreadLocal的味道,用空间换时间。具体而言,多个线程访问 lastFactors 的时候,各自拷贝了一个备份,那么调用encodeIntoResponse(resp, factors);的时候就不需要同步了。
  2. 正如文中所说,耗时操作不要放在同步块里,否则很影响性能:
    Avoid holding locks during lengthy computations or operations at risk of not completing quickly such as network or console I/O.


总结

在保证线程安全的情况下,同时得考虑其性能。大的同步块想一想能不能分解成几个小的。在代码复杂性和性能上寻求一个平衡点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值