[JavaEE系列] Thread类的基本用法

文章目录

  • 线程创建
    • 第一类: 继承 Thread 类
      • 继承 Thread 类, 重写 run 方法
      • 使用匿名内部类, 继承 Thread 类, 重写 run 方法
    • 第二类: 实现 Runnable 接口
      • 实现 Runnable 接口, 重写 run 方法
      • 使用匿名内部类, 实现 Runnable 接口, 重写 run 方法
    • 第三类: 使用 lambda 表达式
  • 启动线程
    • 比较 start() 方法和 run() 方法两者的区别
  • 线程中断
    • 使用自己的标志位(定义一个全局变量)
    • 使用 Thread 自带的标志位
  • 线程等待
  • 获取线程引用
  • 线程休眠
  • 线程的状态(目前了解即可, 后面详细总结)
    • 线程之间状态的转换

线程创建

        关于线程创建的方法主要有三大类五种方法, 下列一一进行介绍:

第一类: 继承 Thread 类

继承 Thread 类, 重写 run 方法

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

public class Main {
    public static void main(String[] args) {
        MyThread1 thread1 = new MyThread1();
        thread1.start();
    }
}

使用匿名内部类, 继承 Thread 类, 重写 run 方法

public class Main {
    public static void main(String[] args) {
        Thread thread3 = new Thread(){
            @Override
            public void run() {
                System.out.println("thread3");
            }
        };
        thread3.start();
    }
}

第二类: 实现 Runnable 接口

实现 Runnable 接口, 重写 run 方法

class MyThread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("thread2");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread2 = new Thread(new MyThread2());
        thread2.start();
    }
}

使用匿名内部类, 实现 Runnable 接口, 重写 run 方法

public class Main {
    public static void main(String[] args) {
        Thread thread4 = new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println("thread4");
            }
        });
        thread4.start();
    }
}

第三类: 使用 lambda 表达式

public class Main {
    public static void main(String[] args) {
        Thread thread5 = new Thread(() -> {
            System.out.println("thread5");
        });
        thread5.start();
    }
}

启动线程

        正如上面的这几段代码, 使用的都是 start() 方法来启动线程的, 但其实我们启动线程除了使用 start() 方法之外, 可能还会使用 run() 方法(但是使用的最多的还是前者, 后者使用的比较少).

比较 start() 方法和 run() 方法两者的区别

  1. start()可以启动一个新线程, run()不可以
  2. start()不可以被重复调用, run()可以
  3. start()实现了多线程, run()没有实现多线程

线程中断

        关于线程中断的方法主要有两种: 一是直接使用自己的标志位来区分线程是否要结束; 二是使用 Thread 自带的标志位.

使用自己的标志位(定义一个全局变量)

public class Main{
    public static boolean isQuit = false;

    //直接使用自己的标志位来区分线程是否要结束
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(!isQuit){
                System.out.println("线程运行中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程执行结束");
        });
        thread.start();
        Thread.sleep(5000);
        isQuit = true;
    }
}

使用 Thread 自带的标志位

        使用 Thread.currentThread().isInterrupted() 判定内置的标志位来让线程中断, 当为true时, 表示线程要被中断.

public class Main{
    //使用 Thread 自带的标志位
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("线程运行中...");
                try{
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    //e.printStackTrace();
                    break;
                }
            }
        });

        thread.start();
        Thread.sleep(5000);
        System.out.println("控制新线程退出");
        thread.interrupt();
    }
}

        这里需要重点注意 interrupt() 方法的行为:

  • 如果调用该方法的 thread 线程没有处在阻塞的状态, 那么此时 interrupt() 方法就会修改内置的标志位.
  • 如果调用该线程的 thread 线程正在处在阻塞状态, 那么此时 interrupt() 方法就会让线程内部产生阻塞的方法抛出 InterruptedException 异常.

        正是因为有 interrupt() 方法的这种特性, 我们就可以灵活地控制线程什么时候退出, 由于线程在阻塞状态的时候会抛出 InterruptedException 异常, 所以我们只需要在捕获异常的时候选择性地编写代码: 1. 可以直接立即退出(直接加上break); 2. 也可以等一会再退出(加上一些收尾工作的代码再加上break); 3. 可以继续执行线程, 不退出(不加break / 啥都不干, 继续执行循环).

线程等待

        在了解线程等待操作之前, 先来看看一个例子:

public class Main {
    public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> {
            int a = 0;
            for(long i = 0; i < 10_0000_0000; i++){
                a++;
            }
        });
        Thread thread2 = new Thread(() -> {
            int a = 0;
            for(long i = 0; i < 10_0000_0000; i++){
                a++;
            }
        });
        thread1.start();
        thread2.start();
        long end = System.currentTimeMillis();
        System.out.println("并发执行的时间:" + (end - begin) + "ms");
    }
}

运行结果:
[JavaEE系列] Thread类的基本用法_第1张图片

        在这个代码中, 我们可以很清楚地看到运行的时间大概是170ms, 但是这样的时间真的是我们所要求的时间吗, 显然不是, 当我们加上 join() 方法来进行线程等待之后, 结果会变成这样:

代码:

public class Main {
    public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> {
            int a = 0;
            for(long i = 0; i < 10_0000_0000; i++){
                a++;
            }
        });
        Thread thread2 = new Thread(() -> {
            int a = 0;
            for(long i = 0; i < 10_0000_0000; i++){
                a++;
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("并发执行的时间:" + (end - begin) + "ms");
    }
}

运行结果:
[JavaEE系列] Thread类的基本用法_第2张图片

        由图中不难看出, 加上 join() 方法执行线程等待的时候, 执行时间翻了几倍之多, 这是为什么?
        我们的目标是要计算出线程1和线程2执行完之后的总时间, 但是在第一段代码中, 也就是在没加入线程等待的时候, 由于线程之间是随机调度执行的, 在 thread1 , thread2 , main 三个线程当中, 很可能就会出现线程1和线程2还没有执行完, main这个线程就已经执行结束了, 这样子就不能准确地计算出执行完线程1和线程2的总时间. 而在第二段代码中, 我们在 main 线程结束之前, 就先线程1和线程2都调用 join() 方法, 效果是让 main 线程等待这两个线程之后再执行下面语句, 从而能够准确地计算出两个线程执行的总时间.
        当然, 直接使用这种不带参数的 join() 方法是会出现死等的现象的, 针对这一问题, 我们有时候就会使用到一些其他的 join() 带参数的版本.

获取线程引用

        我们在对线程进行操作的时候(比如判断线程状态, 优先级, 是否存活, 是否被中断等等), 都是需要获取到线程的引用. 那么, 我们要如何才能获取到线程的引用呢?
        当创建线程的时候是继承 Thread 类并且重写 run() 方法的时候, 我们是可以直接在 run() 方法中使用 this 来获取到线程的实例.
        当创建线程是使用 Runnable 接口或者是使用 lambda 表达式的时候, 就不能再使用 this 了, 因为此时的 this 不是指向 Thread 实例. 我们在这里更为通用的方法是 currentThread() 这个静态方法, 当有一个线程来调用这个静态方法的时候, 得到的结果就是这个线程的实例(这个方法不论在何种情况下都是适用的).

线程休眠

        像我们前面经常使用到的, Thread.sleep(1000) 就是让线程进入休眠. 当然, 线程休眠其实就是在操作系统内核中, 将系统中的PCB在就绪队列(表示PCB可以随时到CPU上执行)和阻塞队列(表示当前的PCB暂时不参与系统的调度)之间相互转换, 而操作系统中的PCB又是以链表的形式存在的, 画成图像可以是这样:

[JavaEE系列] Thread类的基本用法_第3张图片

        其中, 每一个小方块就代表一个PCB. 假设蓝色方块是准备要进入休眠的状态, 那么它就会加入到阻塞队列, 如下图:

[JavaEE系列] Thread类的基本用法_第4张图片

        这时候, 蓝色方块就进入休眠状态, 当 sleep 时间到了之后, 就会自动调度回就绪队列, 如下图:

[JavaEE系列] Thread类的基本用法_第5张图片

        注意: 在操作系统内核中管理PCB的链表实际上是有很多个的, 他们分别都有就绪队列和阻塞队列来参与PCB的调度.

线程的状态(目前了解即可, 后面详细总结)

        线程具体可以分为以下这几种状态:

  • NEW: 安排工作, 并未开始行动. 在这一状态中, 主要是创建了 Thread 对象, 但是还没有调用 start 方法(操作系统内核中还不存在线程).
  • RUNNABLE: 可工作的, 又可分为正在工作中和即将开始工作. 在这一状态中, 主要是就绪状态, 已经在CPU上执行了或者是已经准备好了但是还没有在CPU上执行.
  • BLOCKED: 等待锁. 在这一状态中, 线程是属于加锁状态的, 其他线程要使用这个锁对象就必须阻塞等待.
  • WAITING: 表示线程调用的 wait() 方法.
  • TIMED_WAITING: 表示线程通过 sleep 方法进入阻塞.
  • TERMINATED: 表示工作完成了. 在这一状态中, 主要是系统里面的线程已经执行完销毁了, 但是Thread 对象还在.

线程之间状态的转换

[JavaEE系列] Thread类的基本用法_第6张图片

        Java中把阻塞状态按照不同的原因分成三个状态: BLOCKED, WAITING, TIMED_WAITING. 这三种阻塞状态进入的方式是不一样的, 阻塞的时间和被唤醒的方式也是不同的.

你可能感兴趣的:(JavaEE初阶系列,java-ee,java,jvm)