面试必背的Java基础:线程与线程池

文章目录

  • 基础概念
    • 1. 什么是进程,什么是线程,它们之间的区别是什么?
    • 2. 什么叫做线程调度,有哪些线程调度?
    • 3.什么叫做同步,什么叫做异步?
    • 4.什么叫做并发,什么叫做并行?
    • 5.线程的状态有哪些?
    • 6.线程死锁是什么?
  • 创建新线程
    • 1. 继承Thread类
    • 2. 实现Runable接口
    • 3. 实现Callable接口
    • 4. 实现Runable接口和 继承Thread类相比的优势
    • 5. 实现Runable接口和 实现Callable接口的区别
  • 同步锁
    • 1. 同步代码块
    • 2. 同步方法
    • 3. 显式锁
    • 4. 显式锁的优势
    • 5. 公平锁和不公平锁
  • 线程池
    • Java中的四种线程池

基础概念

1. 什么是进程,什么是线程,它们之间的区别是什么?

  • 进程 - 进程是一个“执行中的程序”,是系统进行资源分配和调度的一个独立单位
  • 线程 - 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少 有一个线程, 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分 成若干个线程

区别:进程是 资源分配 的基本单位; 线程是 程序执行 的基本单位

2. 什么叫做线程调度,有哪些线程调度?

线程调度:指按照特定机制为多个线程分配CPU的使用权。

线程调度分类:

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

3.什么叫做同步,什么叫做异步?

  • 同步:排队执行 , 效率低但是安全
  • 异步:同时执行 , 效率高但是数据不安全

4.什么叫做并发,什么叫做并行?

  • 并发:指两个或多个事件在同一个时间段内发生
  • 并行:指两个或多个事件在同一时刻发生(同时发生)

5.线程的状态有哪些?

线程的状态包括 新建状态,运行状态,阻塞等待状态和消亡状态。其中阻塞等待状态又分为 BLOCKED, WAITING 和 TIMED_WAITING 状态。

  • NEW:这是属于一个已经创建的线程,但是还没有调用 start 方法启动的线程所处的状态。

  • RUNNABLE:总体上就是当我们创建线程并且启动之后,就属于 Runnable 状态。

  • BLOCKED:当线程准备进入 synchronized同步块或同步方法的时候,需要申请一个监视器锁而进行的等 待,会使线程进入BLOCKED 状态

  • WAITING :该状态的出现是因为调用了Object.wait() 或者 Thread.join()或者LockSupport.park ()。处于该状态下的线程在等待另一个线程执行一些其余 action 来将其唤醒。

  • TIMED_WAITING 该状态和上一个状态其实是一样的,是不过其等待的时间是明确的。

  • TERMINATED :消亡状态比较容易理解,那就是线程执行结束了,run() 方法执行结束表示线程处于消亡状态了。

6.线程死锁是什么?

死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于非 Runnable)。死锁的产生必须满足如下四个必要条件:

  • 资源互斥: 一个资源每次只能被一个线程使用
  • 请求与保持条件: 一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已经获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系

创建新线程

创建线程可分为四种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用线程池创建

下面重点介绍前两种:

1. 继承Thread类

首先创建一个类继承Thread类,重写run()方法,将所要完成的任务代码写进run()方法中

public class MyThread extends Thread{
     
    /**
     * run 方法就是线程要执行的方法
     */
    @Override
    public void run() {
     
        //这里的代码 就是一条新的执行路径
        //这个执行路径的触发方式,不是调用run方法,而是通过thread对象的start()来启动任务
        for(int i=0;i<10;i++){
     
            System.out.println("MyThread" + i);
        }
    }
}

然后通过thread对象的start()来启动任务

public class Demo {
     
    public static void main(String[] args) {
     
        MyThread m = new MyThread();
        m.start();
        for(int i=0; i<10; i++){
     
            System.out.println("MainThread"+i);
        }
    }
}

输出如下:

MyThread0
MyThread1
MainThread0
MyThread2
MainThread1
MyThread3
MainThread2
MyThread4
MainThread3
MyThread5
MainThread4
MyThread6
MainThread5
MyThread7
MainThread6
MyThread8
MainThread7
MyThread9
MainThread8
MainThread9

每次的输出不一样,因为Java采用的是抢占式调度,我们不能确保哪个线程优先完成。

2. 实现Runable接口

首先创建一个类并实现Runnable接口,重写run()方法,将所要完成的任务代码写进run()方法中

public class MyRunnable implements Runnable{
     

    @Override
    public void run() {
     
        //线程的任务
        for(int i=0;i<10;i++){
     
            System.out.println("MyThread" + i);
        }
    }
}

然后创建实现Runnable接口的类的对象,将该对象当做Thread类的构造方法中的参数传进去,使用Thread类的构造方法创建一个对象,并调用start()方法即可运行该线程

public class Demo {
     
    public static void main(String[] args) {
     
        //实现Runnable
        //1.    创建一个任务对象
        MyRunnable m = new MyRunnable();
        //2.    创建一个线程,并为其分配一个任务
        Thread t = new Thread(m);
        //3.    执行这个线程
        t.start();

        for(int i=0; i<10; i++){
     
            System.out.println("MainThread"+i);
        }
    }
}

输出如下:

MainThread0
MyThreadByRunnable0
MainThread1
MyThreadByRunnable1
MyThreadByRunnable2
MainThread2
MainThread3
MainThread4
MainThread5
MainThread6
MainThread7
MainThread8
MyThreadByRunnable3
MainThread9
MyThreadByRunnable4
MyThreadByRunnable5
MyThreadByRunnable6
MyThreadByRunnable7
MyThreadByRunnable8
MyThreadByRunnable9

3. 实现Callable接口

Callable使用步骤

  1. 编写类实现Callable接口 , 实现call方法 class XXX implements
	//编写类实现Callable接口 , 实现call方法 
	class XXX implements Callable<T> {
     
            @Override
            public <T> call() throws Exception {
     
return T; }
}
  1. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
  1. 通过Thread,启动线程
new Thread(future).start();

4. 实现Runable接口和 继承Thread类相比的优势

  • 通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程执行相同任务
  • 可以避免单继承所带来的局限性
  • 任务与线程本身是分离的,提高了程序的健壮性
  • 大部分线程池技术,接受Runnable类型的任务,不接受Thread类型的线程

5. 实现Runable接口和 实现Callable接口的区别

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

不同点:

  • Runnable没有返回值;Callable可以返回执行结果
  • Callable接口的call()允许抛出异常;Runnable的run()不能抛出

Callable获取返回值:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

同步锁

这部分为大家讲解synchronized隐式锁(包括同步代码块和同步方法),显式锁ReentrantLock。

隐式锁:具体锁的实现方法我们不关注,只需要写上相应的格式,Java会自动实现加锁和解锁。
显式锁:需要我们手动创建并且手动解锁。

我们一般来使用锁来解决线程不安全的问题。

线程不安全就是不提供加锁机制保护,也就是说多个线程同时访问一个数据,它们看到的数据和得到的数据不一致,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

1. 同步代码块

同步代码块就是在代码块加关键字synchronized,然后被同步的代码块一次只能有一个线程进入,同时锁对象打上标识,其他线程等待,当该线程完成代码块中的操作时,锁对象释放锁,其他线程再进行争抢,从而实现排队的操作。

格式:

synchronized(锁对象){
     
	//方法体
}

2. 同步方法

同步方法即有synchronized关键字修饰的方法。

由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

代码如:

   public synchronized void save(){
     }
   //当synchronized作用于普通方法是,锁对象是this;
   //当synchronized作用于静态方法是,锁对象是当前类的Class对象;

3. 显式锁

// 显式锁的使用示例
ReentrantLock lock = new ReentrantLock();

// 获取锁,这是跟synchronized关键字对应的用法。
lock.lock();
try{
     
    // your code
}finally{
     
    lock.unlock();
}

// 可定时,超过指定时间为得到锁就放弃
try {
     
    lock.tryLock(10, TimeUnit.SECONDS);
    try {
     
        // your code
    }finally {
     
        lock.unlock();
    }
} catch (InterruptedException e1) {
     
    // exception handling
}

// 可中断,等待获取锁的过程中线程线程可被中断
try {
     
    lock.lockInterruptibly();
    try {
     
        // your code
    }finally {
     
        lock.unlock();
    }
} catch (InterruptedException e) {
     
    // exception handling
}

我们需要将unlock()放在finally块里,因为显式锁不像隐式锁那样会自动释放,使用显式锁一定要在finally块中手动释放,如果获取锁后由于异常的原因没有释放锁,那么这把锁将永远得不到释放!将unlock()放在finally块中,保证无论发生什么都能够正常释放。

4. 显式锁的优势

  • 可给锁加个等待时间超时时间,超时还未获得锁就放弃,不至于无限等下去;
  • 可以中断的方式获取锁,这样外部线程给我们发一个中断信号就能唤起等待锁的线程;
  • 可为锁维持多个等待队列,比如一个生产者队列,一个消费者队列,一边提高锁的效率。

ReentrantLock的字面意思是可重入锁,可重入的意思是线程可以同时多次请求同一把锁,而不会自己导致自己死锁。下面是内置锁和显式锁的区别:

  1. 可定时:RenentrantLock.tryLock(long timeout, TimeUnit unit)提供了一种以定时结束等待的方式,如果线程在指定的时间内没有获得锁,该方法就会返回false并结束线程等待。

  2. 可中断:你一定见过InterruptedException,很多跟多线程相关的方法会抛出该异常,这个异常并不是一个缺陷导致的负担,而是一种必须,或者说是一件好事。可中断性给我们提供了一种让线程提前结束的方式(而不是非得等到线程执行结束),这对于要取消耗时的任务非常有用。对于内置锁,线程拿不到内置锁就会一直等待,除了获取锁没有其他办法能够让其结束等待。RenentrantLock.lockInterruptibly()给我们提供了一种以中断结束等待的方式。

  3. 条件队列(condition queue):线程在获取锁之后,可能会由于等待某个条件发生而进入等待状态(内置锁通过Object.wait()方法,显式锁通过Condition.await()方法),进入等待状态的线程会挂起并自动释放锁,这些线程会被放入到条件队列当中。synchronized对应的只有一个条件队列,而ReentrantLock可以有多个条件队列,多个队列有什么好处呢?请往下看。

  4. 条件谓词:线程在获取锁之后,有时候还需要等待某个条件满足才能做事情,比如生产者需要等到“缓存不满”才能往队列里放入消息,而消费者需要等到“缓存非空”才能从队列里取出消息。这些条件被称作条件谓词,线程需要先获取锁,然后判断条件谓词是否满足,如果不满足就不往下执行,相应的线程就会放弃执行权并自动释放锁。使用同一把锁的不同的线程可能有不同的条件谓词,如果只有一个条件队列,当某个条件谓词满足时就无法判断该唤醒条件队列里的哪一个线程;但是如果每个条件谓词都有一个单独的条件队列,当某个条件满足时我们就知道应该唤醒对应队列上的线程(内置锁通过Object.notify()或者Object.notifyAll()方法唤醒,显式锁通过Condition.signal()或者Condition.signalAll()方法唤醒)。这就是多个条件队列的好处。

  5. 使用内置锁时,对象本身既是一把锁又是一个条件队列;使用显式锁时,RenentrantLock的对象是锁,条件队列通过RenentrantLock.newCondition()方法获取,多次调用该方法可以得到多个条件队列。

显示锁的部分内容参考深入理解Java内置锁和显式锁

5. 公平锁和不公平锁

公平锁:所有线程排队取得锁,先来先得。
不公平锁:所有线程抢占锁,谁先抢到就是谁的。

隐式锁都是不公平锁,显式锁在默认情况下也是不公平锁,那我们如何创建一个公平锁:
代码示范:

//显式锁:fair参数为true 就表示公平锁。
private lock = new ReentrantLock(true);

线程池

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

线程池的优势体现如下:

  • 线程池可以重复利用已创建的线程,一次创建可以执行多次任务,有效 降低线程创建和销毁所造 成的资源消耗
  • 线程池技术使得请求可以 快速得到响应,节约了创建线程的时间
  • 线程的创建需要占用系统内存,消耗系统资源,使用线程池可以更好的管理线程,做到 统一分配、调优和监控线程,提高系统的稳定性。

Java中的四种线程池

- 缓存线程池 CachedThreadPool( )

执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在,则创建线程 并放入线程池, 然后使用

作用:

  1. 核心线程池大小为 0,最大线程池大小不受限,来一个创建一个线程
  2. 适合用来执行大量耗时较短且提交频率较高的任务 (短期异步,低负载的系统)

- 定长线程池 CachedThreadPool( )

执行流程:

  1. 判断线程池是否存在空闲线程,存在则使用
  2. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
  3. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

- 单线程线程池 CachedThreadPool( )

执行流程:

  1. 判断线程池 的那个线程 是否空闲
  2. 空闲则使用
  3. 不空闲,则等待 池中的单个线程空闲后 使用执行流程:

作用:便于实现单(多)生产者-消费者模式

- 周期性任务定长线程池CachedThreadPool( )
执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务

作用:定时使用的线程池,适用于定时任务。

你可能感兴趣的:(Java,#,JAVA,基础知识,java,面试,编程语言,多线程,队列)