线程
线程的常见属性
线程属性可以通过下面的表格查看。
•ID 是线程的唯⼀标识,不同线程不会重复
• 名称是各种调试⼯具⽤到(如jconsoloe)
• 状态表示线程当前所处的⼀个情况,下⾯我们会进⼀步说明
• 优先级高的线程理论上来说更容易被调度到
• 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运⾏。
• 是否存活,即简单的理解,为 run 方法是否运⾏结束
• 线程的中断问题,下⾯我们进⼀步说明
线程的状态
线程状态分为New、Terminated、Runnable、Waiting、Timed_Waiting、Blocked
观测线程状态可以通过jconsole观察。
- New 该状态下存在Thread对象,还没有调用start,系统内部PCB还未创建
- Terminated 该状态下表示线程已终止,内部创建的PCB已经销毁,Thread对象还在
- Runnable 该状态下表示线程正在CPU上执行,随时可以被调度。
- Waiting 死等状态进入的阻塞
- Timed_Waiting 带有规定时间的等待
- Blocked 进行锁竞争后出现的阻塞状态
线程状态之间的转换图(省略版)
线程是否存活
在线程生命周期中,根据观察线程中的PCB是否存在。
Thread t = new Thread(()->{
});
在这段代码中,线程t只是初始化了一个Thread对象,但是其中的PCB只有在执行了t.start()以后才会被创建出来。
public static void main(String[] args) {
Thread t = new Thread(()->{
});
t.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
在上面的代码中,线程t在启动以后创建了PCB,但是因为t中代码块没有任何代码,所以线程t很快就运行结束了,内核中的线程和PCB马上就被销毁了。而在main线程中,通过sleep停止了2s,因此线程t的PCB被销毁,而t这个对象并没有被gc回收,t仍然是存在的。
在下面的代码中,我加上了t=null,通过这个方法以求解决t对象能够被gc回收。但显然是不行的,通过t=null,可能会导致t线程还没结束,t对象就为空了。
public static void main(String[] args) {
Thread t = new Thread(()->{
});
t.start();
t = null;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
因此,Thread给了我们一种属性:isAlive,是否存活。通过这个属性我们可以查看到线程是否存在,并对线程t进行操作。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("hello ");
}
System.out.println("线程t是否存活:"+Thread.currentThread().isAlive());
});
t.start();
Thread.sleep(3000);
System.out.println("线程t是否存活:"+ t.isAlive());
}
结果如下图
前台线程和后台线程的关系与区别
线程是并发执行的。当然,当线程多了以后,为了能够不让某些线程影响整个进程的结束,更好的利用系统资源。于是为线程设计了前台和后台两个线程方式。在前台线程中,如果本线程运行没有结束,则此时Java进程也无法结束。在后台线程中,即使该线程还处于执行阶段,当前台线程都结束以后,意味着整个Java进程即将结束。那么这时候后台线程就不得不停止执行了。
简单来说,前台线程决定了Java进程的时间,后台线程无法控制整个进程的时间,只能被迫跟着前台线程的结束而结束。
前台线程也可以是多个的,只有最后一个前台线程结束,整个Java进程才结束。
设置线程的前后台
在Java中,main线程,以及默认情况下的线程都属于前台线程。而改变线程前后台的方式可以通过修改线程前后台属性: t.setDaemon(true) 将对应线程转变为后台线程。
接下来使用一个例子进行简单演示
public class Demo1 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
for (int i = 0;i<5;i++) {
System.out.println("hello thread");
}
}
});
t.setDaemon(true);
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("hello main");
}
}
}
在上面的代码中,我们可以看到,理论上来说线程t应当是进入死循环不断打印"hello thread"的,但是我在线程启动之前,将t线程设置为后台线程。因此,在main线程运行结束以后,垃圾回收机制gc将main线程回收,前台线程结束,后台线程t自然也只能被迫结束。
线程核心操作
启动 t.start()
通过多次的练习,已经明白t.start()执行后创建线程PCB,真正创建线程并开始执行。不再过多赘述
终止 interrupt()
在Java中,终止线程不是简单的停止线程的执行。而是对线程进行提醒,提醒线程是否停止,而真正做决定的还是该线程本身。
1)通过变量控制终止线程
private static boolean isRunning=true;
public static void main(String[] args) throws InterruptedException {
// boolean isRunning = true;//变量捕获
Thread t2 = new Thread(()->{
while (isRunning) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t2线程被结束了");
});
t2.start();
Thread.sleep(4000);
System.out.println("控制t2线程结束");
isRunning = false;
}
结果如下图
2)使用线程的interrupt和isInterrupted方法
通过下面的代码进行解释,在这个代码块中,线程t3通过isInterrupted判断是否结束这个循环,而在main线程中,通过t.interrupt方法,将这个值修改为false,抛出RuntimeException异常,结束了当前的线程。
public static void main(String[] args) throws InterruptedException {
Thread t3 = new Thread(()->{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException();//抛出异常并结束
}
}
System.out.println("t2线程被结束了");
});
t3.start();
Thread.sleep(3000);
t3.interrupt();
}
3)提醒线程而不终止
在下面的代码中,修改了boolean之后,线程t3并没有停止线程,而是打印出异常信息之后继续执行。
在这里出现了一个问题,为什么通过interrupt方法之后修改了boolean值,但是在线程抛出异常之后仍然在继续打印呢?
首先,interrupt方法修改了boolean值的标志位修改为true,在slee过程中,通过interrupt方法强制唤醒线程,在强制唤醒后清除了刚刚的标志位,重新修正回flase。
public static void main(String[] args) throws InterruptedException {
Thread t3 = new Thread(()->{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace(); //打印异常信息而不结束
//throw new RuntimeException();//抛出异常并结束
}
}
System.out.println("t2线程被结束了");
});
t3.start();
Thread.sleep(3000);
t3.interrupt();
}
等待join()
因为线程是抢占式执行的,其调度顺序在系统中是无序的。而这种无序的执行方式不在程序猿的掌控之中,因此我们希望能够通过一些方式控制这些线程。join(等待)就是其中一种方式。
- 基本用法
在下面的代码块中,分为main线程和t两个线程。我们可以注意到,与之前的写法其实是大差不差的,唯一区别的是在try-catch包裹下出现的t.join()方法。
在main线程中使用t.join的作用让main线程进入阻塞等待状态,t线程执行完之后main线程才会接着往下执行。
通过这样的方式在一定程度上解决了线程的控制问题。
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread end");
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 5; i++) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main end");
}
因此我们可以看到结果如下图所示:
- 多个线程的join
在多个线程等待代码了解之前,我们必须了解的是在main线程中调用t1.join,是main线程等待t1,而与其他线程无关,main线程是不会等待其他线程的。当多线程的情况下也是这般理解的。
在下面的代码中,存在t1和t2两个线程,在main线程中,同时调用t1.join()和t2.join() 这意味着main线程只有等t1线程和t2线程全部执行完毕以后才会往下执行。t1和t2线程各自调度执行,互不影响。 所需的时间是t1线程和t2线程运行时间较长的。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 end");
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 end");
});
t1.start();
t2.start();
t1.join();
t2.join();
for (int i = 0; i < 10; i++) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main end");
}
- 有限时间内等待
在上面两个情况下,对于t.join()的方法,只有等待线程t运行结束以后才能进行下一步,属于死等状态。那么如果出现意料之外的情况,线程t无法结束,那么整个程序都会因此卡住,无法执行后续的逻辑,极大降低了系统的容错。
因此Java提供了join带参数的两种方法。通过这两种方法,当线程超过规定时间而不结束,则主线程不再等待,继续执行下面的逻辑。
接下来简单写个demo
在这个demo中,存在三个线程分别为t1、t2、main。在代码中我通过t1.join()让两个线程都进行阻塞状态等待线程t1结束。而对于t2,不相同的是我只让main线程等待2s,超过两秒之后,main线程不再是阻塞状态,而是执行下面的逻辑。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello t1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 end");
});
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 5; i++) {
System.out.println("hello t2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 end");
});
t1.start();
t2.start();
t1.join();
t2.join(2000);//等待2s后main线程自己走了
for (int i = 0; i < 5; i++) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main end");
}
因此代码结果如下:
总结
多线程有许多常见属性以及各类用法,都是需要熟练掌握的。
源码:多线程相关代码