# 第 10 章:随堂复习与企业真题(多线程)


# 一、随堂复习

# 1. 几个概念

程序(program):为完成特定任务,用某种语言编写的`一组指令的集合`。即指一段静态的代码。

进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。
              进程作为操作系统调度和分配资源的最小单位。

线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。
             线程作为CPU调度和执行的最小单位
线程调度策略
分时调度:所有线程`轮流使用` CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。

抢占式调度:让`优先级高`的线程以`较大的概率`优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
> 单核CPU与多核CPU
> 并行与并发

# 2. 如何创建多线程(重点)

  • 方式 1:继承 Thread 类
  • 方式 2:实现 Runnable 接口
  • 方式 3:实现 Callable 接口 (jdk5.0 新增)
  • 方式 4:使用线程池(jdk5.0 新增)

# 3. Thread 类的常用方法、线程的生命周期

熟悉常用的构造器和方法:
1. 线程中的构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

2.线程中的常用方法:
> start():①启动线程 ②调用线程的run()
> run():将线程要执行的操作,声明在run()中。
> currentThread():获取当前执行代码对应的线程
> getName(): 获取线程名
> setName(): 设置线程名
> sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
> yield():静态方法,一旦执行此方法,就释放CPU的执行权
> join(): 在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。
> isAlive():判断当前线程是否存活

3. 线程的优先级:
getPriority():获取线程的优先级
setPriority():设置线程的优先级。范围[1,10]


Thread类内部声明的三个常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

线程的生命周期:

jdk5.0 之前:

image-20221203142900528

jdk5.0 及之后:Thread 类中定义了一个内部类 State

public enum State {
        
        NEW,
    
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
 
        TERMINATED;
}

image-20221203143046926

# 4. 如何解决线程安全问题 (重点、难点)

  • 什么是线程的安全问题?多个线程操作共享数据,就有可能出现安全问题。

  • 如何解决线程的安全问题?有几种方式?

    • 同步机制:① 同步代码块 ② 同步方法

      • 重点关注两个事:共享数据及操作共享数据的代码;同步监视器(保证唯一性
      在实现Runnable接口的方式中,同步监视器可以考虑使用:this。
      在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用:当前类.class。
      
      非静态的同步方法,默认同步监视器是this
      静态的同步方法,默认同步监视器是当前类本身。
      
    • jdk5.0 新增:Lock 接口及其实现类。(保证多个线程共用同一个 Lock 的实例)

# 5. 同步机制相关的问题

  • 懒汉式的线程安全的写法
  • 同步机制会带来的问题:死锁
    • 死锁产生的条件及规避方式

# 6. 线程间的通信

  • 在同步机制下,考虑线程间的通信

  • wait () 、notify () 、notifyAll () 都需要使用在同步代码块或同步方法

  • 高频笔试题:wait () /sleep ()

# 二、企业真题

# 2.1 线程概述

# 1. 什么是线程 (* 云网络)

  • 是进程内部的 一条执行路径
  • CPU 调度和执行的最小单位

# 2. 线程和进程有什么区别 (* 团、腾 *、* 云网络、神 ** 岳、言 * 有物、直 * 科技)

进程:对应一个运行中的程序。是操作系统调度和分配资源的最小单位

线程:是运行中的进程的一条或多条执行路径。是CPU调度和执行的最小单位

# 3. 多线程使用场景(嘉 * 医疗)

  • 手机 app 应用的图片的下载
  • 迅雷的下载
  • Tomcat 服务器上 web 应用,多个客户端发起请求,Tomcat 针对多个请求开辟多个线程处理

# 2.2 如何实现多线程

# 1. 如何在 Java 中出实现多线程?(阿 * 校招、当 * 置业、鸿 * 网络、奥 * 医药、* 科软、慧 *、上海驿 * 软件、海 * 科)

类似问题:
> 创建多线程用Runnable还是Thread(北京中*瑞飞)
> 多线程有几种实现方法,都是什么?(锐*(上海)企业管理咨询)

四种:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口
  • 线程池

# 2. Thread 类中的 start () 和 run () 有什么区别?(北京中油 **、爱 * 信、神 * 泰岳、直 * 科技,* 软国际,上海 * 学网络)

start():① 开启线程(状态由 NEW 到 RUNNABLE)② 调用线程的 run ()

# 3. 启动一个线程是用 run () 还是 start ()?(* 度)

start()

# 4. Java 中 Runnable 和 Callable 有什么不同?(平 * 金服、银 * 数据、好 * 在、亿 * 征信、花儿 ** 网络)

与 Runnable 接口相比, Callable 功能更强大些

  • call () 可以有返回值
  • call()可以抛出异常
  • 支持泛型参数

缺点:如果在主线程中需要获取分线程 call () 的返回值,则此时的主线程是阻塞状态的

# 5. 什么是线程池,为什么要使用它?(上海明 * 物联网科技)

此方式的好处:
> 提高了程序执行的效率。(因为线程已经提前创建好了)
> 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
> 可以设置相关的参数,对线程池中的线程的使用进行管理

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。

好处:

  • 提高响应速度(因为线程已经提前创建好了)

  • 降低资源消耗(因为执行完的线程并未销毁,可以继续执行其他任务)

  • 便于线程管理,相关参数如下:

    • corePoolSize :核心池的大小
    • maximumPoolSize :最大线程数
    • keepAliveTime :线程没有任务时最多保持多长时间后会终止

# 2.3 常用方法、生命周期

# 1. sleep () 和 yield () 区别?(神 * 泰岳)

sleep ():一旦调用,就进入TIMED_WAITING状态

yield ():释放 cpu 的执行权,仍处在RUNNABLE的状态

# 2. 线程创建的中的方法、属性情况?(招通 **、数 * 互融)

继承 Thread 类的方式:

  • 方法:
    • start ():启动线程,执行 run () 方法。
    • run ():定义线程的执行逻辑。
    • sleep ():让当前线程睡眠一段时间,单位是毫秒。
    • interrupt ():中断线程的执行。
    • join():等待该线程终止。
  • 属性:
    • name:线程的名称。
    • priority:线程的优先级,取值范围为 1~10,默认值为 5。
    • id:线程的唯一标识符,由 JVM 自动生成。

实现 Runnable 接口的方式:

  • 方法:
    • run ():定义线程的执行逻辑。
  • 属性:
    • 无。

# 3. 线程的生命周期?(中国 ** 电子商务中心、* 科软、慧 *)

NEW、RUNNABLE、TERMINATED、BLOCKED、WAITING、TIMED_WAITING

image-20230316182537207

# 4. 线程的基本状态以及状态之间的关系?(直 * 科技)

类似问题:
> 线程有哪些状态?如何让线程进入阻塞?(华*中*,*兴)
> 线程有几个状态,就绪和阻塞有什么不同。(美*)
> Java的线程都有哪几种状态(字*跳动、*东、*手)

见上一题。

# 5. stop () 和 suspend () 方法为何不推荐使用?(上海驿 * 软件)

stop ():一旦执行,线程就结束了,导致run () 有未执行完毕的代码。stop()会释放同步监视器,导致线程安全问题

suspend ():与 resume () 搭配使用,会导致死锁

# 6. Java 线程优先级是怎么定义的?(软 * 动力)

三个常量:MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10)。

范围:[1,10]。

# 2.4 线程安全与同步机制

# 1. 你如何理解线程安全的?线程安全问题是如何造成的?(* 软国际)

类似问题:
> 线程安全说一下?(奥*医药)
> 对线程安全的理解(*度校招)
> 什么是线程安全?(银*数据)

线程安全问题通常是由于多个线程同时对共享的数据进行读操作而引起的

# 2. 多线程共用一个数据变量需要注意什么?(史 * 夫软件)

线程安全问题

# 3. 多线程保证线程安全一般有几种方式?(来 * 科技、北京 * 信天 *)

类似问题:
> 如何解决其线程安全问题,并且说明为什么这样子去解决?(北京联合**)
> 请说出你所知道的线程同步的方法。(天*伟业)
> 哪些方法实现线程安全?(阿*)   
> 同步有几种实现方法,都是什么? (锐*企业管理咨询)
> 你在实际编码过程中如何避免线程安全问题?(*软国际)
> 如何让线程同步?(*手)
> 多线程下有什么同步措施(阿*校招)
> 同步有几种实现方法,都是什么?(海*科)
  • 同步机制

    • 同步代码块
    • 同步方法
  • Lock 接口

# 4. 用什么关键字修饰同步方法?(上海驿 * 软件)

synchronized

# 5. synchronized 加在静态方法和普通方法区别(来 * 科技)

synchronized 声明在方法上时,同步监视器默认为:

  • 静态的:当前类.class
  • 非静态的:this

# 6. Java 中 synchronized 和 ReentrantLock 有什么不同 (三 * 重工)

类似问题:
> 多线程安全机制中 synchronized和lock的区别(中*国际、*美、鸿*网络)
> 怎么实现线程安全,各个实现方法有什么区别?(美*、字*跳动)
> synchronized 和 lock 区别(阿*、*壳)
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
Lock是通过两个方法控制需要被同步的代码,更灵活一些。
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。

synchronized 与 Lock 的对比

  1. Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用域、遇到异常等自动解锁
  2. Lock 只有代码块锁synchronized 有代码块锁和方法锁
  3. 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。
  4. (了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
  5. (了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以

说明:开发建议中处理线程安全问题优先使用顺序为:

・Lock ----> 同步代码块 ----> 同步方法

# 7. 当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法?(鸿 * 网络)

需要看其他方法是否使用 synchronized 修饰,同步监视器的 this 是否是同一个。

只有使用了 synchronized,且同步监视器是同一个的情况下,就不能访问了

# 8. 线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?(阿 * 校招、西安 * 创佳 *)

同步一定阻塞;

互斥是同步的保证,互斥了一定会阻塞

阻塞不一定同步。

Thread.sleep () 的调用也会阻塞,但不一定同步

# 2.5 死锁

# 1. 什么是死锁,产生死锁的原因及必要条件(腾 *、阿 *)

什么是死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

死锁的原因:

  • 互斥条件

    同步机制的目的就是为了实现互斥

  • 占用且等待

  • 不可抢占

  • 循环等待

如何避免死锁:可以考虑打破上面的诱发条件

  • 针对 “互斥条件”:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

  • 针对 “占用且等待”:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

  • 针对 “不可抢占”:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源

  • 针对 “循环等待”:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题

# 2. 如何避免死锁?(阿 *、北京 * 蓝、* 手)

见上。

# 2.6 线程通信

# 1. Java 中 notify () 和 notifyAll () 有什么区别 (汇 * 天下)

二者都是 Object 类中的方法,用于在多线程环境下进行线程间的通信。它们的区别在于:

  • notify ():会唤醒被 wait () 的线程中优先级最高的那一个线程(如果被 wait () 的多个线程的优先级相同,则随机唤醒一个),使其从等待状态进入到可运行状态,等待获取锁并从当初被 wait 的位置继续执行;
  • notifyAll ():会唤醒所有正在等待的线程,使它们从等待状态进入到可运行状态,等待获取锁并从当初被 wait 的位置继续执行;

# 2. 为什么 wait () 和 notify () 方法要在同步代码块 / 同步方法中调用 (北京 * 智)

因为 wait ()、notify () 的调用者必须是同步监视器

# 3. 多线程:生产者,消费者代码(同步、wait、notify 编程)(猫 * 娱乐)

类似问题:
> 如何写代码来解决生产者消费者问题(上海明*物联网)
> 多线程中生产者和消费者如何保证同步(*为)
> 消费者生产者,写写伪代码(字*)
/**
 * ClassName: ProducerConsumer
 * Package: threadcommunication
 * Description:
 * 案例:生产者 / 消费者问题
 * 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),
 * 如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;
 * 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
 * 分析:
 * 1. 是否是多线程问题?是,生产者线程,消费者线程;
 * 2. 是否有共享数据?是,店员(或产品);
 * 3. 是否有线程安全问题?是,店员(或产品);
 * 4. 如何解决线程安全问题?同步机制;
 * 5. 是否涉及线程的通信?是,体现在生产者和消费者之间;
 *
 * @Author 贺健翔
 * @Create 2023/3/16 15:06
 * @Version 1.0
 */
class Clerk { // 店员
    private int productCount = 0; // 产品数量
    // 生产产品
    public synchronized void addProduct() {
        if (productCount >= 20) {
            try {
                wait(); // 生产者线程进入 WAITING 状态,同时会释放同步监视器!
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            productCount++;
            System.out.println(Thread.currentThread().getName() + "生产了第" + productCount + "个产品");
            notifyAll(); // 唤醒被 wait () 的消费者线程
        }
    }
    // 消费产品
    public synchronized void minusProduct() {
        if (productCount <= 0) {
            try {
                wait(); // 消费者线程进入 WAITING 状态,同时会释放同步监视器!
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "消费了第" + productCount + "个产品");
            productCount--;
            notifyAll(); // 唤醒被 wait () 的生产者线程
        }
    }
}
class Producer extends Thread { // 生产者
    private Clerk clerk;
    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true) {
            System.out.println("生产者开始生产产品");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.clerk.addProduct();
        }
    }
}
class Consumer extends Thread { // 消费者
    private Clerk clerk;
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true) {
            System.out.println("消费者开始消费产品");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.clerk.minusProduct();
        }
    }
}
public class ProducerConsumer {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);
        producer.setName("生产者");
        consumer.setName("消费者");
        producer.start();
        consumer.start();
    }
}

# 4. wait () 和 sleep () 有什么区别?调用这两个函数后,线程状态分别作何改变?(字 *、* 东)

类似问题:
> 线程中sleep()和wait()有什么区别?(外派*度)
> Java线程阻塞调用 wait 函数和 sleep 区别和联系(阿*)
> wait和sleep的区别,他们两个谁会释放锁(软*动力、*创)

相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态(WAITING / TIMED_WAITING)

不同点:

不同点Thread.sleep()Object 类实例的 wait ()
所属的类Thread 类中的静态方法Object 类中的实例方法
适用范围任意同步代码块或同步方法中
是否释放同步监视器
结束方式指定时间一到就结束阻塞(TIMED_WAITING)可以指定时间(TIMED_WAITING),也可以无限等待(WAITING)直到 notify 或 notifyAll

# 2.7 单例模式(线程安全)

# 1. 手写一个单例模式 (Singleton),还要安全的(* 通快递、君 * 科技)

饿汉式:

// 饿汉式单例设计模式
class Bank {
    // 1. 私有化类的构造器
    private Bank() {
    }
    // 2. 内部创建类的实例对象
    // 4. 要求此对象属性也必须声明为 static 的
    private static Bank instance = new Bank();
    // 3. 通过 get 方法获取当前类的实例对象,必须声明为 static 的
    public static Bank getInstance() {
        return instance;
    }
}

安全的懒汉式:

class Bank {
    // 1. 私有化类的构造器
    private Bank() {
    }
    // 2. 内部创建类的对象
    // 4. 要求此对象也必须声明为静态的
    private static Bank instance = null;
    // 3. 提供公共的静态方法,返回类的对象
    // 欲解决线程安全问题,只需将此方法声明为同步的即可。因为是静态方法,所以同步监视器默认是当前类.class,是唯一的。
    public static synchronized Bank getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 此处存在线程安全问题:当有多个线程进入到此处时,有可能会创建多个对象
            instance = new Bank();
        }
        return instance;
    }
}

# 2. 手写一个懒汉式的单例模式 & 解决其线程安全问题,并且说明为什么这样子去解决(5*)

类似问题:
> 手写一个懒汉式的单例模式(北京联合**)

同上。