一、线程的五种状态及相互转换条件
从操作系统的角度,线程分为就绪、运行和阻塞三种状态。
线程的生命周期
创建:一个新的线程被创建,等待该线程被调用执行;
就绪:时间片已用完,此线程被强制暂停,等待下一个属于它的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。
java中定义了线程的六种状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,//无限等待-直到线程终结或线程被唤醒
TIMED_WAITING,//超时等待 到指定时间后线程自动被唤醒
TERMINATED;//线程终止-正常结束和异常被终止
}
新建(New)
线程创建后尚未启动。
可运行(Runnable)
一旦调用了线程的start()方法,线程就处于可运行状态。可运行状态的线程可能正在运行,也可能还没有运行而正在等待 CPU 时间片。(Java规范中并没有分为可运行状态和正在运行状态这两个状态,而是把它们合为一个状态。所以,可以把一个正在运行中的线程仍然称其处于可运行状态。)
阻塞(Blocked)
处于阻塞状态的线程并不会占用CPU资源。
以下情况会让线程进入阻塞状态:
①等待获取锁
等待获取锁分为两种情形:1、在对象锁等待池 2、在对象锁池 在对象锁池的同志可以竞争对象锁 在对象锁等待池的同志就需要先被调度到等待锁池了。。。。。。
等待获取一个锁,而该锁被其它线程持有,则该线程进入阻塞状态。当其它线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
②IO阻塞
线程发起了一个阻塞式IO后也会进入阻塞状态。最典型的场景,如等待用户输入内容然后继续执行。
无限期等待(Waiting)-需要唤醒
处于这种状态的线程不会被分配 CPU 时间片,需要等待其它线程显式地唤醒。
以下方法会让线程进入无限期的等待状态
- Object.wait() 方法 结束:Object.notify() / Object.notifyAll()
- Thread.join() 方法 结束:被调用的线程执行完毕
- LockSupport.park() 方法 结束:LockSupport.unpark(currentThread)
限时等待(Timed Waiting)
处于这种状态的线程也不会被分配CPU 时间片,在一定时间之后会被系统自动唤醒。
以下方法会让线程进入限期等待状态:
- Thread.sleep(time) 方法 结束:sleep时间结束
- Object.wait(time) 方法 结束:wait时间结束,或者Object.notify() / notifyAll()
- LockSupport.parkNanos(time)/parkUntil(time) 方法 结束:park时间结束,或者LockSupport.unpark(当前线程)
死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
注意:在有些书籍(如《Java并发编程实战》中5.4章节)或笼统的称呼中,就将阻塞、等待和超时等待这三种状态统称为阻塞状态。
2.几种状态的转换
yield让出cpu的使用权 进入就绪状态。
线程常见的方法
1.sleep
使当前线程休眠(暂停运行)指定的时间。
用法:Thread.sleep(time)
Thread.sleep(0)
Thread.sleep(0)的作用是触发操作系统立刻重新进行一次CPU竞争。
参考文章:代码中的Thread.sleep(0) 有什么意义?是写错了吗?
2.join
等待相应线程运行结束。若线程A调用线程B的join方法,那么线程A将会暂停运行,直到线程B运行结束。(谁join进来就等谁先运行完)
join实际上是通过wait来实现的。
3.yield
使当前线程主动放弃对处理器的占用,这可能导致当前线程被暂停。
这个方法是不可靠的,该方法被调用时当前线程可能仍然继续运行(视系统当前的运行状况而定)。
4.wait和notify()/notifyAll()
需要注意的是
①wait和notify/notifyAll方法只能在同步代码块里调用
wait()、notify()、notifyAll() 这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()、notify()、notifyAll() 都是在synchronized{}内部被调用的
②notify操作必须要在wait操作之后,否则,可能导致线程不会醒来。
③wait
5.LockSupport.park()/parkNano(time)/parkUntil(time)
LockSupport比Object的wait/notify有两大优势:
①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
②unpark方法可以先于park方法调用,所以不需要担心线程间的执行的先后顺序。
二、join的作用及实例
Join作用是将多线程的并行执行转换为串行执行。控制线程的执行顺序。
必须先调用线程的start方法 再调用join才有用的哦。
public static void main(String[] args) throws InterruptedException
{
System.out.println("main start");
Thread t1 = new Thread(new Worker("thread-1"));
t1.start();
t1.join();
System.out.println("main end");
}
在上面的例子中,main线程要等到t1线程运行结束后,才会输出“main end”。如果不加t1.join(),main线程和t1线程是并行的。而加上t1.join(),程序就变成是顺序执行了。
三、join底层分析
底层代码:
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0); //join()等同于join(0)
}
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0); //join(0)等同于wait(0),即wait无限时间直到被notify
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
可见底层是通过wait进行实现的,wait后进行waiting状态
也就是说:当前线程调用了另一个线程的join方法后 当前线程会被wait等待 等待另一个线程执行完成后才继续执行当前线程。但是从源码中并不能看到线程的notify方法,这是因为线程在die的时候会自动调用自身的notifyAll方法,来释放所有的资源和锁。
补充几道面试题:
1.java中线程的几种状态?状态间的转换?
看上图
2.挂起和等待有什么区别?
线程挂起-suspend 已经被废弃了。挂起不会释放锁资源 导致其他线程执行时会获取不到锁而无法继续执行。而且挂起的线程从线程状态上看仍然是Runnable状态,很难排查。
等待 -waiting 会释放锁资源 一般是因为某条件未达到 使线程等待 条件满足时 被唤醒。必须在同步代码块或同步方法中调用。
3.wait和sleep的区别?
wait | sleep | |
---|---|---|
同步 | 只能在同步上下文中调用wait方法,否则或抛出IllegalMonitorStateException异常 | 不需要在同步方法或同步块中调用 |
作用对象 | wait方法定义在Object类中,作用于对象本身 | sleep方法定义在java.lang.Thread中,作用于当前线程 |
释放锁资源 | 是 | 否 |
唤醒条件 | 其他线程调用对象的notify()或者notifyAll()方法 | 超时或者调用interrupt()方法体 |
方法属性 | wait是实例方法 | sleep是静态方法 |
4.sleep(0)有什么含义?
Thread.sleep(0)的作用是触发操作系统立刻重新进行一次CPU竞争。
5.wait、notify/notifyAll为什么要在synchronized块中调用?
wait()、notify()、notifyAll() 这三个方法能够被调用的前提是已经获取了相应的互斥锁。释放锁资源。所以需要在synchronized块中执行。
6.wait为什么要在while循环而非if中调用?
wait是由于某条件达不到才用的 但是执行唤醒后 可能其他条件又达不到了 while循环判断条件符合后才唤醒 条件达不到的话继续wait。
7.interrupt()和static interrupted()两者的区别?
public void interrupt():中断线程
public boolean interrupted():判断是否被中断
public static boolean interrupted():判断是否被中断,并清除当前中断状态。
8.stop,suspend/resume过时的原因?
stop太暴力 可能导致数据不一致;suspend和resume是一对操作 挂起不会释放锁资源 导致其他线程无法执行 而且如果不小心resume提前执行了 那么线程将永远不可能执行了。