您的位置:首页 > Web前端

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

2016-06-01 14:13 381 查看

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, 1;
3. 写回内存(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处的深拷贝有点
ThreadLocal
的味道,用空间换时间。具体而言,多个线程访问 lastFactors 的时候,各自拷贝了一个备份,那么调用
encodeIntoResponse(resp, factors);
的时候就不需要同步了。

正如文中所说,耗时操作不要放在同步块里,否则很影响性能:

Avoid holding locks during lengthy computations or operations at risk of not completing quickly such as network or console I/O.

总结

在保证线程安全的情况下,同时得考虑其性能。大的同步块想一想能不能分解成几个小的。在代码复杂性和性能上寻求一个平衡点。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  并发 java 线程安全