0%

多线程基础-1

多线程基础

★★★第十七章 多线程基础

程序的进程和线程

相关概念
  • 程序(Program):是为了完成特定任务,用某种语言编写的一组指令的集合。是静态的概念。
  • 进程(Process):运行中的程序。进程是程序的一次执行过程,或是正在运行的一个程序,是动态过程:有它自身的产生、存在和消亡的过程。是动态的概念。
  • 线程(Thread):线程是由进程创建的,是进程的一个实体。一个进程可以拥有多个线程。
    • 单线程:同一个时刻,只允许执行一个线程。
    • 多线程:同一个时刻,可以执行多个线程。比如:一个QQ进程,同时打开多个聊天窗口;一个迅雷进程,同时下载多个文件。
  • 并发:在同一个时间段,多个任务交替执行,造成一种“貌似同时执行”的错觉,简单的说,单核 cpu 实现的多任务就是并发。
  • 并行:同一个时刻,多个任务同时执行。多核 cpu 可以实现并行。

★★★线程基本使用

  • 创建线程的两种方式:
    1. 继承 Thread 类,重写 run 方法。
    2. 实现 Runnable 接口,重写 run 方法。
1.继承Thread创建线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.f.chapter17.threaduse;

/**
* @author fzy
* @date 2023/7/1 20:44
*/
public class ThreadUse {
public static void main(String[] args) {
Cat cat = new Cat();
cat.start(); //start用于启动线程 -> 最终会执行cat的run方法
//当main线程启动一个子线程 Thread-0 后,主线程(main线程)不会阻塞,会继续执行
for (int i = 0; i < 60; i++) {
System.out.println("主线程:" + Thread.currentThread().getName() + "。 i的值为" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

/*
* 1. 当一个类继承了Thread类,该类就可以当作线程使用。
* 2. 我们会重写run方法,写上自己的业务代码。
* 3. Thread 类的 run 方法是实现了 Runnable 接口的 run 方法。
* */
class Cat extends Thread {
@Override
public void run() { //重写run方法,实现自己的业务逻辑
int i = 1;
//该线程每隔1秒,在控制台输出“喵喵...”
while (true) {
i++;
System.out.println("喵喵... 线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//输出80次后,就退出while,同时线程也就退出了
if (i == 80) {
break;
}
}
}
}
  • 输出结果为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    主线程:main。 i的值为0
    喵喵... 线程名:Thread-0
    喵喵... 线程名:Thread-0
    主线程:main。 i的值为1
    喵喵... 线程名:Thread-0
    主线程:main。 i的值为2
    喵喵... 线程名:Thread-0
    主线程:main。 i的值为3
    主线程:main。 i的值为4
    喵喵... 线程名:Thread-0
    喵喵... 线程名:Thread-0
    主线程:main。 i的值为5
    喵喵... 线程名:Thread-0
    主线程:main。 i的值为6
    喵喵... 线程名:Thread-0
    主线程:main。 i的值为7
    喵喵... 线程名:Thread-0
    主线程:main。 i的值为8
    ......
★★★多线程机制
  • 以上面的代码为例讲解多线程机制。

    1. 首先,当进程 ThreadUse 开始运行后,其会启动主线程(即 main线程)。

    2. 由于 Cat 类继承了 Thread 类,所以该类就可以当作线程使用。于是 cat.start(); 就相当于 main线程又启动了一个子线程 Thread-0 ,并且需要注意:当 main 线程启动一个子线程 Thread-0 后,主线程(main线程)不会阻塞,会继续执行。

    3. 然后 main 线程和 Thread-0 线程同时执行,可以用下面的图来表示 进程 ThreadUse、main 线程和 Thread-0 线程的关系:

    4. 当 main 线程输出完毕后,main 线程退出,但此时 Thread-0 线程还在执行(因为main线程被设置为输出60次,Thread-0被设置为输出80次,所以 main线程执行完毕后,Thread-0线程还在执行),所以进程 ThreadUse 还在运行,并没有退出。

    5. 直到 Thread-0 线程输出完毕退出后,进程 ThreadUse 才真正执行结束并退出。

  • 注意:

    1. main 线程在启动了子线程 Thread-0 后,它自己并不会阻塞,会继续执行。
    2. main 线程在执行结束退出后,它所启动的子线程有可能还在继续执行,没有退出,则这些线程所在的进程就会继续运行,没有退出。
    3. main 线程在启动了子线程 Thread-0 后,依然可以启动其他子线程。同时,子线程也可以继续启动其他子线程,即不仅只有 main 线程才能启动子线程。
为什么是start而不是run
  • 如果在之前的代码的 main 方法中,直接调用 cat.run(); 方法而不是使用 cat.start(); 方法,就相当于没有创建子线程,而是 main 方法直接调用 cat.run() 方法,于是输出结果如下:

    1
    2
    3
    4
    5
    6
    喵喵... 线程名:main
    喵喵... 线程名:main
    喵喵... 线程名:main
    喵喵... 线程名:main
    喵喵... 线程名:main
    ......

    就变成了串行化的执行,即先把 cat.run() 方法执行完,再执行 main 中的输出的方法。

    同时也可以看到,输出由 喵喵... 线程名:Thread-0 变为 喵喵... 线程名:main。

  • 我们来看 start 的源码:

    1. 首先会进入 Thread 类的 start 方法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public synchronized void start() {
      ...
      group.add(this);

      boolean started = false;
      try {
      start0();
      started = true;
      } finally {
      try {
      if (!started) {
      group.threadStartFailed(this);
      }
      } catch (Throwable ignore) {
      /* do nothing. If start0 threw a Throwable then
      it will be passed up the call stack */
      }
      }
      }
    2. 然后执行 Thread 类的 start0 方法:

      1
      private native void start0();

      这是一个 native 方法,由 JVM 来调用,底层是 C/C++。

    3. 真正实现多线程效果的,是 start0 方法。

      ![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/Java-notebook/img/Java/C17-2.jpg)

2.Runnable创建线程
  • 前面我们用类继承 Thread 类来创建线程。但是 Java 是单继承的,在某些情况下一个类可能已经继承了某个父类,这时再用继承 Thread 类的方法来创建线程显然不可能了。

    所以我们可以通过实现 Runnable 接口来创建线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package com.f.chapter17.threaduse;

    /**
    * @author fzy
    * @date 2023/7/1 21:55
    * 通过实现接口Runnable来创建线程
    */
    public class ThreadUse02 {
    public static void main(String[] args) {
    Dog dog = new Dog();
    //dog.start(); //由于Runnable没有start方法,所以不能这么做
    //使用代理模式
    Thread dogThread = new Thread(dog);
    dogThread.start();
    }
    }

    class Dog implements Runnable {

    int cnt = 0;

    @Override
    public void run() {
    while (true) {
    cnt++;
    System.out.println("小狗汪汪叫... " + "线程为:" + Thread.currentThread().getName() + " cnt为:" + cnt);
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    if (cnt == 10) {
    break;
    }
    }
    }
    }
    • 在使用 “实现 Runnable 接口,重写 run 方法” 来创建线程时,由于 Runnable 接口并没有 start 方法,所以我们无法使用 dog.start(); 来创建子线程。

      为此,我们用代理模式来使得能够创建子线程。具体操作为:

      1. 将 dog 对象赋给 Thread 类的 target。
      2. 通过 Thread 对象的 start 方法,来调用 start0 方法以创建线程。
      3. 该创建的线程发现 target 非空,所以会调用该 target 的 run 方法,即 dog 对象的 run 方法,由此实现了线程的创建。

      即,将 Thread 对象作为 Dog 对象的代理,去帮助 Dog 对象创建线程并调用 Dog 对象的 run 方法,使用到了静态代理模式。

Thread和Runnable的区别
  1. 从 Java 的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程本质上没有区别,从 jdk 帮助文档我们可以看到 Thread 类本身就实现了 Runnable 接口。

  2. 实现 Runnable 接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。

    • 如下面代码所示,T 是实现了 Runnable 接口的类。

      1
      2
      3
      4
      5
      6
      7
      T t = new T("hello");
      Thread thread01 = new Thread(t);
      Thread thread02 = new Thread(t);
      thread01.start();
      thread02.start();

      System.out.println("主线程执行完毕");

      我们既用对象 t 创建了线程 thread01,又用对象 t 创建了线程 thread02,这两个线程调用的都是 t 的 run 方法,由此实现了多个线程共享一个资源的情况,即**这两个线程处理的都是同一个对象 t **。

线程终止

  1. 当线程完成任务后,会自动退出。

  2. 通过使用变量来控制 run 方法退出的方式停止线程,即通知方式。

    关键在于 main 线程能够控制 t 线程中的通知变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    package com.f.chapter17.threadexit;

    /**
    * @author fzy
    * @date 2023/7/2 10:59
    */
    public class ThreadExit {
    public static void main(String[] args) {
    T t = new T();
    Thread thread = new Thread(t);
    thread.start();

    System.out.println("主线程休眠10秒...");
    try {
    Thread.sleep(10 * 1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    //main线程通知t线程退出
    t.setLoop(false);
    }
    }

    class T implements Runnable {

    private int cnt = 0;

    //设置一个控制变量
    private boolean loop = true;

    @Override
    public void run() {
    while (loop) {
    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    System.out.println("线程还在运行中... " + (++cnt));
    }
    }

    public void setLoop(boolean loop) {
    this.loop = loop;
    }
    }

★线程常用方法

  • 第一组方法:

    1. setName:设置线程名称,使之与参数 name 相同。
    2. getName:返回该线程的名称。
    3. start:使该线程开始执行,Java 虚拟机底层会调用该线程的 start0 方法。
    4. run:调用线程对象的 run 方法。
    5. setPriority:更改线程的优先级。
    6. getPriority:获取线程的优先级。
    7. sleep:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
    8. interrupt:中断线程。中断线程不是终止线程。
    • 注意:
      1. start 底层会创建新的线程,去调用 run,run 就是一个简单的方法调用,只用 run 方法并不会启动新线程(在 “为什么是start而不是run” 小节已经说过)。
      2. 线程优先级的范围:
        • MAX_PRIORITY:10
        • MIN_PRIORITY:1
        • NORM_PRIORITY:5
      3. interrupt,中断线程,但并没有真正的结束线程,一般用于中断正在休眠的线程。
      4. sleep,线程的静态方法,使当前线程休眠。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    package com.f.chapter17.threadmethod;

    /**
    * @author fzy
    * @date 2023/7/2 11:16
    */
    public class ThreadMethod {
    public static void main(String[] args) {
    T t = new T();
    Thread thread = new Thread(t);
    thread.setName("自定义的线程名:t");
    thread.setPriority(Thread.MAX_PRIORITY);
    System.out.println(thread.getName() + "的优先级为:" + thread.getPriority());
    thread.start();

    for (int i = 0; i < 5; i++) {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    System.out.println("main线程 - " + i);
    }
    //main线程对t线程发出中断
    thread.interrupt();
    }
    }

    class T implements Runnable {
    /*
    * 下面的run方法的逻辑为:
    * 1.先输出十次 Thread.currentThread().getName() + " - " + i
    * 2.然后线程开始休眠20秒
    * 3.如果线程被中断,就会退出休眠状态
    * 4.然后继续下一个循环,即重复1、2、3的步骤(这里就说明中断线程不是终止线程,因为线程被中断后又开始下一个循环)
    * */
    @Override
    public void run() {
    while (true) {
    for (int i = 0; i < 10; i++) {
    System.out.println(Thread.currentThread().getName() + " - " + i);
    }
    try {
    System.out.println("开始休眠 20 秒...");
    Thread.sleep(20 * 1000);
    } catch (InterruptedException e) {
    //当该线程执行到一个 interrupt 方法时,就会 catch 一个异常,然后在 catch 块可以加入自己的业务代码
    //InterruptedException 是捕获到一个中断异常
    System.out.println(Thread.currentThread().getName() + "的休眠被 interrupt 了。");
    }
    }
    }
    }

    输出结果为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    自定义的线程名:t的优先级为:10
    自定义的线程名:t - 0
    自定义的线程名:t - 1
    自定义的线程名:t - 2
    自定义的线程名:t - 3
    自定义的线程名:t - 4
    自定义的线程名:t - 5
    自定义的线程名:t - 6
    自定义的线程名:t - 7
    自定义的线程名:t - 8
    自定义的线程名:t - 9
    开始休眠 20 秒...
    main线程 - 0
    main线程 - 1
    main线程 - 2
    main线程 - 3
    main线程 - 4
    自定义的线程名:t的休眠被 interrupt 了。
    自定义的线程名:t - 0
    自定义的线程名:t - 1
    自定义的线程名:t - 2
    自定义的线程名:t - 3
    自定义的线程名:t - 4
    自定义的线程名:t - 5
    自定义的线程名:t - 6
    自定义的线程名:t - 7
    自定义的线程名:t - 8
    自定义的线程名:t - 9
    开始休眠 20 秒...
    自定义的线程名:t - 0
    自定义的线程名:t - 1
    自定义的线程名:t - 2
    自定义的线程名:t - 3
    自定义的线程名:t - 4
    自定义的线程名:t - 5
    ......
  • 第二组方法:

    1. Thread.yield():线程的礼让。调用该方法的线程会让出 cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功,受内核态的 CPU 资源的影响。

    2. join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      package com.f.chapter17.threadmethod;

      /**
      * @author fzy
      * @date 2023/7/2 21:40
      * 案例:main线程创建一个子线程,每隔1s输出hello,输出20次,
      * 主线程每隔1秒,输出hi,输出20次。
      * 要求:两个线程同时执行,当主线程输出5次后,就让子线程运行完毕,主线程再继续输出。
      */
      public class ThreadMethod02 {
      public static void main(String[] args) {
      PrintHello printHello = new PrintHello();
      Thread thread = new Thread(printHello);
      thread.start();
      for (int i = 0; i < 20; i++) {
      System.out.println("hi " + i);
      try {
      Thread.sleep(1000);
      } catch (InterruptedException e) {
      throw new RuntimeException(e);
      }
      if (i == 5) { //当主线程输出5次后,就让子线程运行完毕,主线程再继续输出
      try {
      thread.join();
      } catch (InterruptedException e) {
      throw new RuntimeException(e);
      }
      }
      }
      }
      }

      class PrintHello implements Runnable {

      @Override
      public void run() {
      for (int i = 0; i < 20; i++) {
      System.out.println("hello " + i);
      try {
      Thread.sleep(1000);
      } catch (InterruptedException e) {
      throw new RuntimeException(e);
      }
      }
      }
      }
---------------The End---------------