2.1 有关线程你必须知道的事
-
进程是系统进行资源分配和调度的基本单位,是程序的基本执行实体。
-
线程就是轻量级进程,是程序执行的最小单位。
-
线程的生命周期,如图2.3所示。
-
线程的所有状态都在Thread中的State枚举中定义,如下所示:
public enum State {
NEW,
RUNABLE,
BLOKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW状态表示刚刚创建的线程,这种线程还没有开始执行。等到线程的start()方法调用时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在 执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。WAITING和TIMED_WAINTING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAINTING会进入一个有时限的等待。那等待的线程究竟在等什么呢?一般来说,WAINTING的线程正是在等待一个特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。
2.2 初始线程:线程的基本操作
2.2.1 新建线程
- 只要使用new关键字创建一个线程对象,并且将它start()起来即可。
Thread t1 = new Thread();
t1.start();
- 线程Thread,有一个run()方法,start()方法就会新建一个线程并让这个线程执行run()方法。
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("Hello, I am t1");
}
};
t1.start();
- 上述代码使用匿名内部类,重载了run()方法。
- 考虑Java是单继承的,也就是说继承本身也是一种很宝贵的资源,因此,我们也可以使用Runnable接口来实现同样的操作。Runable接口是一个单方法接口,它只有一个run()方法:
public interface Runnable {
public abstract void run();
}
- Thread类有一个非常重要的构造方法:
public Thread(Runnable target)
public class T1 implements Runnable {
public static void main(String[] args) {
Thread t1 = new Thread(new T1());
t1.start();
}
@Override
public void run() {
System.out.println("Oh, I am Runnable");
}
}
2.2.2 终止线程
-
Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会 被写坏,同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章的读到了这个不一致的对象,悲剧也就此发生。整个过程如图2.4所示。
-
可以自行决定线程何时退出,如下所示:
public static class ChangeObjectThread extends Thread {
volatile boolean stopme = false;
public void stopMe() {
stopme = true;
}
@Override
public void run() {
while (true) {
if (stopme) {
System.out.println("exit by stop me");
break;
}
synchronized (u) {
int v = (int) (System.currentTimeMillis() / 1000);
u.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
u.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
2.2.3 线程中断
- 与线程中断有关的三个方法:
public void Thread.interrupt() //中断线程,设置中断标志位
public boolean Thread.isInterrupted() //判断是否被中断(通过检查中断标志位)
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
- 下面这段代码对t1线程进行了中断,那么中断后,t1会停止执行吗?
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
- 在这里,虽然对t1进行了中断,但是在t1中并没有中断处理的逻辑,因此,即使t1线程被置上中断状态,但是这个中断不会发生任何作用。
- 如果希望t1在中断后退出,就必须为它增加相应的中断处理代码:
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interruted");
break;
}
Thread.yield();
}
}
}
- 下面,先来了解一下Thread.sleep()函数,它的签名如下:
public static native void sleep(long millis) throws InterruptedException
- Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted!");
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Interrupted When Sleep");
//设置中断状态
Thread.currentThread().interrupt();
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
- 执行Thread.sleep(),线程被中断,则程序会抛出异常,进入catch中,但没有立即退出线程。因为还要进行后续的处理,保证数据的一致性和完整性,因此,执行了Thread.interrupt()方法再次中断自己,置上中断标记位。只有这样做,在Thread.isInterrupted()检查中,才能发现当前线程已经被中断。
- 注意:Thread.sleep()方法会清除中断标记,故再次设置中断标记位。
2.2.4 等待(wait)和通知(notify)
- 任何对象都可以调用这两个方法。
public final void wait() throws InterruptedException
public final native void notify()
-
当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。一直等到其他线程调用了该对象的notify()方法为止。
-
图2.5展示了两者的工作过程。
-
强调一点,Object.wait()方法必须包含在对应的synchronized语句中,无论是wait()或者notify都需要首先获得目标对象的一个监视器。如图2.6所示,显示了wait()和notify()的工作流程细节。
-
为了方便大家理解,这里给出了一个简单地使用wait()和notify()的案例:
public class SimpleWN {
final static Object object = new Object();
public static class T1 extends Thread {
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T1 start!");
try {
System.out.println(System.currentTimeMillis() + ":T1 wait for object");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static class T2 extends Thread {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T2 start! notify one thread");
object.notify();
System.out.println(System.currentTimeMillis() +":T2 end!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
Thread t1 = new T1();
Thread t2 = new T2();
t1.start();
t2.start();
}
}
- 上述代码中,开启了两个线程T1和T2。T1执行了object.wait()方法。在T1中执行wait()方法前,T1先申请object的对象锁。因此,在执行object.wait()时,它是持有object的锁的。wait()方法执行后,T1会进行等待,并释放object的锁。T2在执行notify()之前也会获得object的对象锁。这里为了让实验效果明显,特意安排在notify()通知后,让T2休眠2秒钟,这样做可以更明显地说明,T1得到notify()通知后,还是会先尝试重新得到object的对象锁。
T1 start!
T1 wait for object
T2 start! notify one thread
T2 end!
T1 end!
- 注意:Object.wait()和Thread.sleep()方法都可以让线程等待若干时间。除了wait()可以被唤醒外,另外一个主要的区别是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。
2.2.5 挂起(suspend)和继续执行(resume)线程
-
这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()操作后,才能继续指定。
-
不推荐使用suspend()去挂起线程的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常运行(如图2.7所示)。直到对应的线程上进行了resume()操作,被挂起的线程才能继续,从而其他所有阻塞在相 关锁上的线程也可以继续执行。但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,居然还是Runnable。
-
为了方便大家理解suspend()的问题,这里准备一个简单的程序。演示了这种情况:
public class BadSuspend() {
public static Object u = new Object();
static ChangeOjectThread t1 = new ChangeObjectThread("t1");
static ChangeOjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println("in " + getName);
Thread.currentThread().suspend();
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.resume();
t2.resume();
t1.join();
t2.join();
}
}
in t1
in t2
2.2.6 等待线程结束(join)和谦让(yield)
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
- 第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。第二个方法给出了最大等待时间,如果超出给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。
public class JoinMain {
public volatile static int i = 0;
public static class AddThread extends Thread {
@Override
public void run() {
for (i = 0; i < 10000000; i++);
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at = new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
- 主函数中,如果不使用join()等待AddThread,那么得到的i很可能是0或者一个非常小的数字。因为AddThread还没开始执行,i的值就已经被输出了。但在使用join()方法后,表示主线程愿意等待AddThread执行完毕,故在join()返回时,AddThread已经执行完毕,故i总是10000000。
- join()的本质是让调用线程wait()在当前线程对象实例上。
while (isAlive()) {
wait(0);
}
- 可以看到,它让调用线程在当前线程对象上进行等待。
- 另外一个有趣的方法,是Thread.yield(),它的定义如下:
public static native void yield();
- 这是一个静态方法,一旦执行,它会使当前线程让出CPU。
- 如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield()。