【JavaEE】浅谈线程(二)

线程

线程的常见属性

线程属性可以通过下面的表格查看。

•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是否存在。

java 复制代码
Thread t = new Thread(()->{
	
});

在这段代码中,线程t只是初始化了一个Thread对象,但是其中的PCB只有在执行了t.start()以后才会被创建出来。

java 复制代码
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对象就为空了。

java 复制代码
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进行操作。

java 复制代码
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) 将对应线程转变为后台线程。

接下来使用一个例子进行简单演示
java 复制代码
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)通过变量控制终止线程

java 复制代码
	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异常,结束了当前的线程。

java 复制代码
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。

java 复制代码
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线程才会接着往下执行。
    通过这样的方式在一定程度上解决了线程的控制问题。
java 复制代码
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线程运行时间较长的。
java 复制代码
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线程不再是阻塞状态,而是执行下面的逻辑。
java 复制代码
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");
    }

因此代码结果如下:

总结

多线程有许多常见属性以及各类用法,都是需要熟练掌握的。

源码:多线程相关代码

相关推荐
J不A秃V头A15 分钟前
IntelliJ IDEA中设置激活的profile
java·intellij-idea
DARLING Zero two♡17 分钟前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
小池先生28 分钟前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
CodeClimb32 分钟前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
odng35 分钟前
IDEA自己常用的几个快捷方式(自己的习惯)
java·ide·intellij-idea
CT随43 分钟前
Redis内存碎片详解
java·开发语言
brrdg_sefg1 小时前
gitlab代码推送
java
hanbarger1 小时前
mybatis框架——缓存,分页
java·spring·mybatis
cdut_suye1 小时前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
苹果醋32 小时前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx