javaEE - 1(9000字详解多线程第一篇)

一:认识线程

1.1 线程的概念

线程是操作系统中执行的最小单位,它是进程中的一个实体。一个进程可以包含多个线程,并且这些线程共享进程的资源,如内存、文件句柄等,但每个线程有自己的独立执行流程和栈空间。

线程在操作系统的调度和执行过程中担任重要角色,一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 它可以独立执行特定任务,也可以与其他线程通过同步机制进行协作。通常,一个进程中的多个线程可以并发执行,共享相同的上下文和资源,从而提高系统的并发处理和资源利用率。

1.2 并行和并发

并发和并行是计算机领域中两个常用的概念。

并发指的是在同一时刻只能执行一个任务,但是通过快速切换和时间片轮转等技术,可以让多个任务交替执行,给人一种同时进行的感觉。在并发的情况下,多个任务之间可能是交替执行的。

并行则指的是多个任务在同一时刻同时执行,每个任务拥有自己的执行资源,比如独立的 CPU 核心。在并行的情况下,多个任务可以同时进行,每个任务都在独立的执行路径上进行处理。

简单来说,如果多个任务交替执行,就是并发;如果多个任务同时执行,就是并行。

但是因为并行和并发难以感知,所以我们把并行和并发这两个概念统称为并发

1.3 为啥要有线程

首先, “并发编程” 成为 “刚需”.

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine),关于线程池我们后面再介绍.

1.4进程和线程的区别

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

javaEE - 1(9000字详解多线程第一篇)_第1张图片
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

二:线程操作

2.1创建线程

2.1.1 方法1 继承 Thread 类

  1. 继承 Thread 来创建一个线程类.
class MyThread extends Thread {
  @Override
  public void run() {
    System.out.println("这里是线程运行的代码");
 }
}

注意:run方法中写的是这个线程要执行的任务,一个线程在调用了start()方法之后,才算是真正创建了。

  1. 创建 MyThread 类的实例
MyThread t = new MyThread();
  1. 调用 start 方法启动线程
t.start(); // 线程开始运行

2.2.2 方法2 实现 Runnable 接口

  1. 实现 Runnable 接口
class MyRunnable implements Runnable {
  @Override
  public void run() {
    System.out.println("这里是线程运行的代码");
 }
}
  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
  1. 调用 start 方法
t.start(); // 线程开始运行

2.2.3两种方法的区别

这两种写法有一些区别:

  1. 继承 Thread 类:当我们继承 Thread 类并重写 run() 方法时,this 关键字表示的是当前线程对象的引用,即 new 出来的线程对象本身。我们可以直接调用该对象的方法,比如 this.sleep()。

  2. 实现 Runnable 接口:当我们实现 Runnable 接口并创建了一个 Thread 对象来运行该 Runnable 对象时,this 关键字表示的是实现了 Runnable 接口的类的实例,即 MyRunnable 的引用。我们不能直接调用 this.sleep(),而是要使用 Thread.currentThread().sleep()。

总体上说,两种写法都可以创建线程并实现线程的运行逻辑,不同之处在于 this 表示的对象引用不同。

2.2.4其他变形

  1. 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
  @Override
  public void run() {
    System.out.println("使用匿名类创建 Thread 子类对象");
 }
};
  1. 匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
  @Override
  public void run() {
    System.out.println("使用匿名类创建 Runnable 子类对象");
 }
});
  1. lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
  System.out.println("使用匿名类创建 Thread 子类对象");
});

通过这几种方法就可以创建一个线程了,需要注意的是,一个线程在调用了start()方法之后,才算是真正创建了。调用start()方法会导致线程进入就绪状态,并且系统会为该线程分配执行资源,使得它可以开始执行run()方法中的代码。

只有调用start()方法才会创建一个新的线程,run()方法只是分配了这个线程的任务是什么,不会创建新的线程。所以,当我们使用多线程编程时,要确保通过调用start()方法来启动线程,以保证线程的正确创建和执行。

三:Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

2.1Thread 的常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group, Runnable target) 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2 Thread 的几个常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了

2.3 线程中断问题

线程的中断问题,下面我们进一步说明:

interrupt()、interrupted() 和 isInterrupted() 方法是 Java 中用于线程中断的相关方法,它们有着不同的功能和用法。下面我将逐一解释它们的区别,并提供相应的代码示例。

  1. interrupt() 方法:
    interrupt() 方法是用于中断线程的方法。它并不会真正地中断线程,而是给线程设置一个中断标志,表示线程被请求中断。具体来说,当调用线程的 interrupt() 方法时,如果线程处于阻塞状态(如 sleep、wait、join 等方法),就会抛出 InterruptedException 异常并清除中断状态,接着结束睡眠,如果线程未处于阻塞状态,仅仅是设置中断标志,并不是真正的中断线程。线程的中断由线程决定,线程可以通过检查中断标志来决定是否中断自己的执行。

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记:
javaEE - 1(9000字详解多线程第一篇)_第2张图片

以下是一个示例代码,演示了如何使用 interrupt() 方法中断线程:

public class MyThread extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            // 线程任务逻辑
            // ...
        }
    }
}

public static void main(String[] args) {
    MyThread thread = new MyThread();
    thread.start();

    // 等待一段时间后中断线程
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread.interrupt();
}

注意:当sleep被唤醒之后,sleep会自动把 isInterrupted()标志位给清空(true - > false)
javaEE - 1(9000字详解多线程第一篇)_第3张图片
注意:异常的抛出会清除线程的中断标志位,但并不会立即停止线程的执行。它只会在线程的指定时间段内抛出异常,告诉线程当前处于中断状态,然后线程有机会从阻塞状态恢复,并在异常被捕获之后继续执行。

这种设计的目的是为了给开发者一个处理线程中断的机会。当线程被中断后,开发者可以根据具体业务需求来决定如何处理中断,因此,当线程被InterruptedException异常唤醒时,isInterrupted()的返回结果会变为false, 要不要结束线程取决于 catch 中代码的写法. 可以选择
忽略这个异常, 也可以跳出循环结束线程

  1. interrupted() 方法:
    interrupted() 方法是一个静态方法,用于判断当前线程是否被中断,并返回中断状态。而且在判断中断状态后,还会自动清除中断标志。如果线程在判断中断状态时没有被中断,则返回 false;如果线程在判断中断状态时被中断,则返回 true。

以下是一个示例代码,演示了如何使用 interrupted() 方法判断当前线程是否被中断:

public class Main {
    public static void main(String[] args) {
        if (Thread.interrupted()) {
            System.out.println("线程被中断了");
        } else {
            System.out.println("线程未被中断");
        }
    }
}

注意:interrupted() 方法会清除中断状态,即使线程未被中断。

  1. isInterrupted() 方法:
    isInterrupted() 方法是实例方法,用于判断线程是否被中断。如果线程在判断中断状态时没有被中断,则返回 false;如果线程在判断中断状态时被中断,则返回 true。并且标志位是否清除取决于这两种写法:Thread.isInterrupted() 和Thread.currentThread().isInterrupted()

标志位是否清除, 就类似于一个开关:

  • Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”,线程中断会清除标志位
  • Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为 “不清除标志位”, 线程中断标记位不会清除.

示例代码1:

public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        System.out.println(Thread.interrupted());
     }
   }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    thread.start();
    thread.interrupt();
 }
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false

示例代码2:

public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
     }
     }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    thread.start();
    thread.interrupt();
 }
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true

2.4线程启动

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了,调用 start 方法, 才真的在操作系统的底层创建出一个线程.

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

javaEE - 1(9000字详解多线程第一篇)_第4张图片

2.5 等待一个线程

因为线程是并发执行的,线程的调度是抢占式执行的,所以操作系统对于线程调用的顺序的不知道的,无法判断哪个线程先结束,因此java提供了join()方法。

在Java中,join()方法是Thread类的一个方法,它允许一个线程等待另一个线程的完成。当一个线程调用另一个线程的join()方法时,调用线程将被阻塞,直到被调用的线程执行完毕。

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos) 同理,但可以更高精度

下面是代码示例:

public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                // 模拟线程执行耗时的操作
                Thread.sleep(2000);
                System.out.println("子线程执行完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread.start(); // 启动子线程
        thread.join(); // 主线程等待子线程执行完毕

        System.out.println("主线程继续执行");
    }
}

在上面的代码中,主线程启动子线程后调用了子线程的join()方法,这样主线程就会等待子线程执行完毕后再继续执行。在子线程中,我们通过模拟一个耗时的操作(线程休眠2秒)来演示子线程的执行过程。当子线程执行完毕后,主线程才会继续执行并输出"主线程继续执行"。

2.6获取当前线程引用

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用

这个方法我们非常熟悉了

public class ThreadDemo {
  public static void main(String[] args) {
    Thread thread = Thread.currentThread();
    System.out.println(thread.getName());
 }
}

2.7 休眠当前线程

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException 可以更高精度的休眠
public class ThreadDemo {
  public static void main(String[] args) throws InterruptedException {
    System.out.println(System.currentTimeMillis());
    Thread.sleep(3 * 1000);
    System.out.println(System.currentTimeMillis());
 }
}

四:线程的状态

4.2线程的状态

线程的状态是一个枚举类型 Thread.State,我们可以通过这段代码得到线程的状态:

public class ThreadState {
  public static void main(String[] args) {
    for (Thread.State state : Thread.State.values()) {
      System.out.println(state);
   }
 }
}

线程在Java中有几种不同的状态,每种状态表示线程在执行过程中的不同阶段或状态。线程的状态主要有以下几种:

  1. 新建状态(NEW):当线程对象被创建时,它处于新建状态。在这个状态下,线程没有被启动,还未开始执行。

  2. 可运行状态(RUNNABLE):线程处于可运行状态意味着线程已经通过调用start()方法启动,可以开始执行了。线程可能正在执行,也可能在等待CPU时间片来执行,或者在等待其他资源。

  • 正在运行(Running):线程正在执行中。
  • 就绪(Ready):线程等待CPU调度,具备执行条件,但尚未获得CPU时间片。
  1. 阻塞状态(BLOCKED):线程可能因为等待获取一个排它锁(synchronized关键字)而进入阻塞状态。当其他线程持有锁时,该线程将被阻塞,只有当获取到锁时才能继续执行,BLOCKED 表示等待获取锁

  2. 等待状态(WAITING):线程在等待其他线程的特定操作,例如等待其他线程的通知或等待输入/输出操作完成,WAITING 表示等待其他线程发来通知,WAITING 线程在无限等待唤醒

  3. 超时等待状态(TIMED_WAITING):类似于等待状态,但是可以设置一个超时时间,当超过指定时间后,线程将自动恢复到可运行状态, TIMED_WAITING 表示线程在等待唤醒,等待其他线程发来通知,但设置了时限。

  4. 终止状态(TERMINATED):线程已经完成执行,或者因为异常而终止。

线程状态之间可以相互转换,而且转换的过程是由Java的线程调度器负责控制的。

下面是对线程状态的比喻:

  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了.

javaEE - 1(9000字详解多线程第一篇)_第5张图片
下面是各个状态的详细关系图:

javaEE - 1(9000字详解多线程第一篇)_第6张图片
所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

4.2 yield()

在Java中,yield()方法用于暂停当前正在执行的线程,并允许其他线程运行。它是线程调度器的一部分,用于实现线程间的合作和协调。

yield()方法的作用是提醒调度器当前线程可以放弃cpu控制权,但是它不保证CPU资源一定会让给其他线程,只是将当前线程的状态更改为就绪状态,然后再次与同等或更高优先级的线程竞争CPU资源。

下面是yield()方法的一般语法:

Thread.yield();

以下几点要注意:

  1. yield()方法只能在线程内部调用,不能在静态方法中使用。
  2. yield()方法不会释放锁,即使线程调用了yield()方法,它仍然持有当前获取的锁。
  3. 当线程调用yield()方法后,它可能会立即重新获得执行权,因此yield()方法不能用来实现线程的顺序执行。

下面是一个简单的示例代码,演示了yield()方法的使用:

Thread t1 = new Thread(new Runnable() {
  @Override
  public void run() {
  while (true) {
      System.out.println("张三");
      // 先注释掉, 再放开
      // Thread.yield();
   }
 }
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
  @Override
  public void run() {
    while (true) {
      System.out.println("李四");
   }
 }
}, "t2");
t2.start();

可以看到:

  1. 不使用 yield 的时候, 张三李四大概五五开
  2. 使用 yield 时, 张三的数量远远少于李四

你可能感兴趣的:(javaEE,java-ee,java)