每日一面系列之并发基础面试题

1.什么是进程?什么是线程?二者的关系和区别是什么?

进程是程序执行的一次过程,是系统运行程序的基本单位。进程的本质是一个正在执行的程序。在 CPU 对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。

线程是正在执行程序的主体,线程有自己的程序计数器、虚拟机栈以及本地方法栈。创建线程消耗的资源一般要比创建进程小号的资源小得多,因此线程也被称为轻量级进程。

线程是进程划分成更小的程序执行单位,一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源。多个进程之间是相互独立的,但是同一个进程中的多个线程之间可能会相互影响。一个线程是不能独立存在的,它必须是进程的一部分。

2.并发与并行的区别?

并发:同一个时间段内,多个任务都在执行(CPU不断进行上下文的切换,从用户角度来看多个任务是一起执行的,实际上在同一个时间点只有一个任务在执行)。
并行:同一个时间点,多个任务同时执行。

3.什么是上下文切换?

一个任务在执行完CPU时间片准备切换到另一个任务之前会先保存自己执行的位置,以便下次再切换到这个任务的时候可以接着上次结束的位置执行。任务从保存到再次加载的一个过程就叫上下文切换。

4.说一下线程的生命周期(状态)以及各个状态之间的切换(关系)?

Java线程在运行的生命周期中一共有以下六种状态:

  1. NEW:初始状态。Java线程刚刚被创建,还没有调用start()方法(启动);
  2. RUNNABLE:运行状态。调用start()方法之后的状态,Java线程在操作系统中的就绪状态和运行中状态统称运行状态(调用启动方法之后不代表线程会立即执行,但是会进入就绪状态,直到该线程抢到CPU时间片之后才会进入运行中状态);
  3. BLOCKED:阻塞状态。没有抢占到锁的Java线程会被阻塞,进入阻塞状态;
  4. WAITING:等待状态。Java线程进入等待状态表示需要等待其他线程通知或者中断才会 执行;
  5. TIMED_WAITING:超时等待状态。可以给Java线程设定一个等待时间,达到指定时间时会执行;
  6. TERMINATED:终止状态。表示该Java线程已经执行结束;

下面以图解方式说明各个状态之间的切换关系:
在这里插入图片描述

5.创建Java线程都有哪几种方式?

  1. 继承Thread类;
  2. 实现Runnable接口(无返回值);
  3. 实现Callable接口(有返回值);
  4. 基于线程池的方式创建线程;

6.为什么要使用多线程?

  • 从计算机层面来说:线程就是一个轻量级的进程,也是程序执行的最小单位,线程之间的切换和调用的成本要远远小于进程。在这个多核CPU的时代,多线程可以大大提高CPU和IO设备的综合利用率;
  • 从互联网发展趋势来说:现在的很多系统动不动就会要求有成百上千玩级别的并发量,而多线程就是用来提高高并发系统的效率的,利用好多线程可以大大提高系统的并发能力;

追问:多线程的三大特性是什么?

  • 原子性:保证数据一致性,线程安全;
  • 有序性:线程之间顺序执行;
  • 可见性:一个线程对共享资源的操作对另一个线程可见;

追问:使用多线程带来了什么问题?

内存泄漏、频繁上下文切换、死锁以及受限于硬件和软件资源等。

追问:什么是死锁?

多个线程同时被阻塞,它们都在等待其他线程释放资源而全部被无限制的阻塞。举个例子,线程A持有资源1,线程B持有资源2,线程A只有拿到资源2才会执行结束,线程B只有拿到资源1才会执行结束,双方都想拿到对方的资源,所以会无限制的互相等待从而进入死锁状态。
在这里插入图片描述
死锁必须具备的四个条件:

  1. 互斥条件:该资源在任意时刻只能被一个线程占用;
  2. 请求与保持:一个线程因为请求资源而被阻塞,已获得该资源的线程保持资源的拥有不释放;
  3. 不剥夺:线程已经获得的资源在线程没有执行完之前不能被其他线程强行剥夺,只能等自己执行之后再释放资源;
  4. 循环等待:若干线程之间形成一种头尾相接的循环等待资源的关系;

追问:如何避免死锁?

上面讲过死锁的四个必备条件,那么只要破坏其中一个条件,就可以避免死锁。

  1. 破坏互斥条件:这个条件自然不能被破坏,因为我使用锁就是为了让资源互斥 ;
  2. 破坏请求与保持条件:线程在执行的时候一次性申请完所有要用的资源;
  3. 破坏不剥夺条件:占用资源的线程如果在申请其他资源的时候,申请不到,那么就释放自己已经占用的资源;
  4. 破坏循坏等待条件:按照申请资源的顺序来访问资源,释放资源的顺序正好与其相反;

为了方便理解,下面写一个死锁的小例子

public class DeadLockDemo {

    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2


    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

执行结果
在这里插入图片描述
破坏这个死锁很简单,只需要改一个地方

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();

执行结果
在这里插入图片描述

7.启动一个线程为什么不能直接使用run()方法而要使用start()方法呢?

如果你直接调用run()方法,JVM是不会去创建线程的,run()方法只能用于已有线程中。Java里面创建线程之后必须要调用start()方法才能真正的创建一个线程并且让线程处于就绪状态,该方法会调用虚拟机启动一个本地线程,本地线程的创建会调用当前系统创建线程的方法进行创建,并且线程被执行的时候会回调 run()方法进行业务逻辑的处理。

8.sleep()方法和wait()方法有什么共同点和区别?

  • 两者最主要的区别就是slee(0不会释放锁。wait(0会释放锁;
  • 两者都可以让线程暂停执行;
  • sleep()方法通常备用来让线程暂停执行。wait()方法通常用于线程之间的通信;
  • sleep()方法执行完成之后,线程会自动苏醒。wait()方法执行完成之后,线程不会自动苏醒,必须等待其他线程使用调用同一个对象上的notify()方法或者notifyAll()方法才会苏醒;

9.现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

join作用是让其他线程变为等待,只有当前线程执行完毕后,等待的线程才会被释放。

10.如何优雅的中断一个线程?

当其他线程通过调用当前线程的 interrupt ()方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。线程通过检查自身是否被中断来进行相应,可以通过
isInterrupted()来检查自身是否被中断。

追问:为什么不用stop()去中断线程?

stop 方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态,相当于我们在linux上通过kill -9强制结束一个进程。

追问:如何将线程进行复位?

线程中提供了静态方Thread.interrupted()对设置中断标识的线程复位,还有一种被动复位的场景,就是抛出了一个InterruptedException 异常,在InterruptedException 抛出之前,JVM 会先把线程的中断标识位清除,然后才会抛出 InterruptedException,这个时候如果调用 isInterrupted 方法,将会返回 false。

追问:为什么要进行复位?

Thread.interrupted()是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经得到了中断信号,但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断前,他的中断状态仍然是 false,这就是复位的原因。

如果你想了解更多的关于线程启动、中断、复位相关的底层原理,想知道在虚拟机层面它到底做了什么,可以点击这里Java并发编程(一)之线程

练习、用心、持续------致每一位追梦人!加油!!!

猜你喜欢

转载自blog.csdn.net/w1453114339/article/details/106687782