第一章:介绍
知识点:
- 进程是资源(CPU、内存等)分配的基本单位
- 线程是CPU调度和分派的基本单位
- 一个进程包括一个或多个线程
1、为什么应用程序需要从单线程发展为多线程(从同步到异步)?
- 资源利用。程序有时候需要等待外部的操作,比如输入和输出,并且在等待的时候不可能进行有价值的工作。在等待的时候,让其他的程序运行会提高效率。
- 公平。多个用户或程序可能对系统资源有平等的优先级别。让他们通过更好的时间片方式来共享计算机,这要比结束一个程序后才开始下一个程序更可取。
- 方便。写一些程序,让它们各自执行一个单独任务并进行必要的相互协调,这要比编写一个程序来执行所有的任务更容易,更让人满意。
第二章:线程安全
一个对象的状态就是它的数据。编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。
所谓共享,是指一个变量可以被多个线程访问;所谓可变,是指变量的值在其生命周期内可以改变。我们讨论的线程安全性好像是关于代码的,但是我们真正要做的,是在不可控制的并发访问中保护数据。
在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它:
- 不要跨线程共享变量;
- 使状态变量为不可变的;或者
- 在任何访问状态变量的时候使用同步。
线程安全的定义:
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
无状态对象永远是线程安全的。
线程不安全之复合操作
读-改-写(read-modify-write)
@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);
}
}
检查再运行(check-act)
@NotThreadsafe
public class LazyInitRace {
private Expensive0bject instance = null;
public ExpensiveObject getInstance() {
if ( instance == null ) {
instance = new Expensive0bject();
}
return instance;
}
}
为了确保线程安全,“检查再运行” 操作(如惰性初始化)和读-改-写操作(如自增)必须是原子操作。我们将“检查再运行”和“读-改-写”的全部执行过程看作是复合操作:为了保证线程安全,操作必须原子地执行。
我们会在下一节考虑用Java内置的原子性机制——锁。现在,我们先用其他方法修复这个问题——使用已有的线程安全类。
@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);
}
}
java.util.concurrent.atomic
包中包括了原子变量(atomic variable) 类,这些类用来实现数字和对象引用的原子状态转换。把long
类型的计数器替换为AtomicLong
类型的,我们可以确保所有访问计数器状态的操作都是原子的。计数器是线程安全的了。
内部锁
Java提供了强制原子性的内置锁机制:synchronized
。一个synchronized
块有两部分:锁对象的引用,以及这个锁保护的代码块。
每个Java对象都可以隐式地扮演一个用于同步的锁的角色;这些内置的锁被称作内部锁(intrinsic locks)或监视器锁(monitor locks)。
执行线程进入synchronized
块之前会自动获得锁;而无论通过正常控制路径退出,还是从块中抛出异常,线程都在放弃对synchronized
块的控制时自动释放锁。
获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。
内部锁在Java中扮演了互斥锁(mutual exclusion lock,也称作mutex)的角色,意味着至多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它。如果B永远不释放锁,A将永远等下去。
重进入(Reentrancy)
当一个线程请求其他线程已经占有的锁时,请求线程将被阻塞。然而内部锁是可重进入的,因此线程在试图获得它自己占有的锁时,请求会成功。
重进入意味着所的请求是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation) ”的。
重进入的实现是通过为每个锁关联一个请求计数(acquisition count)和一个占有它的线程。当计数为0时,认为锁是未被占有的。
线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器达到0时,锁被释放。
/**
* 如果该锁不是可重入的,代码将死锁
*/
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
@Override
public synchronized void doSomething() {
super.doSomething();
}
}
对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。
每个共享的可变变量都需要由唯一一个确定的锁保护。
适当调整
synchronized
块的大小,使其在安全和性能之间达到平衡。
有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作期间不要占有锁。