线程状态及其属性
线程状态及其属性
线程的 6 种状态:
- New(新建)
- Runable(可运行)
- Blocked(阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminates(终止)
要确定一个线程的当前状态,只需要调用 getState()
方法。
1. 新建线程
一个 java 程序从 main()
方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 java 程序天生就是一个多线程程序,包含了:
- (1)分发处理发送给给 JVM 信号的线程;
- (2)调用对象的 finalize 方法的线程;
- (3)清除 Reference 的线程;
- (4)main 线程,用户程序的入口。
在用户程序中新建一个线程,一般有四种方式:
通过继承
Thread
类,重写run
方法;通过实现
runable
接口;通过实现
callable
接口;通过线程池创建。
创建线程池Demo:
public class CreateThreadDemo {
public static void main(String[] args) {
// 1.继承Thread
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("继承Thread");
super.run();
}
};
thread.start();
// 2.实现runable接口
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现runable接口");
}
});
thread1.start();
// 3.实现callable接口
ExecutorService service = Executors.newSingleThreadExecutor();
Future<String> future = service.submit(new Callable() {
@Override
public String call() throws Exception {
return "通过实现Callable接口";
}
});
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
第四种通过线程池新建线程后面单拎出来。
以上三种新建线程的方式需要注意的是:
由于 java 不能多继承可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式;
实现
callable
接口,提交给 ExecutorService 返回的是异步执行的结果,另外,通常也可以利用FutureTask(Callable callable)
将 callable 进行包装然后 FeatureTask 提交给 ExecutorsService。如下图所示:另外由于 FeatureTask 也实现了 Runable 接口,也可以利用上面第二种方式(实现 Runable 接口)来新建线程;
可以通过 Executors 将 Runable 转换成 Callable,具体方法是:
Callable callable(Runnable task, T result); Callable callable(Runnable task);
注意
不要调用 Thread 类或 Runnable 对象的 run
方法。直接调用 run
方法只会在同一个线程中执行这个任务,而没有启动新的线程。
实际上,应当调用 Thread.start()
方法,这会创建一个新线程来执行 run
方法。
2. 可运行线程
一旦调用 start
方法,线程就处于可运行状态。一个可运行的线程可能正在运行,也可能没有运行,要由操作系统为线程提供具体的运行时间。
一旦一个线程开始运行,它不一定始终保持运行。事实上,运行中的线程有时需要暂停,让其他线程有机会运行。线程调度的细节依赖于操作系统提供的服务。
抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完时,操作系统会剥夺该线程的运行权,并给另一个线程一个机会来运行。当选择下一个线程时,操作系统会考虑线程的优先级。
所有现代桌面和服务器操作系统都是用抢占式调度。但是,对于像手机这样的小型设备可能使用协作式调度。在这样的设备中,一个线程只有在调用 yield
方法或者像被阻塞或等待时才失去控制权。
在有多个处理器的机器上,每个处理器可以运行一个线程,而且可以有多个线程并行运行。但如果线程数多于处理器的数目,调度器仍然需要分配时间片。
3. 阻塞和等待线程
当线程处于阻塞或等待状态时,它是暂时不活动的,不执行任何代码,且消耗最少的资源。要由线程调度器重新激活这个线程。
- 当一个线程试图获取一个内部的对象锁,而这个锁目前被其他线程占有,该线程就会被阻塞。当所有其他线程都释放了这个锁,并且线程调度器允许该线程持有这个锁时,它将变成非阻塞状态。
- 当线程等待另一个线程通知调度器出现某个条件时,这个线程会进入等待状态。
4. 终止线程
线程会由于以下两个原因之一而终止:
- 由于
run
方法正常退出,线程自然终于。 - 由于一个没有捕获到的异常终止了
run
方法,使线程意外终止。
5. 线程状态转换
此图来源于《JAVA并发编程的艺术》一书中,线程是会在不同的状态间进行转换的,java 线程线程转换图如上图所示。
线程创建之后调用
start()
方法开始运行,当调用wait()
,join()
,LockSupport.lock()
方法线程会进入到 WAITING 状态;而同样的
wait(long timeout)
,sleep(long)
,join(long)
,LockSupport.parkNanos()
,LockSupport.parkUtil()
增加了超时等待的功能,也就是调用这些方法后线程会进入 TIMED_WAITING 状态;当超时等待时间到达后,线程会切换到 Runable 的状态,另外当 WAITING 和 TIMED _WAITING 状态时可以通过
Object.notify()
,Object.notifyAll()
方法使线程转换到 Runable 状态;当线程出现资源竞争时,即等待获取锁的时候,线程会进入到 BLOCKED 阻塞状态;
当线程获取锁时,线程进入到 Runable 状态;
线程运行结束后,线程进入到 TERMINATED 状态。
状态转换可以说是线程的生命周期。另外需要注意的是:
当一个线程被重新激活,调度器会先检查它是否具有比当前允许线程更高的优先级。如果是,则调度器会剥夺某个当前允许线程的运行权,选择运行该新线程。
当线程进入到
synchronized
方法或者synchronized
代码块时,线程切换到的是 BLOCKED 状态,而使用 java.util.concurrent.locks 下 lock 进行加锁的时候线程切换的是 WAITING 或者 TIMED_WAITING 状态,因为 lock 会调用LockSupport
的方法。
用一个表格将上面六种状态进行一个总结归纳。
6. 线程的基本操作
6.1 interrupted
中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的 interrupt()
方法对其进行中断操作,同时该线程可以调用 isInterrupted()
来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用 Thread 的静态方法 interrupted()
对当前线程进行中断操作,该方法会清除中断标志位。
注意
当抛出 InterruptedException 时候,会清除中断标志位,也就是说在调用 isInterrupted()
会返回 。
结合具体的实例看一看:
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
// sleepThread睡眠1000ms
final Thread sleepThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
};
// busyThread一直执行死循环
Thread busyThread = new Thread() {
@Override
public void run() {
while (true) ;
}
};
sleepThread.start();
busyThread.start();
sleepThread.interrupt();
busyThread.interrupt();
while (sleepThread.isInterrupted());
System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
}
}
输出结果:
sleepThread isInterrupted: false
busyThread isInterrupted: true
开启了两个线程分别为 sleepThread 和 BusyThread,sleepThread 睡眠 1s,BusyThread 执行死循环。然后分别对着两个线程进行中断操作,可以看出 sleepThread 抛出 InterruptedException 后清除标志位,而 busyThread 就不会清除标志位。
另外,同样可以通过中断的方式实现线程间的简单交互,while (sleepThread.isInterrupted())
表示在 Main 中会持续监测 sleepThread,一旦 sleepThread
的中断标志位清零,即 sleepThread.isInterrupted()
返回为 时才会继续,Main 线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。
一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。
6.2 join
join
方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这就像两个好基友,一个基友先走在前面突然看见另一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友赶上来后,就两人携手并进。其实线程间的这种协作方式也符合现实生活。在软件开发的过程中,从客户那里获取需求后,需要经过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进。
如果一个线程实例 A 执行了 threadB.join()
,其含义是:当前线程 A 会等待 threadB 线程终止后 threadA 才会继续执行。
关于 join
方法一共提供如下这些方法:
public final synchronized void join(long millis);
public final synchronized void join(long millis, int nanos);
public final void join() throws InterruptedException;
Thread 类除了提供 join()
方法外,另外还提供了超时等待的方法,如果线程 threadB 在等待的时间内还没有结束的话,threadA 会在超时之后继续执行。
join方法源码关键是:
while (isAlive()) {
wait(0);
}
可以看出来当前等待对象 threadA 会一直阻塞,直到被等待对象 threadB 结束后即 isAlive()
返回 的时候才会结束 while 循环,当 threadB 退出时会调用 notifyAll()
方法通知所有的等待线程。
下面用一个具体的例子来说说 join 方法的使用:
public class JoinDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 1; i <= 10; i++) {
Thread curThread = new JoinThread(previousThread);
curThread.start();
previousThread = curThread;
}
}
static class JoinThread extends Thread {
private Thread thread;
public JoinThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
System.out.println(thread.getName() + " terminated.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果为:
main terminated.
Thread-0 terminated.
Thread-1 terminated.
Thread-2 terminated.
Thread-3 terminated.
Thread-4 terminated.
Thread-5 terminated.
Thread-6 terminated.
Thread-7 terminated.
Thread-8 terminated.
在上面的例子中一个创建了 10 个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程......
6.3 sleep
public static native void sleep(long millis)
这个方法显然是 Thread 的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。
需要注意的是如果当前线程获得了锁,sleep
方法并不会失去锁。
sleep()
方法经常拿来与 Object.wait()
方法进行比价,这也是面试经常被问的地方。
sleep() VS wait()
两者主要的区别:
sleep()
方法是 Thread 的静态方法,而 wait()
是 Object 实例方法。
wait()
方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而 sleep()
方法没有这个限制可以在任何地方种使用。另外,wait()
方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源,而 sleep()
方法只是会让出CPU并不会释放掉对象锁。
sleep()
方法在休眠时间达到后如果再次获得 CPU 时间片就会继续执行,而 wait()
方法必须等待 Object.notift/Object.notifyAll 通知后,才会离开等待池,并且再次获得 CPU 时间片才会继续执行。
6.4 yield
public static native void yield();
这是一个静态方法,一旦执行,它会使当前线程让出 CPU,但是,需要注意的是,让出的 CPU 并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了 CPU 时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。
什么是线程优先级呢?⬇️
线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。
在 Java 程序中,每个线程都有一个优先级。默认情况下,一个线程会继承构造它的那个线程的优先级。通过一个整型成员变量 Priority 来控制优先级,优先级的范围从 ~ 。在构建线程的时候可以通过 setPriority(int)
方法进行设置,默认优先级为 ,优先级高的线程相较于优先级低的线程优先获得处理器时间片。需要注意的是在不同 JVM 以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。
另外需要注意的是,sleep()
和 yield()
方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()
交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而 yield()
方法只允许与当前线程具有相同优先级的线程能够获得释放出来的 CPU 时间片。
7. 守护线程 Daemon
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT 线程就可以理解守护线程。
与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退出。
当一个 Java 应用,只有守护线程的时候,虚拟机就会自然退出。
下面以一个简单的例子来表述 Daemon 线程的使用。
public class DaemonDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println("i am alive");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally block");
}
}
}
});
daemonThread.setDaemon(true);
daemonThread.start();
//确保main线程结束前能给daemonThread能够分到时间片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果为:
i am alive
finally block
i am alive
上面的例子中,daemodThread 的 run
方法中是一个 while 死循环,会一直打印,但是当 main 线程结束后 daemonThread 就会退出所以不会出现死循环的情况。main 线程先睡眠 保证 daemonThread 能够拥有一次时间片的机会,也就是说可以正常执行一次打印 “i am alive” 操作和一次 finally 块中 “finally block” 操作。紧接着 main 线程结束后,daemonThread 退出,这个时候只打印了 “i am alive” 并没有打印 finally 块中的语句。
因此,这里需要注意的是守护线程在退出的时候并不会执行 finnaly 块中的代码,所以将释放资源等操作不要放在 finnaly 块中执行,这种操作是不安全的。
线程可以通过 setDaemon(true)
的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于 start()
方法,否则会报
Exception in thread "main" java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1365) at learn.DaemonDemo.main(DaemonDemo.java:19)
这样的异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。