线程的生命周期
学一个东西肯定得先了解它的生命周期,也就是状态的变化。Java中,线程大概有这5个状态:
-
新建(new):使用new操作符新建的线程
-
就绪(runnable):线程在等待调度以获得CPU执行时就处于该状态。比如一个new出来的线程被调用start方法。此外,阻塞态的线程也会因为以下原因被恢复为就绪态:
① 调用sleep()方法的线程经过了指定时间。
② 线程调用的阻塞式IO方法已经返回。
③ 线程成功地获得了试图取得的同步监视器。
④ 线程正在等待某个通知时,其他线程发出了个通知。
⑤ 处于挂起状态的线程被调甩了resdme()恢复方法。
-
运行(Running):当就绪态的线程获得CPU执行时就处于运行状态
-
阻塞(Blocked):在运行态的线程可能会因为以下原因而变为阻塞态:
① 线程调用sleep()方法主动放弃所占用的处理器资源
② 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③ 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将存更深入的介绍
④ 线程在等待某个通知(notify)
⑤ 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
- 销毁(Terminated):如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁以释放资源
从图中可知,阻塞态的线程是没法直接运行的,它只能重新进入就绪队列,等待CPU的调度。
线程调度
Java提供了非常多的API来手动调度线程:
- Thread.yield(): 调用该方法可以让当前线程暂时让出CPU,也就是把一个线程从运行态转化为就绪态。但由于Java的线程调度方式是抢占式的,所以一个线程被调用yield让出CPU资源后有可能立刻又会抢占到CPU,所以该方法实际不太可控,在开发种也较少用到
- Thread.sleep(time):让当前线程休眠指定时间,被休眠的线程虽然会进入Blocking状态并让出CPU,但是缺不会让出其占用的锁。这是sleep和wait最大的区别
- Thread.interrupted():中断处于阻塞态的对应线程,该方法和不被推荐使用的suspend以及stop不同,interrupted不会去强行中断一个处于就绪或运行态的线程,调用该方法只会将线程内部的一个中断标识符置为true,线程在运行中不会去检测该标识符的状态,但线程被阻塞时中断标识符如果被设置为true,则线程会中断当前的阻塞态,抛出异常并结束自己的生命周期
- Thread.join(): join方法是等待线程执行完毕,是线程间通信的一个常用方法,一个线程调用另一个线程的join方法后就会进入阻塞状态,直到对应线程执行完毕。
- Object.wait(): 和sleep方法不同,wait方法不是Thread自身提供的,而是Java的顶层类Object自带的。调用一个Object的wait方法时,需要确保Object本身是一个同步代码块的同步锁。该方法实际就是在一个以obj1为同步锁的代码块中,让出object1,并且让线程进入类似休眠的状态。
- Object.notify & Object.notifyAll:和wait类似,调用Object.notify以及Object.notifyAll也需要Object对象处于一个以自身为锁对象的同步代码块里,调用这两个方法会唤醒在这个Object上调用过wait的对象,如果是notifyAll则会唤醒所有的在这个Object上wait过的对象。wait和notify也是线程间通信的重要方法。
下面是相关的示例代码:
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("t1 is running");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 will dead");
}
}); // 进入新建状态
t1.start(); // 进入就绪状态
Thread.sleep(5000);
synchronized (lock) {
System.out.println("t1明显被阻塞");
lock.notify();
System.out.println("t1即将被唤醒");
lock.wait();
}
}
通过这个示例可以很好的理解notify和wait的用途。
除了上述的API外,使用Java的ReentryLock.newCondition我们可以获得一个Condition对象,该对象有await和signal方法,使用这两个方法我们也可以做到和wait与notify一样的事情,但是不同的是代码不再需要放置在一个同步代码块中,示例代码如下:
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Condition cond = lock.newCondition();
Thread t1 = new Thread(() -> {
System.out.println("t1 is running");
try {
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 进入新建状态
t1.start(); // 进入就绪状态
Thread.sleep(5000);
System.out.println("t1明显被阻塞");
cond.signal();
System.out.println("t1即将被唤醒");
}
另外,除了上述的两种方式,我们通过Java提供的LockSupport也可以实现唤醒或阻塞指定线程的方式。LockSupport提供了park()方法来阻塞当前线程,以及upark(Thread t)来唤醒线程t。实际上追溯到底层实现的话,ReentryLock也是通过LockSupport方法来唤醒以及阻塞线程的。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("t1 is running");
LockSupport.park();
System.out.println("t1 will dead");
}); // 进入新建状态
t1.start(); // 进入就绪状态
Thread.sleep(5000);
System.out.println("可以看到,t1明显被阻塞,5s过去都没有结束");
System.out.println("t1即将被唤醒");
LockSupport.unpark(t1);
}