Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。 这里定义和线程相关的另一个术语 进程:

  • 一个进程包括由操作系统分配的内存空间,包含一个或多个线程。
  • 一个线程不能独立的存在,它必须是进程的一部分。
  • 一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。 并发与并行 并发 指两个或多个事件在同一个时间段内发生 并行 指两个或多个事件在同一时刻发生(同时发生)

  • 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
  • 而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行, 即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率
  • 单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同 理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个 线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为 线程调度

在多线程编程时,你需要了解以下几个概念:

  • 线程同步
  • 线程间通信
  • 线程死锁
  • 线程控制:挂起、停止和恢复

线程与进程

进程

指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多 个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创 建、运行到消亡的过程
image.png

线程

线程是进程中的一个执行单元,负责当前进程中程序的执行

  • 一个进程中至少有一个线程
  • 一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序
  • 多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈

线程调度:

  • 分时调度 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
  • 抢占式调度 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为 抢占式调度
  • 主线程main和多线程的关系

主线程main和多线程的关系.png

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 Thread.MIN_PRIORITY(1)-Thread.MAX_PRIORITY(10)
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

一个线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期
Mxt6KCH5GQ-compress.jpg

  • 新建状态 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态

创建一个线程

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程

通过实现 Runnable 接口来创建线程

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体,声明如下:

    1. public void run()
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对

    1. Thread(Runnable threadOb,String threadName);
  3. 这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。

新线程创建之后,你调用它的 start() 方法它才会运行。

  1. void start();

下面是一个创建线程并开始让它执行的实例:

  1. class MyRunable implements Runnable {
  2. private Thread thread;
  3. private String threadName;
  4. MyRunable() {
  5. };
  6. MyRunable(String name) {
  7. threadName = name;
  8. }
  9. @Override
  10. public void run() {
  11. for (int i = 0; i < 1; i++) {
  12. System.out.println(Thread.currentThread().getName());
  13. try {
  14. Thread.sleep(100);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. public void start() {
  21. if (thread == null) {
  22. thread = new Thread(this, threadName);
  23. thread.start();
  24. }
  25. }
  26. }
  27. public class RunnableDemo {
  28. public static void main(String[] args) {
  29. // ---------------------------------------------------------------
  30. new Thread(new MyRunable(), "实现Runnable接口的线程").start();
  31. // ---------------------------------------------------------------
  32. new MyRunable("实现实现Runnable的类中调用Thread").start();
  33. // ---------------------------------------------------------------
  34. new Thread(new Runnable() {
  35. @Override
  36. public void run() {
  37. System.out.println(Thread.currentThread().getName());
  38. }
  39. }, "匿名内部类方式实现线程的创建").start();
  40. // ---------------------------------------------------------------
  41. new Thread(() -> {
  42. System.out.println(Thread.currentThread().getName());
  43. }, "用Lambda表达式实现Runnable接口的线程").start();
  44. // ---------------------------------------------------------------
  45. }
  46. }
  47. /*
  48. 实现Runnable接口的线程
  49. 实现实现Runnable的类中调用Thread
  50. 匿名内部类方式实现线程的创建
  51. 用Lambda表达式实现Runnable接口的线程
  52. */
  1. class RunnableDemo implements Runnable {
  2. private Thread thread;
  3. private String threadName;
  4. RunnableDemo(String name) {
  5. threadName = name;
  6. System.out.println("Creating " + threadName);
  7. }
  8. public void run() {
  9. System.out.println("Running " + threadName);
  10. try {
  11. for (int i = 4; i > 0; i--) {
  12. System.out.println("Thread: " + threadName + ", " + i);
  13. Thread.sleep(50); // 让线程睡眠一会
  14. }
  15. } catch (InterruptedException e) {
  16. System.out.println("Thread " + threadName + " interrupted.");
  17. }
  18. System.out.println("Thread " + threadName + " exiting.");
  19. }
  20. public void start() {
  21. System.out.println("Starting " + threadName);
  22. if (thread == null) {
  23. thread = new Thread(this, threadName);
  24. thread.start();
  25. }
  26. }
  27. }
  28. public class ThreadTestDemo {
  29. public static void main(String args[]) {
  30. RunnableDemo R1 = new RunnableDemo("Thread-1");
  31. R1.start();
  32. RunnableDemo R2 = new RunnableDemo("Thread-2");
  33. R2.start();
  34. }
  35. }
  36. /*
  37. 编译以上程序运行结果如下:
  38. Creating Thread-1
  39. Starting Thread-1
  40. Creating Thread-2
  41. Starting Thread-2
  42. Running Thread-1
  43. Thread: Thread-1, 4
  44. Running Thread-2
  45. Thread: Thread-2, 4
  46. Thread: Thread-1, 3
  47. Thread: Thread-2, 3
  48. Thread: Thread-1, 2
  49. Thread: Thread-2, 2
  50. Thread: Thread-1, 1
  51. Thread: Thread-2, 1
  52. Thread Thread-1 exiting.
  53. Thread Thread-2 exiting.
  54. */

通过继承Thread来创建线程

创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例
Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例

  • 继承类必须重写 run() 方法,该方法是新线程的入口点(线程执行体)
  • 它也必须调用 start() 方法才能执行
  • 创建Thread子类的实例,即创建了线程对象

该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

  1. class MyThread extends Thread {
  2. public MyThread(String name) { // 定义指定线程名称的构造方法
  3. super(name); // 调用父类的String参数的构造方法,指定线程的名称
  4. }
  5. // 重写run方法,完成该线程执行的逻辑
  6. @Override
  7. public void run() {
  8. for (int i = 0; i < 10; i++) {
  9. System.out.println(getName() + ":正在执行!" + i);
  10. }
  11. }
  12. }
  13. public class ThreadDemo {
  14. public static void main(String[] args) {
  15. MyThread mt = new MyThread("新的线程!"); // 创建自定义线程对象
  16. mt.start(); // 开启新线程
  17. for (int i = 0; i < 10; i++) { // 在主方法中执行for循环
  18. System.out.println("main线程!" + i);
  19. }
  20. }
  21. }
  22. /*
  23. main线程!0
  24. main线程!1
  25. 新的线程!:正在执行!0
  26. 新的线程!:正在执行!1
  27. 新的线程!:正在执行!2
  28. 新的线程!:正在执行!3
  29. 新的线程!:正在执行!4
  30. 新的线程!:正在执行!5
  31. main线程!2
  32. main线程!3
  33. 新的线程!:正在执行!6
  34. 新的线程!:正在执行!7
  35. 新的线程!:正在执行!8
  36. 新的线程!:正在执行!9
  37. main线程!4
  38. main线程!5
  39. main线程!6
  40. main线程!7
  41. main线程!8
  42. main线程!9
  43. */
  1. class ThreadDemo extends Thread {
  2. private Thread t;
  3. private String threadName;
  4. ThreadDemo(String name) {
  5. threadName = name;
  6. System.out.println("Creating " + threadName);
  7. }
  8. @Override
  9. public void run() {
  10. System.out.println("Running " + threadName);
  11. try {
  12. for (int i = 4; i > 0; i--) {
  13. System.out.println("Thread: " + threadName + ", " + i);
  14. // 让线程睡眠一会
  15. Thread.sleep(50);
  16. }
  17. } catch (InterruptedException e) {
  18. System.out.println("Thread " + threadName + " interrupted.");
  19. }
  20. System.out.println("Thread " + threadName + " exiting.");
  21. }
  22. public void start() {
  23. System.out.println("Starting " + threadName);
  24. if (t == null) {
  25. t = new Thread(this, threadName);
  26. t.start();
  27. }
  28. }
  29. }
  30. public class ThreadTestDemo {
  31. public static void main(String args[]) {
  32. ThreadDemo T1 = new ThreadDemo("Thread-1");
  33. T1.start();
  34. ThreadDemo T2 = new ThreadDemo("Thread-2");
  35. T2.start();
  36. }
  37. }
  38. // 编译以上程序运行结果如下:
  39. // Creating Thread-1
  40. // Starting Thread-1
  41. // Creating Thread-2
  42. // Starting Thread-2
  43. // Running Thread-1
  44. // Thread: Thread-1, 4
  45. // Running Thread-2
  46. // Thread: Thread-2, 4
  47. // Thread: Thread-1, 3
  48. // Thread: Thread-2, 3
  49. // Thread: Thread-1, 2
  50. // Thread: Thread-2, 2
  51. // Thread: Thread-1, 1
  52. // Thread: Thread-2, 1
  53. // Thread Thread-1 exiting.
  54. // Thread Thread-2 exiting.

Thread 方法

下表列出了Thread类的一些重要方法:

方法 描述
构造方法:
public Thread() : 分配一个新的线程对象。 程对象并指定名字
public Thread(String name) : 分配一个指定名字的新的线程对象
public Thread(Runnable target) : 分配一个带有指定目标新的线程对象
public Thread(Runnable target,String name) : 分配一个带有指定目标新的线
public void start() 使该线程开启
Java虚拟机调用该线程的 run 方法。
public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
public final void setName(String name) 改变线程名称,使之与参数 name 相同。
public final void setPriority(int priority) 更改线程的优先级。
public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
public void interrupt() 中断线程。
public final boolean isAlive() 测试线程是否处于活动状态。
public static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
public static Thread currentThread() 返回对当前正在执行的线程对象的引用

测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

序号 方法描述
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

如下的ThreadClassDemo 程序演示了Thread类的一些方法:

  1. // 通过实现 Runnable 接口创建线程
  2. class RunnableDisplayMessage implements Runnable {
  3. private String message;
  4. public RunnableDisplayMessage(String message) {
  5. this.message = message;
  6. }
  7. public void run() {
  8. while (true) {
  9. System.out.println(message);
  10. }
  11. }
  12. }
  13. // 通过继承 Thread 类创建线程
  14. class ThreadGuessANumber extends Thread {
  15. private int number;
  16. public ThreadGuessANumber(int number) {
  17. this.number = number;
  18. }
  19. public void run() {
  20. int counter = 0;
  21. int guess = 0;
  22. do {
  23. guess = (int) (Math.random() * 100 + 1);
  24. System.out.println(this.getName() + " guesses " + guess);
  25. counter++;
  26. } while (guess != number);
  27. System.out.println("** Correct!" + this.getName() + "in" + counter + "guesses.**");
  28. }
  29. }
  30. public class ThreadClassDemo {
  31. public static void main(String[] args) {
  32. Runnable hello = new RunnableDisplayMessage("Hello");
  33. Thread thread1 = new Thread(hello);
  34. thread1.setDaemon(true);
  35. thread1.setName("hello");
  36. System.out.println("Starting hello thread...");
  37. thread1.start();
  38. Runnable bye = new RunnableDisplayMessage("Goodbye");
  39. Thread thread2 = new Thread(bye);
  40. thread2.setPriority(Thread.MIN_PRIORITY);
  41. thread2.setDaemon(true);
  42. System.out.println("Starting goodbye thread...");
  43. thread2.start();
  44. System.out.println("Starting thread3...");
  45. Thread thread3 = new ThreadGuessANumber(27);
  46. thread3.start();
  47. try {
  48. thread3.join();
  49. } catch (InterruptedException e) {
  50. System.out.println("Thread interrupted.");
  51. }
  52. System.out.println("Starting thread4...");
  53. Thread thread4 = new ThreadGuessANumber(75);
  54. thread4.start();
  55. System.out.println("main() is ending...");
  56. }
  57. }
  58. /*
  59. 运行结果如下,每一次运行的结果都不一样。
  60. Starting hello thread...
  61. Starting goodbye thread...
  62. Hello
  63. Hello
  64. Hello
  65. Hello
  66. Hello
  67. Hello
  68. Goodbye
  69. Goodbye
  70. Goodbye
  71. Goodbye
  72. Goodbye
  73. .......
  74. */

join()

表示一个线程会无限等待至被等待线程死亡为止

  1. class ThreadA extends Thread {
  2. public void run() {
  3. synchronized (this) {
  4. try {
  5. System.out.println("睡眠5秒");
  6. Thread.sleep(5000);
  7. System.out.println("睡完了");
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. try {
  13. System.out.println("再睡眠5秒");
  14. Thread.sleep(5000);
  15. System.out.println("睡完了");
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  21. public class Test {
  22. public static void main(String[] args) throws InterruptedException {
  23. long start = System.currentTimeMillis();
  24. ThreadA a = new ThreadA();
  25. a.start();
  26. a.join(2000);
  27. long end = System.currentTimeMillis();
  28. System.out.println("共运行" + (end - start) / 1000 + "秒");
  29. }
  30. }

通过 Callable 和 Future 创建线程

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。 ```java import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;

class CallableThreadDemo implements Callable { @Override public Integer call() throws Exception { int i = 0; for (; i < 10; i++) { System.out.println(Thread.currentThread().getName() + “ “ + i); } return i; } }

public class CallableThreadDemotest { public static void main(String[] args) { CallableThreadDemo ctt = new CallableThreadDemo(); FutureTask ft = new FutureTask<>(ctt);

for (int i = 0; i < 20; i++) {
  System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
  if (i == 2) {
    new Thread(ft, "CallableThreadDemotest有返回值的线程:").start();
  }
}

try {
  System.out.println("子线程的返回值:" + ft.get());
} catch (InterruptedException e) {
  e.printStackTrace();
} catch (ExecutionException e) {
  e.printStackTrace();
}

} } ```

创建线程的三种方式的对比

实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去共享同一个资源
  • 可以避免java中的单继承的局限
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
  • 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类

采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程

  • 扩展:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程