Java Thread
Process & Thread
Process
进程有自己的执行环境,进程一般都有完整的私有的基本的运行时资源,并且每个进程有自己的内存空间。大部分的JVM都是以单进程的形式运行的。通过使用ProcessBuilder类Java应用程序可以创建更多的进程。
Thread
线程是轻量级的进程,进程和线程都有自己的运行环境,但是创建线程只需要很少的资源。
线程存在在进程之中,每个进程都至少有一个线程,线程分享进程的资源。我们从主类的main方法运行应用程序,它会创建一个主线程,这个线程可以创建其它的线程。
Thread
start
我们可以通过两种方式来构造我们自己的线程
- 继承Thread类。重写run方法,然后通过关键字new直接创建它的实例。
- 实现Runnable接口。实现run方法,然后通过Thread(Runnable r)这个构造函数来创建。
我们有了自己的线程对象,就使用start方法启动我们的线程。
sleep
sleep方法会使得当前运行的线程挂起一段时间。它可以使得其它线程获得执行机会。sleep方法的参数表示休眠时间,它可接受微秒级或者纳秒级的时间,然而我们根本不可能使用sleep来精确地控制线程的休眠时间,它与低层OS相关。如果我们要使一个线程让出CPU,那么我们可以使用interrupt方法来中断一个正在睡眠的线程,线程接收到中断信号它会立刻抛出InterruptedException,线程就中断了当前的执行。
那么你有什么理由让一个线程Sleep?
线程Sleep时间是不可控的,使用Sleep,你很难保证线程像预期一样执行。
如果一个线程正在很好的执行它的任务,你却毫无理由的让它睡觉去了,然后另一个线程在CPU里运行,等它让出CPU了,先前睡觉的线程又去运行。这有什么意义呢?CPU切换线程也是有开销的。而且你没有理由让一个正在很好地执行任务的线程Sleep呀!那如果线程工作得不好呢?线程工作地不好是因为执行的业务出了问题,我们应该编写一个健壮的线程当业务出现问题,线程应该自己懂得处理这些无效的工作。
有时候某个线程确实需要让出CPU一段时间,但是这一段时间是多少才合适呢?谁知道呢!所以你根本不知道要让线程Sleep多久,而且就算你知道,Sleep都不能保证让线程睡那么长的时间。
如果线程确实要退让CPU,比如一个线程向一个缓冲区添加东西,当缓冲区满的时候,它就要让出缓冲区对象,让那些读取缓冲区的线程使用。这些都属于多线程协作的内容,我们要使用wait和notifyAll这样的方法。
interrupt
调用线程的interrupt方法――实际上线程不一定会中断,为什么呢?
实际上调用线程的interrupt方法只是给线程对象发送了一个中断信号,线程会检察这个中断信号的值,但即使是发现了中断信号,线程也不会立即中断。
还记得那个InterruptedException异常么?如果从线程中断的角度来看,抛出一个这样的异常实际上是给程序员一个处理中断的入口。那些会抛出InterruptedException异常的线程方法检察到中断信号后抛出此异常,程序员就在catch子句里面写中断处理代码让线程停止当前做的事情转而去做别的事情。这也恰好是interrupt方法的作用。
那如果我们自定义的线程根本不会去调用那些能抛出InterruptedException的方法时,如何中断当前的程序呢。既然调用interrupt只是发送出改变一个中断信号量的状态,那么API当然提供一个方法让我们来检察这个信号量,Thread的静态方法interrpted可以检察当前是否要发生中断。
PS:一个CPU内核一个时间只能执行一个Thread,于是Thread.interrupted()静态方法就可以知道当前线程是否接受到一个中断信号。
有哪些方法会抛出InterruptedException异常?sleep,join,对象的wait方法
join
join方法可以让当前运行的线程等待某个线程运行完毕之后才运行。
这里涉及到两个线程,一个是调用join方法的消息发出者线程,一个是那个接收join消息的线程。是消息发出者线程等待那个消息接收者线程运行完毕自己才运行。听上去有些绕口,写个代码试试就知道了。
yield
线程想要放弃运行,有人说使用yield方法会使用自己的优先级降低以便于别的线程获得运行机会,有的人说使用yield方法会使得与自己有相同优先级的线程得到运行的机会,但有些人也说使用yield方法之后的线程可能会立即得到运行的机会。我也在查阅资料,自己也写了一些代码并分析代码运行的结果,可惜并没有找到什么规律。我只能说与优先级有关,但到底是怎么回事,我也没有弄明白。但Java支持1到10总共10个优先级,我们OS能支持的优先级等级数量我们就不知道了,平台不一致,使用这个方法我们无法预料哪些线程会在将来获得运行机会,这种不确定因素使得我们大都放弃使用这个方法。
Synchronization
死锁
我需要A却占有B,你需要B却占有A,我不给你B,你不给我A,真是冤冤相报何时能了?程序卡了,不动了。哈哈,我要点击Eclipse上的红色小按钮才能终止程序的运行。
活锁
老妈叫我去买菜,可是想用10.1狂冲级,魔兽世界的任务做个没完没了,老妈不来催我,我是永远也不会买菜了。我忙于接受新任务没有时间去做买菜,最终整个事件产生很恶劣的结果,那就是我在中午的时候挨骂了。
线程太忙了,没有时间做那个很久以前就接受了的任务,最后程序崩溃了。
冲突
PS: 我写的代码很烂,如果文章中要包含代码,那只会让文章看起来长篇大论。但为了叙述方便还是贴吧。
public class ThreadTest {
static class CounterThread1 extends Thread{
Counter c;
public CounterThread1(String s, Counter c) {
super(s);
this.c = c;
}
public void run() {
System.out.println(this.getName()+"取得C");
c.increment();
System.out.println(this.getName()+"输出C值为:"+c.value());
}
}
static class CounterThread2 extends Thread{
Counter c;
public CounterThread2(String s, Counter c) {
super(s);
this.c = c;
}
public void run() {
System.out.println(this.getName()+"取得C");
c.decrement();
System.out.println(this.getName()+"输出C值为:"+c.value());
}
}
public static void main(String[] arg) {
Counter c = new Counter();
Thread t1 = new CounterThread1("A", c);
Thread t2 = new CounterThread2("B", c);
t1.start();
t2.start();
}
}
class Counter {
private int c = 0;
public void increment() {
System.out.println("A加1:"+ ++c);
}
public void decrement() {
System.out.println("B减1:"+ --c);
}
public int value() {
return c;
}
}
看一个典型结果:
A取得C
B取得C
A加1:1
B减1:0
A输出C值为:0
B输出C值为:0
看到没有,A加了1,但是后面A输出的结果却是0,因为C的值被线程B减了1,因此发生了覆盖。
这里演示一个结果,如果我们的程序中有这样的问题,那么这里很容易产生Bug。像这样的Bug你很难检测即使检测到了也不容易重现,像上面的结果是我在Eclipse下运行了几遍才出来的结果,要重现这个Bug,你得多运行几遍才能出来。这只是一个小的程序,如果是个比较大的程序,你可能会得到许多Bug报告,而这些Bug都是由于线程冲突产生那些不可预料结果导致的。
同步
我们需要一种机制让线程以我们期望的方式运行。一个简单的处理是修改Count类。
class Counter {
private int c = 0;
public synchronized void increment() {
System.out.println("A加1:"+ ++c);
}
public synchronized void decrement() {
System.out.println("B减1:"+ --c);
}
public synchronized int value() {
return c;
}
}
这样能不能解决问题?我在Eclipse里疯狂地点击运行,看每一次运行的结果。结果其实很惨,我们根本不能预料运行结果。加了synchronized和没加一样。拿线程A来说,A运行方法increment()时B确实是不能访问Counter的,但是一旦increment()这个方法执行完了呢?完了之后Counter又游离了,这个时候B趁火打劫抢了Counter自己跑了。
我拿着这个在Sun文档里抄来的例子,很傻地把它放到Eclipse里运行。现在想想我到底要做什么?我希望的是A和B的运行不发生冲突,也就是A运行的时候B不能处理Counter,或者B运行的时候A不能处理Counter。显然,如果A在处理Counter的时候,A绝对不能放弃使用Counter的权利,以B为例亦然。
把代码改了再试试。
public class ThreadTest {
static class CounterThread1 extends Thread {
Counter c;
public CounterThread1(String s, Counter c) {
super(s);
this.c = c;
}
public void run() {
synchronized (c) {
System.out.println(this.getName() + "取得C");
c.increment();
System.out.println(this.getName() + "输出C值为:" + c.value());
}
}
}
static class CounterThread2 extends Thread {
Counter c;
public CounterThread2(String s, Counter c) {
super(s);
this.c = c;
}
public void run() {
synchronized (c) {
System.out.println(this.getName() + "取得C");
c.decrement();
System.out.println(this.getName() + "输出C值为:" + c.value());
}
}
}
public static void main(String[] arg) {
Counter c = new Counter();
Thread t1 = new CounterThread1("A", c);
Thread t2 = new CounterThread2("B", c);
t1.start();
t2.start();
}
}
class Counter {
private int c = 0;
public void increment() {
System.out.println("A加1:" + ++c);
}
public void decrement() {
System.out.println("B减1:" + --c);
}
public int value() {
return c;
}
}
上面的代码可以完全Copy到文件里面运行,它换了一种同步方式,原则是没有处理完Counter就不放弃使用Counter的权利。多运行几遍这个程序,看到这个程序会出现两种结果:A在前面运行然后运行B或者B在前A在后。
那么第一种方法到底废不废?相比之下,我比较倾向第二种。我觉得优秀的设计总是倾向于代码简单的小方法,那么要完成一个具体的复杂的任务,我们完全有可能对一个对象的多个方法调用多次。我始终认为任务没做完,这个对象的使用权就不轻易让出,我认为在方法上控制同步可能开销要更大,我老师说:释放和获取一个对象锁是有开销的,那么如果对一个同步方法多次调用,它的释放和获取对象锁的开销是很大的。
当然上面的Counter类中每个方法只有一行语句,这点很值得注意,如果方法有两行或者更多语句了情况可能又不一样。
代码多于文字的篇幅了。
工作队列
一个工作队列里有许多工作任务,这些工作任务由多个工作者来完成。每个工作者完成了自己的工作就再去队列里再取一个工作任务来做,如果队列里没有任务了,就结束运行。直到到所有的工作者都结束运行,那么工作完成。
我写了一个数组实现的队列。
class Queue<T> :
public T offer(T t) {
//t不能为空
if (t == null) {
throw new NullPointerException(""); // t can not be null
}
//size表示队列目前有多少数据,capacity表示队列最多能容纳多少数据
if (size == capacity) {
//size == capacity 表示队列已满,立即返回空
return null;
}
//往队列头放元素
queue[head] = t;
head = (head + 1) % capacity;
size++;
return t;
}
public T poll() {
//如果队列里有数据
if (size != 0) {
//取队列尾的数据
T t = (T) queue[tail];
queue[tail] = null;
tail = (tail + 1) % capacity;
size--;
return t;
}
//如果队列没有数据,直接返回空
return null;
}
这个队列没有一个同步方法,我通过继承创建一个子类的实现同步方法。
class WorkQueue extends Queue<ITask> :
public synchronized ITask putTask(ITask task) {
return super.offer(task);
}
public synchronized ITask fetchTask() {
return super.poll();
}
此类的方法签名更加容易理解,它表示一个任务队列,有添加任务和取任务的方法。
这些方法是同步的。
ITask是一个接口:
public interface ITask extends Runnable {
String taskDescription();
}
实现ITask的run方法定义一个具体的工作流程。
现在工作有了,工作队列也有了,缺少工作者。
工作者是线程,注意程序运行之初就分配工作任务,所以工作队列在很早之前就会初始化好,run实现就像下面那样:
public void run() {
ITask task;
while (true) {
//取一个工作
task = workQueue.fetchTask();
//队列已经没有工作时,就结束运行了
if (task == null) {
return;
}
//工作开始
task.run();
task = null;
}
//没工作了,线程就结束运行了
}
工作队列的好处就在于处理那些数量多而短小的任务可以减少线程创建和销毁的开销。我们不可能对每个文件创建一个单独的读写线程,这好几百个文件由一个工作队列来读取,队列有好几个工作者线程,比如说有5个,每个工作者线程都一个接一个地从执行工作队列中的任务,那么一个工作队列需要几个这样的工作者线程?依据是你的性能需求。增加线程不一定能加快完成任务的时间,多线程增加的只是CPU的利用率,并且如果计算机只能承受10个线程运行,超过这个数字CPU占用率99%,如果是这样那么多于10条线程就没有什么意义。
写线程与读线程 I
写线程与读线程之间交换的数据,为了有更好的性能,我们经常把这些数据被组织在一个缓冲区中,这可以减少线程的切换开销。
何时读数据?何时写数据?何时让出缓冲区?
写线程从缓冲区取数据写入设备或者数据库。那么写线程就应该把缓冲区中的数据全部取出来才让出缓冲区。
while (true) {
synchronized (cache) {
while (cache.isEmpty()) {
try {//缓冲区是空的,就等待
cache.wait();
} catch (InterruptedException e) {
}
}
//缓冲区不是空的,就读取。
while (!cache.isEmpty()) {
从缓冲区读取数据,并写入设备。
}
//缓冲区数据处理完了就发出通知
cache.notifyAll();
}
读线程从文件中读取数据并封装为对象存入缓冲区中。相应的,它应该在缓冲区满时才让出缓冲区。
while (true) {
synchronized (cache) {
while (cache.isFull()) {//缓冲区满的就等等
try {
cache.wait();
} catch (InterruptedException e) {
}
}
//不满就可以往缓冲区写数据了
while (!cache.isFull()) {
读取文件,封装数据,存入缓冲区
}
cache.notifyAll();
}
}
上面的代码简化了很多,最关键的东西都出来了。我们可以预料到当一个线程让出了CPU后会发生什么。如果一个读线程把缓冲区填满了,退出读取文件数据的循环,调用notifyAll(),这时所有在等待队列中的线程会被唤醒,自己则由于循环运行到wait,这时它退让了CPU,那么下一个线程会是谁?我们期望下一个线程应该是一个写线程。但实际上可能是写线程也可能是另一条读线程,如果是写线程,那就是我们期望的,继续;如果是读线程,这个时候cache还是满的,它又立刻进入等待中,这个时候CPU又让出了,前面那些被唤醒的线程仍旧在抢夺CPU,极端情况每次都是读线程先抢到CPU但每一次进行缓冲区为满的判断后就wait了,所有的读线程都进入等待队列了,这时那些处于唤醒状态的写线程总有一个会得到CPU的,而在你看来,读线程写缓冲区满了,写线程就运行了。那么反过来,写线程把数据写完了,我们也可预料结果。
写线程与读线程 II
要注意一些问题,读线程把所有的文件都读取完了然后怎么办?啊,当然是先notifyAll并且return,这样这个读线程就结束运行了。
缓冲区的数据处理完了自己就等待了,等待缓冲区有新数据,然而文件都读完了不可能有新的数据再进入缓冲区,读线程是正常结束了,而写线程呢?如果有很多写线程,这些写线程会全部进入等待状态,更严重的是没有其它线程可以通过notifyAll来将它们从等待队列唤醒出来了,这些线程会全部存在在等待队列中永远都不能结束,我们的程序出现了不能正常结束的Bug。
所以读线程在结束前要告知写线程:“所有的文件都读完了,你们把数据处理完之后就可以回家了!”这里有几个细节:首先设置一个任务数,每完成一个任务就进行一次自减,当任务都完成了,剩余任务就为0,,然后是告知那些线程,所以要notifyAll,然后是return。而写线程在把缓冲区中所有的数据都处理完之后就去访问那些剩余任务数标志,如果是剩余任务数为0,说明任务已经全部完成,于是结束运行。
wait, notify, notifyAll
wait使得当前线程主动放弃对象锁,notify通知等待队列中的某一个线程对象锁可用,notifyAll通知等待队列中的所有线程对象锁可用。
wait很容易理解,但还要说明一下,一个线程wait之后一旦被唤醒是重新开始运行吗?
notify通知某一个线程资源可用,具体是哪一个我们不知道。某些应用中,我们可以确定notify唤醒的线程,那么使用notify是不错的选择。但在绝大多数情况下我们用了一种散弹式的方法――notifyAll,它让所有线程都知道对象可用了,结下来的工作交给CPU。
还是给一段代码看一下:
public class MainClass {
static class NotifyThread extends Thread {
int[] lock;
public NotifyThread(int[] lock) {
this.lock = lock;
}
public void run() {
synchronized(lock) {
System.out.println(this + " notifyThread notify");
lock.notifyAll();
}
}
}
static class WaitThread extends Thread {
int[] lock;
public WaitThread(int[] lock) {
this.lock = lock;
}
public void run() {
synchronized(lock) {
try {
System.out.println(this + " waitThread is waiting");
lock.wait();
System.out.println(this + " waitThread is notified");
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
int[] lock = new int[0];
Thread[] waitThread = new WaitThread[5];
for(int i=0; i<5;i++) {
waitThread[i] = new WaitThread(lock);
waitThread[i].start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
Thread notify = new NotifyThread(lock);
notify.start();
}
}
上面的代码可以编译运行且可以正常结束。
程序试图先让五个线程等待,然后再由一个线程唤醒,在大部分情况下运行出来的结果都是我期望的结果。我们主要关注wait,notify,notifyAll。运行打印的出来信息我就不贴了,大家可以自己运行一下。从结果可以看出线程wait再被唤醒是从调用wait的那行语句继续执行的,这样很好也必需这样,但这也是造成某些死锁的前提,上面所有程序都讨论一个对象共享资源,如果多个那就要协调好了,拿两个A和B来说,一个线程等待A之后被唤醒继续执行在某个时刻请求B资源,而另一个资源等待B之后被唤醒继续执行在某个时刻请求A,循环等待死锁了,一个拙而有效的办法是只有拿到了A且B才运行,效率虽低但不会出错。我将在以后或补充或再写一篇文章来解决这种更加复杂的需要。
回到我上面给出的代码来看一下notify怎么用。A通知B,B通知C,C通知D………
static class NotifyThread extends Thread {
int[] lock;
public NotifyThread(int[] lock) {
this.lock = lock;
}
public void run() {
synchronized(lock) {
System.out.println(this + " notifyThread notify");
lock.notify();
}
}
}
static class WaitThread extends Thread {
int[] lock;
public WaitThread(int[] lock) {
this.lock = lock;
}
public void run() {
synchronized(lock) {
try {
System.out.println(this + " waitThread is waiting");
lock.wait();
System.out.println(this + " waitThread is notified");
} catch (InterruptedException e) {
}
lock.notify();
}
}
}
把代码改了一下。它运行的结果和我期望的大部分时间都是相同的,和前面的写法运行的结果也相符。在这个例子中,等待线程waitThread都等待之后,通知线程notifyThread发出通知,我们可以想象通知的一定是某个等待中的线程waitThread,接下的情况依此类推,我就不重复说了。
线程生命周期
上面的内容已经涉及到了线程生命周期的问题。很前面介绍的sleep方法使线程进入休眠状态,对象的wait方法使线程进入等待状态,而notifyAll则可以让线程进入被唤醒的状态,它在跑(run)的时候,就是执行状态。这些状态是比较口语化的说法,实际上不同的OS,其进程和线程的状态种类和状态数也不完全相同。
当线程被创建(初始化,实例化),它就是“新线程”。在Java中我们new了一个线程,只只是new了而已。它是一个“新线程”。我认为这个时候它只是有了内存空间而已。
接下来我们通过start启动一个线程。实际上通过start方法启动一个,只是将线程加入到系统的一个队列里而已。这个队列是“就绪队列”,有线程让出CPU后,CPU借自己的调度算法从就绪队列中取一个线程来运行,也就是说start并不会让我们的线程立刻占用CPU执行,而线程处于就绪队列中,它就是处于就绪态。当被CPU选中后,调度执行,线程进入执行状态。在执行过程中可能要某个资源不可用,线程必需等待这个资源可用时才能接着运行,它让出CPU,进入等待队列,处于等待态。当资源可用时,它马上得运行所需的资源,又进入就绪态,等候CPU调度。当所有的任务都完成,线程进入结束运行状态,释放自己的资源,到最后彻底被销毁。就绪状态是只差CPU调度,等待态是等待某个资源。
wait会使当前进入等待态,而notifyAll会使等待队列中的所有线程进入就绪态。这或多或少会带来一些性能的问题。不得不提notify方法,它会使等待队列中的某一个线程进入就绪态,它的性能好得多,可是唤醒的是哪一个线程我们不得而知,因此notify方法会给我们带来不可预知性,许多建议告诉我们尽量不使用notify方法。sleep会使线程进入什么状态?看起来没有执行,所以不是执行状态,sleep并不释放对象锁(不释放资源),因此也不像是等待状态。那看起来像就绪态了,可是一旦就绪态的线程CPU选中它就一定执行了,尽管我们为Sleep指定的时间并不能精确控制线程的行为,但是或多或少它会影响CPU对线程的调度,所以也不像是就绪态。补充一个状态,叫做休眠状态。不同的OS对线程的休眠状态处理都不一样,线程的休眠状态会让线程休眠(挂起)一段时间,然后线程又进入就绪状态等候CPU调度运行,之所以使用Sleep有这么不确定性,和OS对休眠状态处理有关也和OS对就绪状态线程调度算法有关。join方法看起来像是比较高级的API,因为它使得一个线程等待另一个线程完成之后才运行,我不知道底层的OS有没有实现这个功能,我没法讲线程调用join之后会处于什么状态,在我看来,它应该是在等待状态吧。
最后
我大学刚毕业才工作三个月,学别人写点文章,平时做些东西遇到许多困难,Google是我最好的助手。包括这篇文章,其中有些是翻译Sun的文档而来,有些是看了别人的文章抄来的,有些需要自己动手验证,有些又是自己的想法,这一篇主要介绍线程都是些比较基础的内容。写这样的文章,为了自己今后有个参考,更是为了和大家交流,我没有什么实践经验,因此我写的不一定是正确的,尽信书不如无书,一个优秀的软件设计师不仅要懂得吸收知识,也要懂得求证。
赞美是一种美德,好的批评也是一种美德。谢谢!