进程试运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程。操作系统会将进程互相隔离开,因为他们不会彼此干涉。编写多线程程序最基本的困难在于在协调不同线程驱动的任务之间对共享资源的使用,以使得这些资源不会同时被多个任务访问。
1.线程机制
一个线程是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的线程。每个任务都觉得自己在一直占用CPU,但事实上CPU时间是划分成片段分配了所有线程。
线程驱动任务。
Runnable接口
实现Runnable接口并编写run()方法。
1
2
3
4
5
6
|
public
class
LiftOff
implements
Runnable {
@Override
public
void
run() {
// TODO Auto-generated method stub
}
}
|
Thread类
将Runnable对象转换为工作任务的方式是把它提交给一个Thread构造器。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
class
BasicThreads {
public
static
void
main(String[] args) {
Thread t =
new
Thread(
new
LiftOff());
t.start();
}
}
public
class
MoreBasicThreads {
public
static
void
main(String[] args) {
for
(
int
i =
0
; i <
5
; i++) {
new
Thread(
new
LiftOff()).start();
}
}
}
|
调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,在这个线程中启动任务。
Executor
java.util.concurrent包中的执行器(Executor)将管理Thread对象,从而简化并发编程。Executor在客户端和任务执行之间提供了一个间接层,Executor允许管理异步任务的执行,而不需显式的管理线程的生命周期。
Executor在Java SE5/6中是启动任务的优选方法。
1
2
3
4
5
6
7
8
9
|
public
class
CachedThreadPool {
public
static
void
main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for
(
int
i =
0
; i <
5
; i++) {
exec.execute(
new
LiftOff());
}
exec.shutdown();
}
}
|
ExecutorService通过构建上下文来执行Runnable对象,CachedThreadPool为每个任务都创建一个线程。注意:ExecutorService对象是使用静态方法Executor方法创建的。
对shutdown()方法的调用可以防止新任务被提交给这个Executor,当前任务将继续运行在shutdown()被调用之前提交的任务。这个程序将在Executor中所有任务完成之后退出。
1
2
3
4
5
6
7
8
9
|
public
class
FixedThreadPool {
public
static
void
main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(
5
);
for
(
int
i =
0
; i <
5
; i++) {
exec.execute(
new
LiftOff());
}
exec.shutdown();
}
}
|
FixedThreadPool可以一次性预先执行代价高昂的线程分配,因此可以限制线程的数量。FixedThreadPool使用的Thread对象的数量是有界的。
CachedThreadPool在程序执行时会创建与所需数量相同的线程,在回收旧线程时停止创建新线程,因此它是Executor的首选。
SingleThreadExecutor是线程数量为1的FixedThreadPool。如果向SingleThreadExecutor提交多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有任务使用相同的线程。
任务返回值
Runnable是执行工作的独立任务,他不返回任何值。如果希望在任务完成时返回一个值,那通过实现Callable接口而不是Runnable接口。
在Java SE5中Callable是一个具有类型参数的泛型,它的类型参数表示的是从方法call(),而不是run(),并且必须使用ExecutorService.submit()方法调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public
class
TaskWithResult
implements
Callable<String> {
private
int
id;
public
TaskWithResult(
int
id) {
this
.id = id;
}
@Override
public
String call()
throws
Exception {
return
"result of TaskWithResult "
+ id;
}
}
public
class
CallableDemo {
public
static
void
main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> results =
new
ArrayList<Future<String>>();
for
(
int
i =
0
; i <
10
; i++) {
results.add(exec.submit(
new
TaskWithResult(i)));
}
for
(Future<String> fs : results)
try
{
System.out.println(fs.get());
}
catch
(Exception e) {
e.printStackTrace();
}
finally
{
exec.shutdown();
}
}
}
|
submit()方法会产生Future对象,他用Callable返回结果的特定类型进行了参数化。当任务完成时,可以调用get()方法获取该结果。
休眠
影响人物行为的一种简单方法是调用sleep(),这将给任务中止执行给定的时间。
1
2
3
4
5
6
7
8
9
|
public
class
SleepingTask
extends
LiftOff {
public
void
run() {
try
{
TimeUnit.MILLISECONDS.sleep(
100
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
|
优先级
线程的优先级将该线程的重要性传递给调度器,调度器将倾向让优先权最高的线程先执行。优先级的高低仅仅是执行频率的高低。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
class
SimplePriorities
implements
Runnable {
private
int
priority;
public
SimplePriorities(
int
priority) {
this
.priority = priority;
}
@Override
public
void
run() {
Thread.currentThread().setPriority(priority);
}
public
static
void
main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(
new
SimplePriorities(Thread.MIN_PRIORITY));
exec.execute(
new
SimplePriorities(Thread.NORM_PRIORITY));
exec.execute(
new
SimplePriorities(Thread.MAX_PRIORITY));
}
}
|
让步
当调用yield()是,是在暗示调度器可以运行其他具有相同优先级的线程。
后台线程
指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止,同时会杀死线程中所有后台线程。反过来说,只要有任何非后台线程在运行,程序就不会终止。
Thread继承
可以直接从Thread继承来替换Runnable实现的方式。
1
2
3
4
|
public
class
SimpleThread
extends
Thread {
public
void
run() {
}
}
|
加入一个线程
一个线程可以在其他线程上调用join()方法,其效果是等待一段时间直到第二个线程结束才能继续执行。如果某个线程在另一个线程t上调用t.join(),次线程将被挂起,直到目标线程结束,才能恢复。
捕获异常
由于线程的本质特性,不能捕获从线程中逃逸的一场。一旦异常逃出任务的run()方法,它就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误的异常。
Thread.UncanghtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上附着一个异常处理器。Thread.UncanghtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。为了使用它,需创建一个新类型的ThreadFacotry,它将在每个新创建的Thread对象上附着一个Thread.UncanghtExceptionHandler。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MyUncaughtExceptionHandler
implements
Thread.UncaughtExceptionHandler {
@Override
public
void
uncaughtException(Thread t, Throwable e) {
}
}
public
class
HandlerThreadFacotry
implements
ThreadFactory {
@Override
public
Thread newThread(Runnable r) {
Thread t =
new
Thread(r);
t.setUncaughtExceptionHandler(
new
MyUncaughtExceptionHandler());
System.out.println(t.getUncaughtExceptionHandler());
return
t;
}
}
|
2.共享资源
想象一下,你坐在桌边手拿叉子,正要去叉盘子中的最后一片肉,当你的叉子就要够着它时,这片肉消失了,因为你的线程被挂起,而另一个餐者进入并吃掉了它。
防止上述冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问其资源的任务必须锁定这项资源,是其他任务在其被解锁之前,无法访问它,而在其被解锁之时,另一个任务就可以锁定并使用它。
基本上所有并发模式在解决线程冲突问题的时候,都采用序列化访问共享资源的方案。这意味着在给定时间只允许一个任务访问共享资源。
Java提供关键字synchronized的形式,为防止资源冲突提供内置支持。
当对象上调用其任意synchronized方法的时候,此对象都被加锁,这是该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。
synchronized void f() { }
synchronized void g() { }
如果某个任务对对象调用f()方法,对于同一个对象而言,就要等到f()方法调用结束并释放了锁之后,其他任务才能调用f()和g()方法。所以,对于某个特定的对象,其所有synchronized方法共享同一个锁。
Lock对象
java.util.concurrent类库提供java.util.concurrent.locks中的显式的互斥机制。
Lock对象必须被显式地创建、锁定和释放。
1
2
3
4
5
6
7
8
9
10
11
|
public
class
MuteEvenGenerator {
private
Lock lock =
new
ReentrantLock();
public
int
next() {
lock.lock();
try
{
return
0
;
}
finally
{
lock.unlock();
}
}
}
|
当使用synchronized关键字时,需要写的代码量更少,用户错误出现的可能性也低,因此只有在解决特殊问题时,才使用显式Lock对象。
显式的Lock对象在家所和释放所得方面,相对于内建的synchronized锁赋予了更细粒度的控制力。
原子性和易变性
不正确的认识:原子操作不需要进行同步控制。
原子性可以应用于除long和double之外的所有基本类型的简单操作。对于读写除long和double之外的基本类型变量的操作,可保证它们会被当作不可分(原子)来操作。但JVM将64位(long和double)的读写当作两个分离的32位操作,这造成在读写中发生上下文切换。如果使用volatile关键字,就会获得原子性。
原子操作可由线程机制来保证其不可中断。
volatile关键字还保证了应用中的可见性,如果一个域声明为volatile,只要对这个域产生写操作,那么所有读操作都可以看到这个修改。
同步
3.终结任务
一个线程可以处于四种状态:新建(New),就绪(Runnable),阻塞(Blocked),死亡(Dead)
阻塞
一个任务进入阻塞状态,可能是:
一,通过调用sleep(milliseconds)是任务进入休眠状态;
二,调用wait()是线程挂起,直到线程得到notify()或notifyAll()消息,线程才会进入就绪状态;
三,任务在等待某个输入/输出完成;
四,任务试图在某个对象上调用其同步控制方法,但是对象锁不可用。
中断
Thread类包含interrupt()方法,因此可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置个线程的中断状态将抛出InterruptedException。当抛出该异常或调用Thread.interrupted()时,中断状态将被复位。
为了调用interrupt(),必须持有Thread对象,新的concurrent类库在避免对Thread对象的直接操作,尽量通过Executor来执行所有操作。
如果在Executor上调用shutdownNow(),那么他将发送一个interrupt()调用给它启动的所有线程。如果通过调用submit()而不是executor()启动任务,则可以持有改任务的上下文,submit()将返回一个泛型Future<?>,在其上调用cancel(),可以中断某个特定任务。
4.线程协作
任务协作,关键问题是这些任务之间的握手—— 互斥,互斥能确保只有一个可以任务可以响应某个信号。在互斥上,通过为任务添加一种途径,将其自身挂起,知道至某些外部条件发生变化,表示可以让这个任务向前开始运行。
握手通过Object方法的wait()和notify()来安全地实现。
wait()与notifyAll()
wait()方法使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。
不断地进行空循环称为忙等待,这是一个不良的CPU周期使用方式,因此wait()在等待外部产生变化的时候将任务挂起,并且只有在notify()和notifyAll()发生时,这个任务才会被唤醒并去检查所产生的变化。
wait()是一种在任务之间对活动同步的方式。
wait()、notify()和notifyAll()是基类Object的一部分,而不属于Thread的一部分。所以,可以把wait()放到任何同步控制方法中,而不用考虑这个类是继承Thread还是实现了Runnable接口。
5.死锁
一个对象可以有synchronized方法或其他形式的加锁机制来防止别的任务在互斥还没有释放的时候就访问这个对象。
任务可以变成阻塞状态,所以可能出现:某个任务在等待另一个任务,而后者有等待别的任务,这样一直下去,知道这个链条上的任务又在等待第一个任务锁释放。这得到一个任务之间相互等待的连续循环,没有那个线程能继续,即称为死锁。