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对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序
- 二叉查找树