第一章 简介
1. 计算机加入操作系统实现多个程序的同时执行,是为了:
- 资源利用率
- 公平性:不同用户和程序对计算机上资源的同等使用权,比如时间分片的运行方式
- 便利性:从编程角度看,可以把任务更好的分解开,而不用一个程序来完成所有的任务
线程被称为轻量级进程,现代操作系统中基本的调度单位是线程(而不是进程),线程允许在同一个进程中同时存在多个程序控制流,线程会共享同一个进程范围内的资源,比如:内存句柄,文件句柄。
2. 线程的优势
- 发挥多处理器的强大能力
- 建模的简单性
- Servlet/RMI框架辅助让建模简单,框架负责解决细节问题:请求管理、线程创建、负载平衡,并在正确的时刻把请求分发给正确的应用程序组件,这样编写Servlet的开发人员就不需要了解有多少 请求在同一时刻需要被处理,也不需要了解套接字的输入流/输出流是否阻塞。当调用Servlet的Service方法来响应Web请求时,可以以同步的方式处理这个请求,就好像它是一个单线程程序
- 异步事件的简化处理:作者的意思是如果线程开销足够低(系统能够支持创建更多的线程),那么很多异步的操作可以用多线程方式来代替(每个线程互不影响),进而避开处理异步复杂性
- 更灵敏的响应用户界面
3. 线程的风险
-
安全性问题:永远不发生糟糕的(错误的)事情。比如发生竞态条件(TODO:添加链接进行详细说明)时结果不可预期或者错误
非线程安全的序列生成器
@NotThreadSafe public class UnsafeSequence { private int value; public int getNext() { return value++; } }
A. (value->9) —> (9+1=10) —> (value=10)
B. —> (value->9) —> (9+1=10) —> (value=10)
解决办法可以是:
@ThreadSafe public class Sequence { @GuardedBy("this") private int value; public synchronized int getNext() { return value++; } }
- 活跃性问题:死锁,饥饿,活锁等
- 性能问题:上下文切换等开销
4. 线程无处不在
- 每一个Java应用程序都在使用线程(垃圾回收线程,终结处理线程,运行main方法的主线程等)
- Servlet/JSP:Servlet规范规定一个Servlet必须为多个用户同时调用它做好准备(也即Servlet应该是线程安全的)
- RMI:rmi对象和Servlet一样需要是线程安全的
第二章 线程安全性
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享(Shared) 和可变的(Mutable) 状态的访问。
- 共享:变量可以由多个线程同时访问
- 可变:变量在其生命周期内可以发生变化
如果多个线程访问同一个可变的状态变量时没有使用合适的同步,那么错误就会发生,有三种方式进行修复:
- 不在线程之间共享该状态变量
- 将状态变量修改为不可变的变量
- 在访问状态变量时使用同步
1. 线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同, 这个类始终都能表现出正确的行为,那么称这个类是线程安全的。 在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施。
无状态的对象,一定是线程安全的。
示例:一个无状态的Servlet
@ThreadSafe
public class StatelessFactorizer extends GenericServlet implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
2. 原子性
一个或者一组(复合)操作,能够逻辑上呈现出最小执行单元执行的效果,则具备原子性。(和数据库事务里的一个事务具备原子性类似)
- 竞态条件(Race Condition):TODO 由于不恰当的执行时序而出现不正确的结果。有2类典型的竞态条件(复合操作)
- 先检查后执行(Check-Then-Act):如
单例实现中先判空后new对象
;也比如:if (!vector.contains(element)) { vector.add(element); }
- 读取修改写入(Read-Modify-Write):如
count++
示例:(不要这么做)在没有同步的情况下统计已处理请求数量的Servlet@NotThreadSafe public class UnsafeCountingFactorizer extends GenericServlet 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); } }
一种改进方式是
使用AtomicLong类型的变量来统计已处理的请求的梳理
,代码 这种改进方式试用于:当在无状态的类中添加一个状态时,如果该状态由线程安全的对象来管理,那么这个类仍然是线程安全的。 但是,当状态数量由1个变为多个时,情况复杂许多,可能需要用到加锁机制。
- 先检查后执行(Check-Then-Act):如
3. 加锁机制
要保持状态的一致性,需要在单个原子操作中更新所有相关的状态变量。 如下是一个示例:(不要这么做)该Servlet在没有足够原子性保证的情况下对其最近计算结果进行缓存:
@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet 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提供了内置的锁机制来支持原子性。
- 内置锁/监视器锁
每个Java对象都可以用作一个实现同步的锁。内置锁体现在同步代码块(Synchronized Block)上,同步代码库包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。静态的synchronized方法以Class对象作为锁。
获得内置锁的唯一途径是进入由这个锁所保护的同步代码块或方法。由于每次只能有一个线程执行内置锁保护的代码块,因此这个锁保护的同步代码块会以原子方式执行。
特点:
- 互斥性:对于某个内置锁,一旦有一个线程获得了该锁且未释放锁,别的线程就无法获得这个锁
- 可重入:如果某个线程试图获得一个已经由它持有的锁,那么这个请求会成功
获取锁的粒度,是线程,而非调用。持有锁的不是方法,而是线程,完整的表述是:线程持有某个对象/类的锁
当子类改写父类的synchronized方法时,
当执行时间较长的计算或者可能无法快速完成的操作时(比如,网络 I/O 或者控制台 I/O),一定不要持有锁。
http://jcip.net/listings.html