在开始多线程之前,首先要知道,程序、进程和线程这三者之间的关系。

程序:为了完成某特定的任务,用某种语言编写的一组指令的集合。也就是我们所编写的一段静态的代码。 进程:指的是一个正在运行的程序,资源分配的单位 线程

  • 是程序内部的一条执行路径
  • 一个程序可以支持多个线程并行运行
  • 线程有自己的生命周期
  • 每个线程都有自己独立的运行栈和程序计数器
  • 线程共享相同的内存单元,可以访问相同的变量和对象。
    • 缺点:会操作共享数据,可能存在线程安全的问题
    • 优点:线程之间的通信更加的方便

并行和并发也是两个常见的概念,那么这两个具体的含义是什么呢

并行:真正意义上的多个CPU实例或者多台机器同时去执行一段逻辑代码,是真正意义上的同时 并发:是通过CPU调度算法实现了一种伪并行,并不是真正的同时


1、线程的创建方式

1.1 继承Thread类

流程

  • 创建一个继承于Thread类的子类
  • 重写Thread类的run方法
  • 创建子类对象
  • 通过此对象调用start方法
    顺序和注意点:
    • 启动当前线程
    • 调用线程的run方法
      • 不可以直接调用run(相当于直接调用了一个对象的方法,还是在主线程中执行的)
    • 一个线程的start方法只可以调用一次
      • 线程执行完run方法后,就会进入死亡状态

🎶多线程 - 图1
Thread类中的常用方法

  • start() 启动当前线程,调用当前线程的run() 方法
  • run()
  • currentThread() 获取执行当前代码的线程
  • getName() 获取当前线程的名字
  • setName() 设置当前线程的名字
  • yield() 释放当前CPU的执行权
  • join()
    • 作用:线程a中调用线程b的join方法,那么线程a就会进入阻塞状态,当线程b完全执行完之后,线程a才会结束阻塞状态
      • 有时某个线程运行过程中需要的资源来自于另外一个线程,那么这个时候我们就需要先暂停该线程,等资源全部获取到了之后再继续执行此线程
  • stop() 强制结束此线程
  • sleep() 挂起(睡眠)当前线程,指定时间内该线程处于阻塞状态

举例:三个窗口同时卖票
🎶多线程 - 图2
输出结果:
🎶多线程 - 图3

  • 实际上,如果不进行处理,就会出现三个窗口类似这种卖掉同一张票的情况,这就是后面会提到的线程安全问题
  • 所有的窗口需要共用一份票数,也就是共用同一份数据

    1.2 实现Runnable接口

    流程
  1. 创建一个类MyThread实现Runnable接口
  2. 实现Runnable接口中的抽象方法run
  3. 创建MyThread类的对象myThread1
  4. 创建Thread类的对象thread1,并将myThread1作为构造器的参数

举例:窗口卖票问题
使用Runnable接口来实现多线程的时候,票数可以不用静态变量来存储,因为三个线程都是由同一个对象生成的,那么自然共用同一份票数。
🎶多线程 - 图4

两种创建方式的比较 优先选择实现Runnable接口的方式,更方便实现对数据的共享。
两种方式的联系Thread类本身就实现了Runnable接口
image.png

1.3 实现Callable接口

创建方法

  1. 创建一个实现了Callable接口的类
  2. 在实现类中重写call方法,将此线程需要实现的操作声明在call方法中。 (必要的话可以通过泛型执行返回值的类型)
  3. 创建实现类的对象
  4. 将创建好的对象传递给FutureTask的构造器中,创建FutureTask类型的对象
  5. FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类型的对象,调用其start方法来开启线程
  6. 通过FutureTask对象的get方法,来获取Callablecall方法的返回值

示例:
MyThread.java

  1. package test9;
  2. import java.util.concurrent.Callable;
  3. /**
  4. * Created By Intellij IDEA
  5. *
  6. * @author Xinrui Yu
  7. * @date 2021/12/21 15:59 星期二
  8. */
  9. public class MyThread implements Callable<Integer> {
  10. @Override
  11. public Integer call() throws Exception {
  12. int sum = 0;
  13. for(int i = 1;i <= 100;i++){
  14. sum += i;
  15. }
  16. return sum;
  17. }
  18. }

App.java

  1. package test9;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.FutureTask;
  4. /**
  5. * Created By Intellij IDEA
  6. *
  7. * @author Xinrui Yu
  8. * @date 2021/12/21 15:58 星期二
  9. */
  10. public class App {
  11. public static void main(String[] args) {
  12. MyThread myThread = new MyThread();
  13. FutureTask<Integer> futureTask = new FutureTask<>(myThread);
  14. Thread thread = new Thread(futureTask);
  15. Integer result = null;
  16. thread.start();
  17. try {
  18. result = futureTask.get();
  19. } catch (InterruptedException | ExecutionException e) {
  20. e.printStackTrace();
  21. }
  22. if(result != null){
  23. System.out.println(result);
  24. }
  25. }
  26. }

为什么说实现Callable方法来创建线程比实现Runnable接口创建多线程更加强大?

  1. call()可以有返回值
  2. call()可以抛出异常,从而被外部进行捕获,进行对应的处理
    1. 在启动线程的start()处进行try-catch捕获处理
      image.png
  3. Callable接口支持泛型

    1.4 使用线程池

    好处

  4. 提高程序的响应速度,减少了新线程的创建时间。

  5. 降低资源消耗(线程池中的线程可以进行重复的利用,而不需要每次都重新进行创建)
  6. 便于对线程的统一集中管理。
    1. corePoolSize:核心池的大小
    2. maximumPoolSize:最大线程数
    3. keepAliveTime:线程没有任务的时候多长时间后会终止

创建线程池示例

  1. package test12;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. /**
  5. * Created By Intellij IDEA
  6. *
  7. * @author Xinrui Yu
  8. * @date 2021/11/30 13:27 星期二
  9. */
  10. public class Application {
  11. public static void main(String[] args) {
  12. // 1、提供指定线程数量的线程池
  13. ExecutorService service = Executors.newFixedThreadPool(10);
  14. //2、执行指定的线程的操作,需要提供实现了Runnable接口或者Callable接口的对象
  15. service.execute(new CountNumber1());
  16. service.execute(new CountNumber2());
  17. // service.submit(Callable callable); 适合使用于实现了Callable接口的对象
  18. //3、关闭线程池
  19. service.shutdown();
  20. }
  21. }
  22. class CountNumber1 implements Runnable{
  23. @Override
  24. public void run() {
  25. for (int i = 0; i < 100; i++) {
  26. if(i % 2 == 1){
  27. System.out.println(Thread.currentThread().getName() + ":" + i);
  28. }
  29. }
  30. }
  31. }
  32. class CountNumber2 implements Runnable{
  33. @Override
  34. public void run() {
  35. for (int i = 0; i <= 100; i++) {
  36. if(i % 2 == 0){
  37. System.out.println(Thread.currentThread().getName() + ":" + i);
  38. }
  39. }
  40. }
  41. }

2、线程安全问题

线程安全 - 掘金
线程安全问题在我们的日常生活中也有具体的体现,比如买票的过程中一定概率会出现重复票、错误票的现象,这种现象出现的根本原因在于,某一个线程在操作车票的过程中,在操作尚未全部完成的时候,由于CPU的调度,使得其他的线程对共享数据进行了操作,导致买票出错。那么我们接下来讨论线程安全的相关问题。


上述问题的解决方案:当线程A在操作票数的时候,其他的线程不可以参与进来,直到线程A操作完,其他的线程才能开始操作票数。在Java中,我们通过线程的同步机制,来解决线程的安全问题。但是这种方式也有一定的局限性:我们在操作同步代码的时候,只能有一个线程参与,其他的线程都只能处于等待状态,相当于只有一个线程,导致程序的效率比较低下。

处理方法

JDK5之前

在JDK5之前,我们可以使用同步代码块或者同步方法的方式来实现线程之间的同步。
同步在Java中的关键字是synchronized
同步监视器:也就是的概念。事实上,任何一个类的对象都可以充当锁来使用。要实现线程的同步,那么多个线程之间必须共用同一把锁。

  • 同步代码块 将多个线程操作共享数据的部分代码使用同步代码块的方式包裹起来。
    • 格式:synchronized(锁){...}
    • 注意:多个线程使用的锁一定要是相同的!!!

🎶多线程 - 图7

  • 同步方法 如果操作共享数据的代码是可以写成完整的一个方法,那么我们不妨把这个方法声明为同步的
    • 注意:同步方法中的同步监视器是this。使用过程中要注意锁的共享问题

🎶多线程 - 图8


JDK5之后

JDK5之后提供了Lock接口,来实现对线程的同步。
创建实现了Lock接口的ReentrantLock对象
🎶多线程 - 图9

优点:对线程同步的操作更加灵活

示例:两个用户共用同一个银行账户,每个人分三次向账户里面存钱,每次存1000元,判断是否会有线程安全的问题,如果有线程安全的问题,那么请对此问题进行处理。
🎶多线程 - 图10

3、线程的死锁

死锁的原因:两个或多个线程都在等待对方放弃自己所需要的同步资源,就形成了线程的死锁。出现了死锁之后,程序不会出现异常也不会提示,但是所有的线程都处于阻塞的状态,不会进行下去。
🎶多线程 - 图11
🎶多线程 - 图12

4、线程之间的通信

线程之间的通信主要涉及到这三个方法wait()notify()notifyAll()

  • wait() 一旦执行此方法,当前线程就会立即进入阻塞状态,并且释放同步监视器
    sleep和wait的相同点和不同点
    • 相同点
      • 一旦执行这两个方法,那么线程就会进入到阻塞状态
    • 不同点
      • 两个方法的定义位置不同
        • sleep定义在Thread类中
        • wait定义在Object类中
      • 调用的要求不同
        • wait()只能用在同步代码块中
        • sleep() 可以在任何需要的场景下使用
      • 是否会释放同步监视器
        • sleep不会释放
        • wait会释放
  • notify() 执行此方法之后,就会唤醒被wait的一个线程。如果当前有多个线程在wait,那么就会唤醒优先级高的那个线程
  • notifyAll() 执行此方法可以唤醒所有被wait的线程

注意点:

  • 上述三个方法都必须使用在同步代码块 / 同步方法中
  • 上述三个方法的调用者都是同步代码块 / 同步方法中的同步监视器(锁),否则会出现异常
    🎶多线程 - 图13
  • 上述三个方法都是定义在Object类下的,进一步验证了任何一个对象都可以充当同步监视器(锁)