(一).线程的概念

1.概念

在Java中,对线程进程了统一的封装,封装成了Thread类

2.run方法

在Thread中,有一个run方法,这个方法是一个抽象方法,我们需要重写我们的run方法来进行执行

run方法是线程的入口方法,一旦新的线程启动,就要执行这里的代码,run方法不需要我们手动调用,新的线程创建好了之后,自动的取执行

注意:run方法相当于 “回调函数”

3.start方法

start方法,表示创建了一个新的线程,多了一个执行流,就意味着在CPU中多了一个人干活,所以这个代码就可以“一心两用”

start方法才是真正的在系统中创建线程,即JVM调用操作系统的API完成线程创建操作

注意:每个Thread对象,只能start一次,即每次想要创建一个新的线程,都得需要创建一个新的Thread对象

(二).线程的创建

1.方法1:通过继承Thread 类,重写run方法

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("Hello Thread");
    }
}

当我将两个线程都写成死循环后

程序的运行结果为

可以发现,程序中的两个线程在交替运行

当我使用一个 “休眠”方法 sleep,程序再运行

sleep方法,是一个静态方法,表示 “休眠,让当前的线程暂时放弃CPU,等指定的时间过了之后再执行”,直接使用Thread类进行调用即可,里面的参数单位为“毫秒”

可以看到,两个线程在交替执行,这说明线程的调度是随机的,抢占式执行

2.方法2:通过实现Runnable接口,重写run方法

Runnable 表示一个 “可执行任务”,通过这个接口来调用run方法,这样写可以更好的 “解耦合”,将来修改代码的时候更方便

class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("Hello Runnable");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

依旧是两个线程

执行的结果还是两者交替执行

当我不使用 thread.start()方法,而使用thread.run()方法的时候,程序的运行结果为

可以看出,全是 “Hello Runnable”,这是因为我调用的是run方法,没有创建线程,所以只有main这一个线程,所以只会执行 run方法里面的内容

注意:main方法对应的线程,即一个进程中至少要包含的那个线程,为主线程

3.优化方法1,使用匿名内部类

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
        
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }

创建了一个Thread子类,子类的名字是匿名的。{ } 里面就可以编写子类的代码,即子类中要包含的哪些属性,方法,以及要重写的父类的方法。创建了这个匿名内部类的实例,并将这个实例的引用赋值给了thread

当程序运行起来的时候,也达到了我们想要的效果

注意:通过匿名内部类来写,一般用于这个代码是 “一次性”的时候

4.优化方法2,使用匿名内部类

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello runnable");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread=new Thread(runnable);
        thread.start();

        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }

和方法2一样,这样做的目的是为了“解耦合”。如果直接继承Thread类,执行的任务本身和 Thread(线程)这个概念是耦合在一起的

我们为了降低耦合是为了后续改代码方便

我们只需要记住:使用Runnable,任务和线程概念是分离的

5.优化方法3和方法4,使用lambda表达式

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
           while (true){
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

        thread.start();

        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }

对于lambda表达式,本质上就是一个 “匿名函数”,最主要的用途就是作为 “回调函数”

在java 中,方法必须要依托于 类 来存在,就像lambda表达式,就类似于 “函数式接口”,创建了一个匿名的函数式接口的子类,并且创建出对应的实例,并且重写了里面的方法

(三).查看线程的工具:jconsole.exe

Jconsole.exe可以看到当前的线程

1.找到JDK文件的位置

2.根据上图中的路径,在此电脑中找到jdk的存放位置

3.点击bin文件夹,找到jconsole.exe可执行文件

4.打开jconsole.exe执行程序

5.选择 “不安全的连接”

6.选择线程

7.Thread-0 和 main  就是我们创建的线程

8.查看线程所在的行数

线程的调用栈,获取线程状态的时刻,线程里的代码执行到哪里了

(四).Thread类的其他属性和方法

1.Thread类的构造方法

方法 说明
Thread() 创建线程对象,必须重写Thread类里面的run()方法
Thread(Runnable target) 使用Runnable对象创建线程对象,不需要重写Thread类里面的run()方法,只需要重写Runnable类里面的run()方法
Thread(String name) 创建线程并命名
Thread(Runnable target ,String name) 使用Runnable对象创建线程对象,并命名
Thread(ThreadGroup group ,Runnable target) 线程可以被用来分组管理,分号的组即为线程组 [了解即可]

对于Thread(String name)这个构造方法,我们可以给线程起名字,起什么样的名字都无所谓,都不会影响线程的运行,起名字的主要目的就是为了 描述线程是干啥的,方便调试

示例:

    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            while (true){
                System.out.println("hello thread1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread2=new Thread(()->{
            while (true){
                System.out.println("hello thread2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread3=new Thread(()->{
            while (true){
                System.out.println("hello thread3");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

这是我手动创建了三个线程,当程序运行起来的时候,三个线程分别抢占式的执行

然后通过“jconsole.exe”文件看一下三个线程的运行状态

可以看到,这就是我们的那三个线程,下面,我们可以分别对他们进行命名,

我们再通过 “jconsole.exe”执行文件看一下我们的线程

可以发现,不管我们是什么类型的名字,都可以识别,所以说,这个命名的主要目的就是为了方便调试

注意:为什么没有main(主线程)线程?

在上图中,我们并没有发现main线程

这是因为,我们之前写的代码,都是单线程的程序,即main方法执行完毕之后,程序就结束了

但是多线程的程序中,当main方法执行完 thread3.start()这一行就直接结束了,主线程随即销毁,所以在 “jconsole.exe”可执行文件中只能看到3个子线程,看不到main线程

2.线程的其他属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
等待一个线程 join()
休眠当前线程 sleep()

(1).getId()

类似于PID,Java中给每个运行的线程都分配了id,标识线程的身份

(2).getName()

获取线程的名字

(3).isDaemon()

想要理解这个方法,我们需要先明白 什么是后台线程和前台线程

如果一个线程的结束不会影响到进程的结束,那么这个线程就是 “后台线程”,如果一个线程的结束影响到进程的结束,此时这个线程就被称为“前台线程”

示例:

    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            while (true){
                System.out.println("hello thread1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"1线程");
        Thread thread2=new Thread(()->{
            while (true){
                System.out.println("hello thread2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"bbb");
        Thread thread3=new Thread(()->{
            while (true){
                System.out.println("hello thread3");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"123");
        System.out.println(thread1.getPriority());
        thread1.start();
        thread2.start();
        thread3.start();
    }

还是通过这个例子来看

在线程图中我们可以看到,即使main线程结束了,但是线程“123”,线程“bbb”,线程“1线程”还在,既然三个线程还在,那么进程就依然存在,也就是说,这三个线程的存在,能够影响到进程继续存在而不能结束,此时这三个线程就被称为 “前台线程”

剩下的其他线程,其他线程都是JVM自带的一些线程,他们的存在不会影响到进程的结束,即使他们继续存在,如果进程结束了,那么他们也就结束了,此时这些线程就是 “后台线程”,例如,垃圾回收线程,垃圾回收线程跟随整个进程持续执行

通过一个例子来看

    public static void main(String[] args) throws InterruptedException {

        Thread thread=new Thread(()->{
           while (true){
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

        thread.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }

        System.out.println("main线程结束");
    }

我们可以发现,main线程的结束,没有影响到thread线程的结束,如果我们想要main线程结束的时候,thread线程也结束,那么我们就需要将thread线程修改成 “后台线程”,我们可以通过setDaemon()方法,将线程改成后台线程

(4).isAlive()

检查系统线程是否存活

首先要明确一点,Java代码中创建的Thread对象,和系统中的线程,是一 一对应的关系,但是Thread对象的生命周期和系统中的线程的生命周期是不同的,可能会存在一种情况就是,Thread对象还存活,但是系统中的线程已经销毁的情况

通俗一点说,系统线程 从start()开始,到run()执行结束,结束之后,系统线程就彻底销毁了

Thread对象,从new开始,就没有任何引用指向它,直到被垃圾回收之后,才算是结束生命周期

所以就会出现,Thread对象还存活,但是对应的系统中的线程已经销毁的情况

综上所述,isAlive()判断的是对应的系统线程是否还在执行/未终止,而不是判断Thread对象本身是否还在内存里。

通过一个例子来看

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println(thread.isAlive());   // 这个地方一定是false,因为这个时候 start 方法还没有执行
        thread.start();

        while (true){
            System.out.println(thread.isAlive());   //isAlive() 判断线程是否存活
            Thread.sleep(1000);
        }
    }

Thread中的代码逻辑,3秒之后就结束了,所以对应的线程的入口方法里面的逻辑就结束了,系统中对应的线程就随之销毁了(在操作系统的角度)

但是我们的thread对象依然存在

当代码运行起来的时候发现

(5).isInterrupted()

是否被中断

”中断“,让一个线程能够结束,让线程的入口方法执行完毕,线程就随之结束了,即 run()方法能够尽快的return()

①.示例

在理解这个方法之前,我们可以先通过一个例子来实现一下 ”中断一个线程“

    private static boolean isFinished=false;
    public static void main(String[] args) throws InterruptedException {

        Thread thread=new Thread(()->{
           while (!isFinished){
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
            System.out.println("thread 结束");
        });

        thread.start();

        Thread.sleep(3000);

        isFinished=true;
    }

上图中的例子,我们是通过修改外部类的成员变量来控制线程,然后内部类通过访问外部类的成员

来判断是否结束线程

问题:如果将成员变量isFinished修改成局部变量,可以吗?

通过上面的图片,发现报错了

这就是我们之前学的变量捕获方面的内容了

在lambda里面,如果希望使用外面的变量,那么就会触发 ”变量捕获“这样的语法,首先,lambda是“回调函数”,执行的时机可能是很久之后了,很有可能线程创建好了,当前main这里的方法都执行完了,那么对应的isFinished就销毁了,所以线程thread就无法获取到isFinished了

针对于这种情况,java就采用了 “变量捕获”的思想,把被捕获的变量拷贝一份,拷贝给lambda里面,外面的变量isFinished是否销毁,就不会影响到lambda里面的执行了,而拷贝,意味着这样的变量就不适合进行修改,因为修改一方另一方不会随之改变,本质上是两个变量,所以最终java这边就不允许进行修改

对于引用类型的变量,不能修改这个引用指向其他的对象,但是引用指向的对象本体是可以进行修改的,例如 :Test   test = new Test();        test 就表示引用类型的变量, new Test() 就表示对象的本体

上图,将局部变量修改成 成员变量,就不会涉及到 “变量捕获”的语法了,而是转换成“内部类访问外部类的成员” 语法

成员变量的生命周期也是让 垃圾回收 来管理的,在lambda里面不担心变量生命周期失效的问题,也就意味着不需要进行拷贝,也不必限制final之类的

②.isInterrupted() 和 interrupt()

Java的Thread对象中提供了现成的变量来中断线程,不需要我们自己进行创建

现在,将上面的代码进行修改

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
           while (!Thread.currentThread().isInterrupted()){
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
            System.out.println("thread 线程结束");
        });

        thread.start();

        Thread.sleep(3000);
        System.out.println("main 线程尝试中断 thread线程");
        thread.interrupt();
    }

currentThread()方法是一个静态方法,所以直接通过类名来进行调用,currentThread()方法的作用就是,在哪个线程中调用,获取到的就是哪个线程的Thread引用,当前是在lambda表达式中进行调用的,所以返回的结果就是thread

isInterrupted()方法是用来判断Thread 里的 boolean 变量的值,即判断线程是否被终止

注意:Thread.currentThread().isInterrupted() 这样写的原因是因为lambda的定义是在Thread实例化之前,我们要先重写完run方法之后才能进行对象的实例化,所以lambda表达式中是不知道thread的存在的,所以我们要先通过Thread.currentThread()方法来获取当前的线程对象

interrupt()方法是主动进行终止,修改这个 boolean 变量的值,从而使调用interrupt()方法的这个线程来结束线程

下面就开始运行程序

当程序运行起来的时候,发现报错了,这是为什么?

这是因为 interrupt()方法,除了设置boolean变量之外,还会唤醒sleep()这样的阻塞方法,在while()循环中,大部分的时间都在sleep,所以当主线程调用 interrupt()方法的时候,极大概率下thread线程正在sleep中,此时这个interrupt()方法就会唤醒sleep ,从而使sleep()方法抛出异常

如何解决这个异常?

我们可以这样做,当抛出的异常的时候,不要进行抛出,直接break掉这个循环

注意:这样的效果其实是抛出了异常我们没有进行捕获,而是直接中断了程序,其实和上面的捕获异常一样

同样,当抛出异常的时候,我们也可以不进行捕获

当程序运行起来的时候

发现,会一直打印 “hello thread”

这又是为什么?

对于上面这个代码,是sleep()方法导致的

当interrupt()方法将isInterrupted()方法内部修改成true,同时将sleep()方法给唤醒了,当sleep()方法被唤醒之后,将 isInterrupted()方法内部设置回了 false,因此在这种情况下,如果继续执行循环的条件判定,就会发现能够继续执行

Java这样实现的好处

Java把决定权交给了被终止的线程自己了,有三种选择

①.直接无视终止指令,继续执行。就像catch{}里面什么都没写一样

②.直接终止线程,直接在{}中加入break或者抛出异常

③.当捕获到异常之后,可以在处理异常处写上一些代码,获取阶段性的结果,也就是说如果线程终止了,我们可以的到线程终止之后的预期结果,而不是随机的结果。

(6).join()

前面我们介绍过多个线程之间是并发执行,随即调度的,如果我们不想让多个线程之间随即调度,那么我们可以通过join()方法,join()方法可以决定多个线程之间的结束的先后顺序

如果在main线程中调用thread.join()方法,可以让main线程等待thread线程先结束

join() 等待线程结束
join(long millis) 等待线程结束,最多等 millis 毫秒
join(long millis , int nanos) 等待线程结束,最多等待millis 毫秒 + nanos 纳秒   (更精确)

示例:

①.调用不带参数的join()方法
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread 线程结束");
        });

        thread.start();

        thread.join();

        System.out.println("main 线程结束");
    }

当在main线程中调用 thread.join()的时候,此时main线程就会等待thread线程先结束,当执行到thread.join()的时候,main线程就会进入 “阻塞等待”,一直等到 thread 线程执行完毕,main线程才会继续执行   

②.调用带一个参数的join()方法
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread 线程结束");
        });

        thread.start();

        thread.join(1000);

        System.out.println("main 线程结束");
    }

对于上面的代码,可以看出,main线程只会等待thread线程1000毫秒,如果经过1000毫秒之后,thread线程还没有结束,那么main线程也就不等了

③.调用带两个参数的join()方法
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread 线程结束");
        });

        thread.start();

        thread.join(1000,500);

        System.out.println("main 线程结束");
    }

带两个参数的join()方法,就会使等待的时间更精确,但是我们使用的计算机一般很难进行ns级别的精确时间计算。有一类操作系统为 “实时操作系统”可以做到更精确的时间,这样的操作系统一般会用于“工业,航天”等方面,我们日常的系统都不是 “实时操作系统”

(7).sleep()

休眠当前线程,但是注意一点,线程是随即调度的,所以这个方法只能保证实际休眠时间大于等于参数设置的休眠时间

当我们写了 sleep(1000),实际上会休眠比1000多一点,因为代码调用sleep,相当当前线程于cpu会让出资源,当时间到了的时候,需要操作系统内核把这个线程重新调用到cpu上才能继续执行,也就是说,当时间到了的时候,意味着允许被调度了而不是立即执行了,所以说会比1000多一点

特殊写法:sleep(0) 写了sleep(0) 意味着让当前的线程 立即放弃cpu资源,把cpu让出来给别人更多的执行机会,等待操作系统重新调度

(8).getState()

获取线程的状态

站在操作系统的角度,进程状态分为 就绪 和 阻塞

Java 线程也是对操作系统线程的封装

针对线程的状态,Java也进行了重新封装,进行表示

NEW:表示已经创建出来了Thread对象,但是还没有start

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            System.out.println("hello thread");
        });
        //获取线程的状态
        System.out.println(thread.getState());
        
    }

RUNNABLE:线程正在CPU上执行/线程随时可以去CPU上执行

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while (true){
                //什么都不写
                //本身也是一段cpu指令,一直循环执行,也是需要在cpu上运行的
            }
        });
        //获取线程的状态
        System.out.println(thread.getState());

        thread.start();

        Thread.sleep(1000);

        System.out.println(thread.getState());
    }

TERMINATED:内核中的线程已经结束了,但是Thread对象还在

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            System.out.println("hello thread");
        });
        //获取线程的状态
        System.out.println(thread.getState());

        thread.start();

        Thread.sleep(1000);

        System.out.println(thread.getState());
    }

TIMED_WAITING:指定时间阻塞,线程阻塞,不参与cpu调度,不继续执行,但是阻塞的时间也是有上限的

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
           while (true){
               try {
                   Thread.sleep(2000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }

while(true){}中sleep()是2000毫秒,然后在主线程中,我sleep() 了 1000毫秒,所以当 1000毫秒之后,我获取thread线程的状态,此时thread线程还在阻塞过程中,所以状态为TIMED_WAITING,但是当2000毫秒之后,又会变成RUNNABLE

另外 join(时间) 也会进入到 TIMED_WAITING状态

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();

        thread.join(60000);
    }

WAITING:死等,和TIMED_WAITING 相对

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
        //只需要调用不带参数的join()方法,就可以将状态变成WAITING
        thread.join();
    }

BLOCKED:是一种由 “锁”导致的阻塞比较特殊,等下一章线程安全的时候再进行介绍

总结:

介绍的这几种线程的状态,主要是用于调试程序,找BUG的时候使用

当发现代码中出现BUG的时候:

①.通过 jconsole.exe 或者 其他工具,查看当前的进程中的所有线程,找到对应逻辑的线程是谁

②.看线程的状态是啥

        看到TIMED_WAITING /WAITING ,怀疑是不是代码中某个方法产生阻塞,没有及时唤醒

        看到BLOCKED,怀疑是不是代码中出现了死锁

        看到RUNNABLE,线程本身没问题,考虑逻辑上某些条件没有预期触发之类的

③.看线程的具体调用栈(尤其是 阻塞的状态,线程代码阻塞到哪一行了)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐