线程学习(2)线程创建,等待,安全,synchronized(一)+https://developer.aliyun.com/article/1413575
5.是否是后台线程
线程可以分为两类,前台线程和后台线程,默认情况下是前台线程。后台线程又叫做"守护线程",就像一场表演的后台工作人员一样,对于后台线程来说,后台线程不结束,不影响整个进程的结束(表演完了,可后台人员还需要处理后事,他们的工作还没结束),而对于前台线程来说,一个Java程序中,如果还有前台进程没有结束,则整个进程是一定不会结束的
获取方法
isDaemon
// 源码规定 默认是前台线程 /* Whether or not the thread is a daemon thread. */ private boolean daemon = false; Thread t = new Thread("我是线程"); boolean isDaemon = t.isDaemon(); System.out.println(isDaemon);// 输出false
代码验证
public static void main(String[] args) { Thread t = new Thread(() -> { while (true) { System.out.println("hello thread"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t.start();// 默认是前台线程 持续打印hello thread }
对于这个代码来说,主线程中没有要执行的语句,也就是说他的主线程是在一瞬间就执行完了,但是由于t是前台线程,前台线程不结束整个进程就不会结束,如果将t设置为后台线程呢
public static void main(String[] args) { Thread t = new Thread(() -> { while (true) { System.out.println("hello thread"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); // 在线程开启前将其设置为后台线程 t.setDaemon(true); t.start(); }
可以看到什么也没有打印。因为主线程是前台线程,飞快执行完毕之后没有其他的前台线程,整个进程终止,也就是说t线程没来得及执行,整个进程就结束了。也就是说只要一个进程中的所有前台线程结束,就代表整个进程的结束
验证,先让主线程休眠3s,3s之后主线程会立即结束,尽管t线程内部还有语句没有执行,由于前台线程的结束,导致整个进程结束
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true) { try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("hello thread"); } }); // 在线程开启前将其设置为后台线程 t.setDaemon(true); t.start(); System.out.println("主线程开启"); Thread.sleep(3000); }
6.是否存活
判断内核线程是否还存活
在Java中我们通过Thread类来创建出一个线程,但实际上Thread类的生命周期要比内核中的线程要长一些,也就是说线程已经不存在了,但是你创建的Thread类仍然存在,使用isAlive判定内核线程是否已经结束
isAlive()
public static void main(String[] args) throws InterruptedException { // 创建一个线程 Thread t = new Thread(() -> { System.out.println("线程开始"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("线程结束"); }); // 开启线程 t.start(); System.out.println(t.isAlive());// 输出true Thread.sleep(3000); System.out.println(t.isAlive());// 输出false }
线程t内部的方法我们称之为回调方法,当回调方法执行完毕之后,就代表t这个线程的终止,但是Thread类对象的生命周期并未结束
System.out.println(t.isAlive());// 输出false // 开启线程 t.start();
如果在线程开启之前打印,输出false,因为此时t线程还没有被创建
三.线程的中断
终止/打断 interrupt
在Java中要想销毁/中断一个线程的方法是比较唯一的,
就是想办法让run方法尽快执行完毕
那么如何实现呢?这里提供两种方法
1.手动设置标志位,来作为run方法结束的条件
很多线程之所以会持续很久,是因为run方法内部存在循环,结束run方法就是终止循环
// 将标志位设置为类变量 private static boolean isQuit = false; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (!isQuit) { System.out.println("Thread is working"); } }); t.start(); // 五秒后改变标志位 Thread.sleep(2000); isQuit = true; }
通过设置标志位,并在主线程中修改标志位,这样就实现了在5秒之后中断此线程的效果
注意:
1.isQuit不能设置为main方法中的局部变量,因为在lambda表达式中使用的变量必须是被final修饰的常量,如果设置为局部变量,就无法再次更改isQuit,导致无法结束循环。
2.将isQuit设置为"类变量",lambda表达式此时访问这个成员就不再是变量捕获了,而是内部类访问外部类这个语法了。此时就没有final的限制
上述方法虽然能够结束run方法,但是过于繁琐,且不优雅,需要人为的手动设置标志位,同时,如果在主线程中我们改变了标志位的值,但是此时线程却在sleep,那就只能等到线程再次苏醒才能终止该线程,所以说通过设置标志位的方法来终止线程还有反应不及时的问题
2.使用Thread内部自带的"标志位"
其实在Thread类中,有自带的标志位isInterrupted,默认是false
public static void main(String[] args) throws InterruptedException { Thread t= new Thread(() -> { // 先获取当前Thread的实例 在判断其自带的标志位isInterrupted while (!Thread.currentThread().isInterrupted()) { while (true) { System.out.println("Thread is working"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start(); Thread.sleep(3000); // 此方法就是将自带的标志位isInterrupted设置为true t.interrupt(); }
通过t.interrupt()方法将标志位设置为true来终止线程,这种方法的一个优点是即使线程内部处于"阻塞"状态(sleep),也能够强制将其唤醒,终止run方法,反应更加及时
总结:两种中断线程的方法逻辑都是一样的,即设置合适的标志位,并修改该标志位来终止run方法,从而终止整个线程,但是更加推荐第二种方法
但是上述代码的运行结果是什么呢?请看
异常被抛出且被捕获,但是t线程仍在工作,并没有发生中断,这是为什么呢?通过interrupt方法唤醒线程之后,此时sleep方法会抛出异常,同时自动清除刚才设置的标志位,相当于白白设置标志位了,为什么要这么做呢?是为了让我们有更多的操作空间,在捕获到异常之后,我们可以自由采用以下三种处理方式
try { Thread.sleep(1000); } catch (InterruptedException e) { // 1.方式1 不管不顾 让t线程继续运行 e.printStackTrace(); // 2.方式2 使用break直接中断进程 // 3.方式3 捕获到线程之后处理其他工作的代码 // 此处就存放需要解决的其他工作的代码 }
四.线程的等待
一个线程等待另一个线程执行结束,再继续执行。线程等待的本质是控制线程结束的顺序
在Java中使用join来实现线程等待效果
主线程中使用join,就是主线程等待另一个线程结束再继续执行主线程的其余代码
public static void main(String[] args) throws InterruptedException { Thread t= new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程工作中!"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t.start(); System.out.println("线程开启"); t.join();// 让主线程等待t线程 System.out.println("线程结束"); }
t.join()的工作过程
- 如果t线程还没有结束,就让主线程等待t线程执行结束,再去执行主线程中剩余的代码,此时主线程就是一个"阻塞"状态
- 如果t线程已经结束了,直接返回,不存在"阻塞状态"
在哪个线程中调用join方法就是让哪个线程等待另一个线程
说明:join方法默认是"死等",即如果被等待的线程没结束,就不会执行其余代码,但这种方式存在一个问题,如果被等待的线程是死循环,那其余代码就永远无法执行,在实际的开发中,我么更推荐"有时间"的等待
此处表示主线程只等待1s,1s之后就会去执行主线程中剩余代码
补充:关于调度开销
当我们使用Thread.sleep方法时,我们通过设置一定的时间让线程处于阻塞状态,结束之后再恢复为就绪状态,由阻塞到就绪其时间一定等于sleep的时间么
long beg = System.currentTimeMillis(); Thread.sleep(1000);// 休眠1s long end = System.currentTimeMillis(); System.out.println("时间:" + (end - beg) + " ms");// 输出1003
可见由阻塞到就绪这部分的时间并不等于sleep的时间,原因在于休眠结束之后,线程并不是立马就变为就绪状态,而是需要通过调度器进行调度,而这种调度是需要时间的,这部分由于调度器调度所产生的时间就叫做调度开销
线程学习(2)线程创建,等待,安全,synchronized(三)+https://developer.aliyun.com/article/1413580