二、创建线程+查看线程+Thread方法+线程状态

一、创建线程的五种方法

前置知识

  1. Thread 类是用于创建和操作线程的类。每个线程都必须通过 Thread 类的构造方法创建,并实现 run() 方法来执行线程的任务。
  2. run() 方法是 Thread 类中用于定义线程要执行的任务的方法。当一个线程被启动后,它会调用自己的 run() 方法,在该方法中执行线程的任务逻辑。
  3. 需要注意的是,直接调用 run() 方法并不会启动一个新的线程,而只会在当前线程中依次执行 run() 方法中的代码。如果要启动一个新的线程并执行 run() 方法中的代码,应该使用 start() 方法来启动线程。

1、方法一:使用继承Thread类,重写run方法

class MyThread extends Thread {

    //run是线程的入口方法
    @Override
    public void run() {

        System.out.println("Hello t");
    }
}

public class ThreadDemo1 {

    //这种方式是使用Thread 的run来描述线程入口
    public static void main(String[] args) throws InterruptedException {

        // Thread通过接收重写Thread内部run方法的子类
        Thread t = new MyThread();
        // start 启动线程
        t.start();
    }
}

2、方法二:实现Runnable接口,重写run方法

在Java中,Runnable是一个函数式接口(Functional Interface),用于表示要在一个线程中执行的任务。

class MyRunnable implements Runnable {

    @Override
    public void run() {

        System.out.println("hello t");
    }
}

public class ThreadDemo2 {

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

        // 1.先实例化实现了Runnable接口的类
        MyRunnable runnable = new MyRunnable();
        // 2.通过Thread的构造方法,传入runnable任务,创建线程
        Thread t = new Thread(runnable);

        t.start();
    }
}

3、方法三:继承Thread,使用匿名内部类

public class ThreadDemo3 {

    public static void main(String[] args) {

        // 此处的new Thread(){...};就相当于一个继承了Thread类的子类
        Thread t = new Thread(){

            @Override
            public void run() {

                System.out.println("hello t");
            }
        };

        t.start();
    }
}

4、方法四:实现Runnable,使用匿名内部类

public class ThreadDemo4 {

    public static void main(String[] args) {

        // 此处的new Runnable(){...}就相当于一个实现了Runnable接口的类
        Thread t = new Thread(new Runnable() {

            @Override
            public void run() {

                System.out.println("hello t");
            }
        });

        t.start();
    }
}

5、方法五:使用lambda表达式(常用)

虽然上面四种方式都可以达到创建线程的目的,但都不是常用的写法,推荐使用 lambda 表达式是最简单最直观的写法!

回顾lambda表达式

提到lambda 表达式,下面我们在来回顾一下:

lambda 表达式,本质上就是一个匿名函数。(Java里面,函数(方法)是无法脱离类的,在Java里面 lambda 就相当于是一个例外,它可以将一个函数(或者说方法)作为参数传递到另一个方法中,而不需要将它包含在一个类中。)

虽然说,lambda 表达式可以在⼀定程度上简化接口的实现。但是,并不是所有的接口都可以使用lambda表达式来简洁实现的。lambda 表达式毕竟只是⼀个匿名方法。当实现的接口中的方法过多或者多少的时候,lambda表达式都是不适用的。lambda 表达式,只能实现函数式接口。

函数式接口:如果说,⼀个接口中,要求实现类必须实现的抽象方法,有且只有⼀个,这样的接口,就是函数式接口。

语法规则:

interface Demo {

    public void test();
}
public class Test {

    public static void main(String[] args) {

        // 使用lambda表达式实现接口
        Demo demo = () -> {

            System.out.println("test");
        };
        demo.test();
    }
}

其他规定

  1. ()里面放参数,如果只有一个参数,可以省略 ()
  2. {}里面放函数体,如果只有一行代码,也可以省略 {}
  3. “变量捕获”,lambda 表达式要想访问外面的局部变量,java 要求变量必须是 final 或是 “等效final”(即变量中没有用final修饰,但是代码中并没有做出修改)

使用 lambda 创建线程

public class ThreadDemo5 {

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

        // lambda本质上是实现了Runnable接口
        Thread t = new Thread(()->{

            System.out.println("hello t");
        });

        t.start();
    }
}

二、体验多线程

运行以下代码体验以下多线程情况下的代码执行。

下面代码中用到了sleep 方法,关于sleep方法说明:

在Java 中,Thread 类中的 sleep(long millis) 方法用于使当前线程进入休眠状态,暂停执行一段时间。该方法接受一个以毫秒为单位的时间参数,表示要休眠的时间长度。

当线程处于休眠状态时,它不会占用CPU资源,也不会执行任何代码,直到休眠时间结束。在休眠期间,线程可以被中断(通过调用 interrupt() 方法),或者其他线程可以抢占 CPU 资源,使得该线程处于等待状态。

public class ThreadDemo5 {

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

        Thread t = new Thread(()->{

            while (true) {

                try {

                    Thread.sleep(1000);
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
                System.out.println("hello t");
            }
        },"t");

        t.start();
        while (true) {

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

file

  1. 上述代码涉及两个线程:(1) main 方法所对应的线程(一个进程中至少有一个线程)也可称为主线程。(2) t 线程
  2. 运行程序,其实就是idea对应的进程创建了一个新的 java进程,这个java进程用来执行自己写的代码。同时这个 java进程里有两个线程,一个是main,一个是t,每个线程都是一个独立的执行流。此时的 hello t 是由 t 线程执行的打印逻辑。
  3. 这里的交替打印并不是严格意义上的交替,每一秒过后,先打印main还是先打印 t 是不确定的,因为多个线程在 CPU 上调度执行的顺序是不确定的(随机的)。

查看线程详情

我们可以使用 jdk 提供的第三方工具,查看java进程里面的线程详情:

file

注意事项:

  1. jconsole 只能分析 Java 进程。
  2. 运行就 jconsole,如果进程列表为空,可以尝试以管理员身份运行。

file

点进当前代码的进程对应的线程:

file

file

三、Thread及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联,即Java代码中的Thread对象和操作系统中的线程是一一对应的。而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

1、构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名

注意:name 名字参数,是给线程起了个名字,这里的名字不影响程序的执行,只是方便我们在调试的时候,快速找到需要的线程。

2、线程属性获取方法

返回值类型 方法名 说明
long getId() 返回线程标识符Id
String getName() 返回线程名称
Thread.State getState() 返回线程状态
int getPriority() 返回线程优先级
boolean isDaemon() 判断是否为后台线程
boolean isAlive() 判断线程是否存活
boolean isInterrupted() 判断线程是否被中断

(1)isDaemon()-前台线程后台线程说明:

  1. isDaemon()返回true-表示后台线程,后台线程不阻止Java进程结束,哪怕后台线程还没执行完,Java进程该结束就结束。
  2. isDaemon()返回false-表示前台线程,前台线程会阻止Java进程结束,必须得Java进程中所有的前台线程执行完Java进程才能结束。

注:创建的线程默认是前台的,可以通过setDaemon(true)设置成后台的。

(2)isAlive()线程存活说明

描述的是系统内核里哪个线程是否存活,也就是说只有调用start()方法(调用 start 方法,
才真的在操作系统的底层创建出一个线程),启动线程之后,当线程正在执行时返回true,否则返回false

(3)isInterrupted()后面详细介绍…

3、启动线程-start()

上面介绍的五种创建线程的方式,都是通过覆写 run() 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。只有调用了start()方法,才真正从系统这里创建一个线程。

调用 start 方法, 才真的在操作系统的底层创建出一个线程。

4、中断一个线程:(让一个线程停下来)

中断一个线程,就是让一个线程停下来,即线程终止,本质上来说,线程终止就是让该线程的入口方法执行完毕。这里的执行完毕可以是 return 返回、代码执行完毕、抛出异常 等情况。具体来说,可以采取如下策略:

(1)给线程设置一个结束标志位
例如:设置标志位isQuite作为线程结束的标志

public class ThreadExample_Interrupted {

    // 由于"变量捕获",这里将isQuite设置为成员变量
    public static boolean isQuite = false;

    public static void main(String[] args) {

        Thread t = new Thread(()->{

            while (!isQuite) {

                System.out.println("hello t");
                try {

                    Thread.sleep(1000);
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
            }
            System.out.println("t线程终止");
        });

        t.start();

        // 3秒后,在主线程中修改isQuite
        try {

            Thread.sleep(3000);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
        isQuite = true;

    }
}

file

注意:这种情况下是将isQuite设置成了成员变量,如果将其设置成局部变量,正常情况下由于线程和线程之间共用一个内存地址空间,语法上是成立的,但是对于lambda表达式要是想要访问外面的局部变量,这时就涉及到了Java变量捕获,即捕获的变量必须是 final 或者 “等效final”,即变量中没有用final修饰,但是代码中并没有做出修改。

(2)使用Thread类内置的标志位

方法名称 说明
public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
public void interrupt() 中断对象关联的线程。如果线程正在阻塞,则以异常方式通知,否则设置标志位true。
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位。
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位。

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记。根据上述提供的方法,我们可以使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位。

例如:还是上面的例子,这次我们使用内置标志位

public class ThreadExample_Interrupted2 {

    public static void main(String[] args) {

        Thread t = new Thread(()->{

            // 此处 currentThread currentThread 是获取到当前线程实例 t
            while (!Thread.currentThread().isInterrupted()) {

                System.out.println("hello t");
                try {

                    Thread.sleep(1000);
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
            }
        });

        t.start();
        try {

            Thread.sleep(3000);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }

        // 将内部的标志位设置成true
        t.interrupt();
    }
}

file

注意:通过上面结果,我们看到,即使sleep被强制唤醒后,触发了两件事:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException异常的形式通知.
  2. 清除中断标志.

因此我们会看到,抛出异常后,t 线程中的循环继续进行(因为此时sleep被唤醒后清空了标志位 true->false)

对于interrupt 只是通知不是命令,至于为什么 Java 不强制设置成“命令结束”的操作,主要是因为,这种强制性的设定是非常不友好的,对于线程 线程何时结束,始终是线程本身最清楚,所以还是交给线程自身来决定比较好。

5、等待一个线程-join()

线程之间是并发执行的,操作系统对于线程的调度是无序的,无法判断两个线程谁先执行结束,谁后执行结束。然而有时候有需要明确规定线程的结束顺序,这时就可以使用线程等待-join来实现。

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等 millis 毫秒。

例如:我们需要等待一个线程t完成它打印工作后,才能进行main线程的打印工作。

public class ThreadExample_join {

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

        Thread t = new Thread(() -> {

            for (int i = 0; i < 5; i++) {

                System.out.println("hello t");
            }
        });

        t.start();
        // 正常情况下,如果不加join,大部分情况下是先打印hello main(因为创建线程也是需要开销的)
        t.join();//这里就使t线程先执行完,main暂时阻塞
        System.out.println("hello main");
    }
}

file

上述代码在main线程中调用 t.join:如果 t 线程还没结束,main 线程就会“阻塞”等待-Blocking。也就是说代码执行到 t.join 时就停下来了,当前这个线程暂时不参与CPU的调度执行了。直到 t 线程执行完毕,此时 main 解除阻塞后将继续向下执行。


四、线程的状态

操作系统中的线程,自身是有一个状态的,但是Java 中Thread是对系统线程的封装,将里面的状态进一步精细化了。

  • NEW :系统中的线程还没创建出来,但是有个 Thread 对象
  • RUNNABLE: 就绪状态(1.正在CPU上执行 2.准备好随时可以去CPU上运行)
  • TERMINATED: 系统中的线程已经执行完了,Thread对象还在
  • TIMED_WAITING: 指定时间等待
  • BLOCKED :等待锁出现的状态
  • WAITING :使用 wait、join 方法出现的状态

file