JavaEE初阶第四期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(二)
专栏:JavaEE初阶起飞计划
个人主页:手握风云
目录
一、Thread类及常用方法
2.1. Thread的常见构造方法
2.2. Thread的常见属性
2.3. 启动一个线程
2.4. 中断一个线程
2.5. 等待一个线程
2.6. 休眠当前线程
一、Thread类及常用方法
2.1. Thread的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象并命名 |
name参数用来给线程取名字。名字叫啥,不影响线程的执行,不同的名字更利于调试。
public class Demo1 {public static void main(String[] args) {// 创建线程对象Thread t1 = new Thread() {@Overridepublic void run() {while (true) {System.out.println("hello t1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t1.start();// 使用Runnable对象创建线程Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello t2");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t2.start();// 创建线程对象并使用name参数命名Thread t3 = new Thread("这是我的t3线程") {@Overridepublic void run() {while (true) {System.out.println("hello t3");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};t3.start();// 使用Runnable对象创建线程并使用name参数命名Thread t4 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello t4");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}, "这是我的t4线程");t4.start();}
}
如上图所示,我们会发现这里没有main主线程。这是因为start执行完毕后,main方法就执行结束了,对应的主线程也结束了,随之自动销毁。
对于ThreadGroup线程组,把若干个线程放到同一个组里面,这样的话就可以给每个组里的线程设置相同的属性。但这个并不常用,更多的是用到线程池。
2.2. Thread的常见属性
属性 | 获取方法 |
ID | getID() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否为后台进程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted |
ID是线程的唯一标识,不同的线程不会重复。获取名称常用于调试和日志记录,帮助开发者了解当前正在执行的线程名称。线程状态可以分为NEW(新建)、RUNNABLE(就绪/运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)和TERMINATED(消亡)。线程的优先级是一个整数,表示线程在运行时的重要程度。Java中线程的优先级范围从1到10,其中1是最低优先级,10是最高优先级。
isDaemon()方法用于判断线程是否为后台线程。后台进程,当线程没运行完,进程可以就结束,无论有多少个后台进程,都无法阻止进程结束。对应的还有前台进程,当线程没运行完,进程就不会结束,当有多个线程时,就得所有线程结束才能结束进程。main线程和自己创建的线程都是前台进程,剩下的都是后台进程。如果一个线程做得任务很重要,这个任务必须要做完,应该设置为前台线程;相反如果任务无关紧要,就可以设置为后台进程。isAlive()方法用于判断线程是否在运行。当一个线程启动后,它会一直运行,直到run()方法执行完毕或者线程被中断。isInterrupted()方法用于判断线程是否被中断。当一个线程被中断时,它的中断状态会被设置为true,可以通过isInterrupted()方法来检查这个状态。
public class Demo2 {public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"This is mine");t.start();System.out.println(t.getId());System.out.println(t.getName());System.out.println(t.getState());System.out.println(t.getPriority());System.out.println(t.isDaemon());System.out.println(t.isAlive());System.out.println(t.isInterrupted());}
}
2.3. 启动一个线程
调用start()方法会真正调用系统中的API,线程跑起来之后就会自动执行到run()。调用start的方法本身就很快,一旦执行start,代码就会自动往下执行,不会产生任何的阻塞等待。
public class Demo2 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello thread");});t.start();System.out.println("hello main");}
}
执行结果如上图所示,大部分情况下都是hello main在前,也可能会有例外。这是因为在start之后,main线程和t线程两个执行流,是一个并发执行关系。操作系统对于线程的调度是随机的,如果执行完start恰好被调度出CPU,此时CPU下次执行main还是t就不确定了。
一个线程对象只能被启动一次。线程执行了start之后,就是就绪状态或者阻塞状态,对于这两种状态,不能再启动了。
public class Demo2 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello thread");});t.start();System.out.println("hello main");t.start();}
}
总结:使用Interrupt方法时,t线程没有使用sleep等阻塞操作,t的isInterrupted()方法返回true,通过循环条件结束t线程;t线程使用了sleep等阻塞操作,t的isInterrupted()方法也会返回true,但sleep如果被提前唤醒,抛出InterruptedException异常。
2.4. 中断一个线程
这里不要跟操作系统里的中断搞混,线程的中断准确来说应该叫“打断”或者“终止”。正常情况下,一个线程需要把入口方法执行完,才能够使线程结束。但有时候,我们希望线程能够提前终止,尤其是在线程休眠的时候。这时就需要通过打断线程的操作,也需要线程本身代码做出配合。
- 通过变量
public class Demo3 {public static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (flag) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println("hello main");Thread.sleep(5000);flag = false;System.out.println("让t线程终止");}
}
- 通过内置的标志位isInterrupted()
Thread对象中,包含了一个布尔变量,如果为false,说明没有人去尝试终止这个线程;如果为true,说明有人尝试终止。
public class Demo4 {public static void main(String[] args) {Thread t = new Thread(() -> {while (!t.isInterrupted()) {}});}
}
此时这个代码是有错的,原因如下图所示:变量t未初始化,因为此处针对lambda表达式的定义是在new Thread()之前。
我们可以使用Thread类内部的静态方法currentThread(),用于获取对当前正在执行的线程对象的引用。
public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {// 当线程t没有被中断时,打印"hello thread"while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");}});t.start();// 主线程休眠4秒Thread.sleep(4000);// 中断线程t// 把标志位false改为truet.interrupt();}
}
这里可能打印的结果有点多,我们也可以让t线程休眠1秒。
public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {// 当线程t没有被中断时,打印"hello thread"while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();// 主线程休眠4秒Thread.sleep(5000);// 中断线程t// 把标志位false改为truet.interrupt();}
}
我们发现当线程抛出异常时,但线程并没有终止结束。线程里面有个奇怪的设定:如果线程t正在休眠,此时在main中调用interrupt()方法,就能把sleep提前唤醒。InterruptedException支持sleep提前唤醒,通过一场区分sleep是睡足了还是提前醒了。sleep提前唤醒,触发异常之后,然后sleep就会把isInterrupted标志位给重置为false。
之所以会有这样奇怪的设定,是为了给程序员留下更多的操作空间。提前唤醒,可能还存在一些“还未完成的工作”,让程序员自行决定线程t是继续执行、立即结束还是稍等一会结束。上面的代码相当于完全忽视了终止请求。
public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {// 当线程t没有被中断时,打印"hello thread"while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {// 打印异常调用栈//// e.printStackTrace();// 触发异常就会结束循环,终止线程break;}}});t.start();// 主线程休眠4秒Thread.sleep(5000);// 中断线程t// 把标志位false改为truet.interrupt();}
}
上述几种方式本质上都是线程t自己决定自己是否要终止,相当于main只是给t提供了一个“建议”而不是强制结束。
2.5. 等待一个线程
有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四转账成功,才决定是否存钱。线程等待,约定了两个线程结束的先后顺序。
哪个线程中调用的join,该线程就是等待的一方;join前面的引用,该线程就是被等的一方。
public class Demo5 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}});t.start();// 主线程中,可以对线程t进行等待System.out.println("主线程等待之前");// 由于主线程也可能触发阻塞,也可能抛出InterruptedException异常t.join();System.out.println("主线程等待之后");}
}
join这个等待是个死等,只要被等的线程没有结束,join都会始终阻塞。如果上面是个死循环,那么主线程就会永远等待t线程。
上面是t线程等待主线程,也可以让主线程等待t线程。
public class Demo6 {public static void main(String[] args) throws InterruptedException {// 获取主线程Thread mainThread = Thread.currentThread();Thread t = new Thread(() -> {try {System.out.println("t线程等待之前");mainThread.join();System.out.println("t线程等待之后");} catch (InterruptedException e) {e.printStackTrace();}});t.start();// 主线程等待t线程for (int i = 0; i < 10; i++) {System.out.println("hello main");Thread.sleep(1000);}}
}
当然也可以让两个线程同时等待对方,但是这样写代码是意义的,会造成两个线程都无法结束,都无法完成对方的等待操作。
虽然join会触发阻塞,但也不一定会触发,比如在主线程等待t线程之前,t线程已经结束了,此时join就不会阻塞。join默认会是死等,但死等这种情况不太好,如果程序出现了意外,永远等不到结果。所以join方法还有其它的重载版本,可以指定一个最大等待时间(超时时间)。
方法 | 说明 |
join() | 等待线程结束 |
join(long millis) | 等待线程结束,最多等millis毫秒 |
join(long millis,int nanos) | 更高精度 |
public class Demo7 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}});t.start();System.out.println("等待之前");// 等3秒之后就不等了t.join(3000);System.out.println("等待之后");}
}
2.6. 休眠当前线程
Thread.sleep本质就是让线程的状态变成“阻塞”状态,此线程就不参与CPU调度了。休眠时间到了,线程的状态恢复成就绪状态(不是立即执行),才能参与CPU调度。