1.1 进程与线程
进程就是运行中的程序,每个进程都有一个独立的内存空间。线程是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径可以划分成若干个线程,它们共享一个内存空间(共用堆内存,栈内存相互独立),线程之间可以自由切换,并发执行。
多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。计算机处理1000个相同的任务,从总的时间来看,排队执行比并发执行速度快,因为省去了在线程之间频繁切换的开销。
Java的线程调度为抢占式调度。优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)
同步:排队执行,效率低但是安全;
异步:同时执行,效率高但数据不安全。
并发:指两个或多个事件在同一个时间段内发生,有随机性;
并行:指两个或多个事件在同一时刻发生(同时发生)。
1.2 线程的状态
线程有以下6种状态:
- New 刚刚新建,还未启动
- Runnable 已经启动,可以抢到CPU时间片。
- Blocked 当线程试图获得一个锁,而这个锁目前被其它线程占有,则阻塞。
- Waiting 当线程获得锁,但是又发现不满足执行程序的条件。调用wait就进入等待状态,并放弃锁。
- Timed waiting 调用Thread.sleep和带计时参数的Object.wait, Thread.join, Lock.tryLock以及Condition.await
- Terminated
阻塞和等待并没有太大区别,一个线程阻塞或等待时,它不会分到时间片,因此不会运行。阻塞状态的线程,在其它所有线程都释放了它需要的锁对象时,重新进入可运行状态;等待状态的线程,直到有其它线程调用signalAll方法时,才进入可运行状态。
1.3 线程的特性
1.3.1 如何中断线程
当run方法return时,或者抛出了未捕获的异常时,线程将终止。Java早期有一个stop方法,其它线程可以调用这个方法来终止一个线程,但是这个方法已经被废弃,因为其它线程无法知道什么时候调用stop方法是安全的,而什么时候会导致正在操作的对象被破坏。因此,不能强行终止线程。
正确的做法是调用 interrupt 实例方法,它会设置线程的中断标志,每个线程都会不时地检查自己的这个标志,然后自己决定如何响应中断。while (!Thread.currentThread().isInterrupted() && more work to do) {
do more work
}
如果线程被阻塞就无法检查中断状态,这里就要引入 InterruptedException 异常。调用了某个线程对象的 interrupt 方法之后,这个线程如果执行了阻塞线程的方法 (比如 sleep 和 wait),这个方法就会抛 InterruptedException 异常并清除中断标志( !),线程从而不能进入阻塞状态。如果在循环体里调用sleep,就不需要 isInterrupted
检查,只要捕获异常并处理就行了。
1.3.2 什么是守护线程 daemon thread
守护线程的唯一用途就是为其它线程提供服务,守护线程是最后结束的线程,当只剩下守护线程时,虚拟机就会退出。
调用 t.setDaemon(true);
将线程t设为守护线程,必须在线程启动之前调用此方法。
1.3.3 未捕获的异常会怎么处理
1.3.4 线程优先级
当调度器选择新线程时,首先选择具有较高优先级的线程,优先级机制依赖于操作系统的实现。
优先级默认为NORM_PRIORITY (在Thread类中定义为5)。一个线程的优先级会继承构造它的那个线程的优先级。可以调用setPriority来设置优先级,可以设为MIN_PRIORITY (定义为1) 到MAX_PRIORITY (定义为10)之间的任何值。
1.4 同步
多个线程同时对数据进行操作,会造成数据错乱。即使是简单的加法运算,也不能保证它执行的时候不能被其它线程干扰。加法不是原子操作,它在被虚拟机执行时会分解成多个步骤。有可能一个线程刚把变量的值加载到内存中,时间片就用完了,下一个执行的线程刚好修改了前一个线程读取的变量,前一个线程下次就会在一个错误的前提下执行。为了避免这一点,必须同步存取数据。
为了说明“同步”的意思,假设有一个任务有两个运行的线程对象,该任务中调用了 transfer 方法来修改主程序里定义的变量。下图就是非同步执行的线程和同步执行的线程的对比。
1.4.1 锁
java.util.concurrent 框架为线程安全的基础机制提供了单独的类。
Java 1.5引入了 java.util.concurrent.locks.ReentrantLock
类,用 ReentrantLock 保护代码块的基本结构如下:
mylock.lock(); // 一个ReentrantLock对象
try {
// 临界区
} finally {
myLock.unlock(); // 即使线程抛异常而中断,仍能保证锁被释放。但对象可能处于被破坏的状态。
}
这个结构确保任何时刻只有一个线程进入临界区。一旦一个线程锁定了锁对象,其它任何线程都无法通过lock语句。当其它线程调用lock时,它们会阻塞,直到第一个线程释放这个锁对象。每一个对象都有自己的锁对象,锁对象可以保证不同线程访问同一对象时排队运行。当多线程访问不同的对象,会获得不同的锁对象,此时线程之间不会相互影响。
前面描述的锁称为重入锁,因为一个线程可以嵌套调用lock。也就是说,被一个锁保护起来的代码,可以调用另一个使用相同锁的方法。锁有一个持有计数来跟踪对lock方法的嵌套调用,每次调用lock,持有计数加一,调用unlock,持有计数减一。持有计数变为0的时候,线程释放锁。
1.4.2 条件
线程获得锁后发现只有满足了某个条件之后它才能执行,这时候就必须等待。可以用条件对象来管理这种线程。
- 一个锁对象可以关联多个条件对象
java.util.concurrent.locks.Condition
。调用锁对象的newCondition
方法来获得一个条件对象,习惯上要给条件对象一个合适的名字来反映它表示的条件。 - 当发现条件不满足,就调用条件对象的await方法,此时线程暂停并放弃锁,线程进入条件的等待集(wait set)。直到其它线程在同一个条件上调用 signalAll 方法。
当完成数据的修改时,就应该去调用 signalAll 方法,把等待这个条件的所有线程转换到 Runnable 状态,其中一个线程将会获得锁,然后从await调用返回,从之前暂停的地方继续执行。此时,线程应该再次测试条件。
class Bank {
private ReentrantLock bankLock = new ReentratntLock();
private Condition sufficientFunds = bankLock.newCondition();
. . .
public void transfer(int from, int to, int amount) {
bamkLock.lock();
try {
// 如果账户资金小于转账金额,线程进入waiting状态
while(accounts[from] < amount) {
sufficientFunds.await();
}
// 下面进行转账
. . .
// 账户资金有变化,唤醒其他线程检查条件
sufficientFunds.signalAll();
}
finally {
bankLock.unlock();
}
}
. . .
}
如果所有线程都进入了waiting状态,这就是死锁现象,程序会永远挂起。为了避免死锁,只要一个对象的状态有变化,而且可能有利于满足程序中要测试的条件,就要调用 signalAll。
1.4.3 synchronized 关键字
先对 Lock 和 Condition 对象的机制做一个总结:
- 锁用来保护代码片段,同一时刻只能有一个线程执行被保护的代码;
- 锁可以管理试图进入被保护代码段的线程;
- 一个锁可以关联一个或多个条件对象;
- 每个条件对象管理那些已进入被保护代码段但还不能运行的线程。
Lock和Condition接口允许程序员充分控制锁定。不过大多数情况不需要这样控制,可以使用更简单的方式。从Java1.0 开始每个对象都有一个内部锁,可以不进行显式的声明。如果声明一个方法时加上 synchronized
关键字,那么对象的内部锁会保护整个方法。也就是说下面两个写法是等价的:
public synchronized void method() {
method body
}
****************************************
public void method() {
this.intrinsicLock.lock();
try {
method body
}
finally {
this.intrinsicLock.ublock();
}
}
对象的内部锁只有一个关联条件,wait方法将一个线程添加到等待集中,notifyAll 方法可以让线程回到Runnable状态。wait 和 notify 等价于intrinsicCondition.await();
intrinsicCondition.signalAll();
wait
notifyAll
和notify
是Object类的final方法。Condition 类的方法必须命名为await
signalAll
和signal
来避免冲突。
静态方法也可以在声明中加 synchronized
关键字。线程调用静态同步方法,将获得相关Class对象的内部锁,例如:Bank.class 的内部锁。
synchronized 关键字减少了代码量,减少了出错几率,但仍不是最好的选择。实际上,多数时候可以使用 java.util.concurrent 包中的某种数据结构,它会自动处理所有的锁定。如果特别需要对锁机制的完全控制能力,则使用 Lock/Condition 结构。