基于这样的事实,线程不是一直并行执行的,比如当某个线程需要很长时间的读写操作,这个时候则需要线程调度,即线程间切换;

一、start()

启动一个新线程,在新的线程中运行run方法中的代码,start方法只是让线程进入就绪状态,里面代码不一定立刻执行(比如CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,否则会报IllegalThreadStateException,直接在main方法中不创建线程,而是创建run方法再调用只是在调用普通方法,而不是开启了另一个线程,故启动线程一定要用start方法,再由新的线程去执行run()方法。

image.png

输出结果:
image.png
可以看出start之间仅仅是存在一个新进程,start之后才是真正开启了一个线程。

二、sleep()

  • 调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞)
  • 其它线程可以使用interrupt方法打断正在睡眠的线程(叫醒),这sleep方法会抛出InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行,要等待cpu调度分配时间片给线程时该线程才会执行
  • 建议用TimeUnit的sleep代替Thread的sleep来获得更好可读性

1. 线程进入sleep状态

image.png

image.png
当刚开启线程t1线程时,线程处于Runnable状态,即为可运行但是还没运行状态(就绪状态),当继续执行主线程时候,线程t1开始执行,主线程休眠0.5秒,t1线程休眠2秒,当主线程休眠结束后,线程t1仍在休眠,所以打印出的状态是TIMED_WAITING,注意sleep方法写在哪个线程中,就对哪个线程休眠.

2. 使用interrupt打断sleep

image.png

3. sleep可读性

与Thread.sleep()方法等价,TimeUnit.SECONDS.sleep()方法也是让当前线程睡眠,只不过精度单位是秒级,当然也可以指定分钟级,
即TimeUnit.MINUTES.sleep();
image.png

4. sleep应用

有些应用场景需要While(True)循环,比如保持服务器端程序一直运转,这个时候则需要在线程中加入sleep()方法防止CPU空转

  1. public class Test2 {
  2. public static void main(String[] args) {
  3. Thread task = new Thread(){
  4. @Override
  5. public void run() {
  6. while (true){
  7. try {
  8. Thread.sleep(500);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. }
  14. };
  15. task.start();
  16. }
  17. }

三、yield()


调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程,
具体的实现依赖于操作系统的任务调度器,有可能出现想让出CPU但没有让出去的情况.

yield和sleep的区别: sleep后线程是进入阻塞状态,相当于除了CPU之外仍有条件未满足,比如sleep就是睡眠时间还没达到.而yield后线程是进入就绪状态,即只差cpu资源就进入运行态,所以就绪态是可以分配时间片的,也可以立马被cpu调度,这是两者最大的区别.

四、线程优先级:setPriority()

  • 线程优先级会提示(hint)调度器优先调度该进程,但它仅仅是一个提示,调度器可以忽略它。
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU空闲时,优先级几乎没作用;

如果创建两个线程,线程1之中正常运行,线程2中使用yield方法将线程空出cpu使用权(变成就绪态,但仍然可以被CPU调度,且是否调度由操作系统决定),可以看到CPU整体上是向着线程1偏离,即线程1会更多时间使用CPU。

弹幕里出现了这样的问题:多核cpu为什么不可以并行执行线程?还要这样用CPU来回切换线程执行?
答:CPU核心数一共就那么多,如果CPU只跑一个线程,那么可能确实这个进程的线程能够并行地运行,但主机有那么多的进程,CPU要在进程之间来回调度(注意进程只能并发不能并行,除非多处理器CPU),再加上进程内线程间的运行进度肯定不一样(比如某个进程执行io操作),这样即说明进程和线程的调度是很复杂的,每个时候的情况都不同。所以说线程一般情况下肯定是不能并行执行,仍然需要CPU在进程间来回调度。

image.png

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/23190196/1637832853268-b396bb05-c516-451d-b387-33943e780949.png#clientId=u38af7b40-1c90-4&from=paste&height=275&id=u1b98b478&margin=%5Bobject%20Object%5D&name=image.png&originHeight=376&originWidth=323&originalType=binary&ratio=1&size=20775&status=done&style=none&taskId=ubed20730-5716-4d49-96df-166b36f02cc&width=236.49578857421875)<br />

image.png

给线程设置优先级,setPriority():

此方法给线程设置优先级, 即CPU有更大概率调度高优先级的线程,注意设置优先级要在启动线程(start)之前完成。
image.png

五、join()

下列代码最后输出r=0,分析:

  • 因为主线程和线程t1是并行执行的,t1线程需要1秒之后才能算出r=10;
  • 而主线程一开始就要打印r的结果,所以只能打印出r=0;

    1. public class Test2 {
    2. static int r = 0;
    3. public static void main(String[] args) {
    4. test1();
    5. }
    6. public static void test1(){
    7. Thread t1 = new Thread(()->{
    8. try {
    9. Thread.sleep(1);
    10. r=10;
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. });
    15. t1.start();
    16. System.out.println(r);
    17. }
    18. }

故引入join()方法:等待线程结束,会等待调用join方法的线程对象执行完毕。实际上是与本线程与调用join方法的线程同步,即等待执行完成。
上述例子改成如下,则r=10:

  1. public class Test2 {
  2. static int r = 0;
  3. public static void main(String[] args) {
  4. test1();
  5. }
  6. public static void test1(){
  7. Thread t1 = new Thread(()->{
  8. try {
  9. Thread.sleep(1);
  10. r=10;
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. });
  15. t1.start();
  16. t1.join();//主线程到join这儿即卡住,必须等待t1线程执行完毕后,才可以继续执行;
  17. System.out.println(r);
  18. }
  19. }


join方法-同步应用:

从调用者角度看,如果当前线程需要等待某一线程执行结束,则称为同步,否则称为异步。

  1. public class Test2 {
  2. static int r1=0;
  3. static int r2=0;
  4. public static void main(String[] args) throws InterruptedException {
  5. test1();
  6. }
  7. public static void test1() throws InterruptedException {
  8. Thread t1= new Thread(()->{
  9. try {
  10. Thread.sleep(1);
  11. r1=10;
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. });
  16. Thread t2 = new Thread(()->{
  17. try {
  18. Thread.sleep(2);
  19. r2=10;
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. });
  24. long start = System.currentTimeMillis();
  25. t1.start();
  26. t2.start();
  27. //等待t1和t2执行完毕
  28. t1.join();
  29. t2.join();
  30. long end = System.currentTimeMillis();
  31. System.out.println(end-start);
  32. }
  33. }

这里t1.join()和t2.join()是在主线程调用,所以主线程要等待两个分线程结束后再执行.
还是有老生常谈的疑问,注意多核情况下线程1和线程2是并行执行的(理想状态下,实际上应该也是并行的,因为CPU分给了当前进程,那么核就可以分给线程并行执行,没什么问题),所以时间差是2秒而不是3秒.

注意:如果是单核CPU,总的运行时间差也是2秒,因为当CPU分给t1线程后,t1线程会执行sleep()即进入阻塞态,这时候CPU会立马切换到t2线程执行,当t2调用sleep方法时,cpu此时空转,因为主线程要等待分线程执行,此时CPU没有可执行的线程,当线程t2睡眠结束后,r1和r2再赋值完成,所以时间总共是2秒钟。、

join方法-限时同步:

可以设置join方法等待的时间,如果超过等待的时间线程仍未结束,则不再等待该线程执行完毕,继续运行当前线程。

  1. public class Test2 {
  2. static int r2=0;
  3. public static void main(String[] args) throws InterruptedException {
  4. test1();
  5. }
  6. public static void test1() throws InterruptedException {
  7. Thread t1= new Thread(()->{
  8. try {
  9. Thread.sleep(2000);
  10. r2=10;
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. });
  15. long start = System.currentTimeMillis();
  16. t1.start();
  17. //主线程等待t1线程执行1.5秒,如果1.5秒后线程仍未结束,则不再等待,转而继续执行当前主线程。
  18. t1.join(1500);
  19. long end = System.currentTimeMillis();
  20. System.out.println(end-start);
  21. }
  22. }

如果join设置的等待时间大于线程实际执行时间,不会按照最大时间等待,而是按照实际时间运行,例如:

  1. import javax.swing.table.TableRowSorter;
  2. public class Test2 {
  3. static int r2=0;
  4. public static void main(String[] args) throws InterruptedException {
  5. test1();
  6. }
  7. public static void test1() throws InterruptedException {
  8. Thread t1= new Thread(()->{
  9. try {
  10. Thread.sleep(2000);
  11. r2=10;
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. });
  16. long start = System.currentTimeMillis();
  17. t1.start();
  18. //t1线程2秒执行完毕,主线程不会等待3秒,而是当t1线程执行结束后,主线程立马不再等待,开始执行
  19. t1.join(3000);
  20. long end = System.currentTimeMillis();
  21. System.out.println(end-start);
  22. }
  23. }

六、interrupt()

打断标记:
每个线程拥有一个打断标记,如果标记为真代表该线程在被打断,但什么时候打断由该线程自己控制,如果标记为假,表示该线程处于未被打断状态。注意以下两种打断情况的区别,陷入【WAITING】和【TIMED-WAITING】状态的线程被打断会抛出打断异常,直接进入catch块处理后事,而正常运行状态下的线程会将打断标志置为真,至于怎样中断由线程内部自己实现。

打断阻塞:
这里特指打断进入Waiting/Time_waiting等状态的阻塞线程,如打断执行sleep、wait、join的线程。
当打断此种线程时,清空打断标记,将打断标记置为FALSE值,同时抛出InterruptedException。

打断正常:
打断正常运行的线程,不会清空打断标记,打断标记实际是一个打断信号,将打断标记置为TRUE,告诉被打断线程,你要被打断了,但实际不会执行打断操作。
所以当打断正常执行的线程时,并不会立刻终止被打断线程的执行,而是交由被打断线程去判断应该什么时候终止自己,线程仍然处于正常执行状态。

例如这种情况,打断后程序仍会继续执行,并不会打断线程t1的执行,只是将线程的打断标志置了TRUE;

  1. import javax.swing.table.TableRowSorter;
  2. import java.security.KeyStore;
  3. public class Test2{
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread t1 = new Thread(()->{
  6. while(true){
  7. System.out.println("1111");
  8. }
  9. },"t1");
  10. t1.start();
  11. //注意这里主线程要睡眠,一会儿否则进程t还没开始,主线程就要去打断线程,这并不会执行任何操作
  12. Thread.sleep(1000);
  13. t1.interrupt();
  14. }
  15. }

这种情况,在t1线程内部通过判断打断标记的真假,自己决定什么时候中断线程;

  1. public class Test2{
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 = new Thread(()->{
  4. while(true){
  5. boolean interrupted = Thread.currentThread().isInterrupted();
  6. if (interrupted){
  7. System.out.println("被打断了,退出循环");
  8. //跳出循环,即t1线程运行结束。
  9. break;
  10. }
  11. }
  12. },"t1");
  13. t1.start();
  14. //注意这里主线程要睡眠,一会儿否则进程t还没开始,主线程就要去打断线程,这并不会执行任何操作
  15. Thread.sleep(1000);
  16. t1.interrupt();
  17. }
  18. }

设计模式-两阶段终止:

两阶段终止模式(Two Phase Termination):
在一个线程T1中如何优雅终止线程T2?这里的【优雅】指的是给T2一个料理后事的机会。

1.错误思路

  • 使用线程对象的stop()方法停止线程

    1. stop方法会真正直接杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁;
  • 使用System.exit(int)方法停止线程

    1. 目的仅仅是停止一个线程,但这种做法会让整个进程都停止运行;(**自爆卡车,全军覆没**)

2.两阶段终止模式

首先是一个while(true)循环,根据打断标记判断是否被打断,如果打断标记是TRUE,则开始料理后事,释放一些相关共享资源等,然后结束循环,线程结束。如果打断标记是FALSE,表示目前还没有被打断,则进入睡眠状态2秒,若在sleep时无异常,那么睡眠后继续执行监控,在执行监控记录过程中如果被打断,会将打断标记记为TRUE,在下次循环的时候跳出循环,若在sleep时有异常,则抛出InterruptedException,因为InterruptedException会将打断标记置为FALSE值,这里需要重新设置打断标记为TRUE,以在下次循环的时候能够跳出循环。
image.png

两阶段终止代码实现:

注意细节,如果在情况1被打断,此时线程在sleep,即阻塞状态,那么打断后会抛出异常进入catch块,需要重新设置打断标记;
如果在情况2以及其它点被打断,那么会默认将打断标记置为TRUE,下次循环的时候则会跳出循环。

  1. public class Test3 {
  2. public static void main(String[] args) throws InterruptedException {
  3. TwoPhaseTermination tpt = new TwoPhaseTermination();
  4. //创建线程,监控该线程状态,
  5. tpt.start();
  6. Thread.sleep(3500);
  7. //停止线程,但优雅地停止,选择interrupt线程;
  8. tpt.stop();
  9. }
  10. }
  11. //监控类
  12. class TwoPhaseTermination{
  13. private Thread monitor;
  14. //启动监控线程
  15. public void start(){
  16. //java基础,调用start方法,这里的monitor即指当前对象的monitor属性,与this.monitor等价
  17. monitor = new Thread(()->{
  18. while(true){
  19. Thread current = Thread.currentThread();
  20. if(current.isInterrupted()){
  21. System.out.println("料理后事");
  22. break;
  23. }
  24. try {
  25. Thread.sleep(1000);//打断情况1
  26. System.out.println("执行监控记录");//打断情况2
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. /*
  30. 只要是在sleep阶段被打断,即情况1时候被打断,会进入catch块
  31. 所以要重新设置打断标记为真
  32. */
  33. //重新设置打断标记为真
  34. current.interrupt();
  35. }
  36. }
  37. });
  38. monitor.start();
  39. }
  40. // 停止监控线程
  41. public void stop(){
  42. monitor.interrupt();
  43. }
  44. }

isInterrupted()和interrupted()方法区别:

isInterrupted:判断当前线程是否会被打断(实际上就是判断打断标记是TRUE还是FALSE),并不会清除打断标记。
interrupted:不仅判断当前线程是否会被打断,但会清除打断标记。

打断park线程:

LockSupport.park()方法:当前线程调用park方法会被挂起,可以被interrupt方法打断,打断后线程可以正常执行,相当于是正常运行的线程被interrupt,只是将打断标记置为TRUE。
当前线程如果打断标记为假,则park方法生效,如果打断标记为真再调用park方法,则线程会一直执行无法停下。
注意挂起状态可认为是暂时被淘汰出内存的状态,和阻塞状态要区分开。

七、主线程与守护线程

默认情况下,JAVA进程需要等待所有线程都运行结束,才会结束(这句话约等于废话,进程是指令序列,而线程是分指令序列,线程不执行完毕进程一定不可能结束),有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

示例,主线程可以先结束,其它副线程还在继续运行,这样是可行的:

  1. public class Test4 {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 =new Thread(()->{
  4. while(true){
  5. if(Thread.currentThread().isInterrupted()){
  6. break;
  7. }
  8. }
  9. },"t1");
  10. t1.start();
  11. Thread.sleep(1000);
  12. System.out.println("结束");
  13. }
  14. }

可以看到主线程已经执行完毕,t1线程仍在空转,被打断才会执行完毕。

可以将t1线程通过setDaemon()方法设置为守护线程,这样当主线程运行结束后,线程t1也会被强制结束。
代码如下:

  1. public class Test4 {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 =new Thread(()->{
  4. while(true){
  5. if(Thread.currentThread().isInterrupted()){
  6. break;
  7. }
  8. }
  9. },"t1");
  10. t1.setDaemon(true);
  11. t1.start();
  12. Thread.sleep(1000);
  13. System.out.println("结束");
  14. }
  15. }

setDaemon方法一定是在线程启动之前设置,即start方法之前。程序正常结束,可以看到t1线程确实被强制结束。

注意:

  • GC线程就是一种守护线程,当其它功能线程已经执行完毕后,GC线程无用处则会自动结束;
  • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求;