JUC(基础常见面试题)

1.什么是线程和进程?

1.1进程?

进程是程序的一次执行过程,是程序运行程序的基本单位,因此进程是动态的,系统运行一个程序是一个进程从创建,运行到消亡的过程。

每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

任务管理器

1.2 线程?

线程与进程相似,但线程是一个比进程更小的执行单位。进程中的一个执行任务(控制单位),一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

与进程不同的是同类的多个线程共享进程的方法区,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程被称为轻量级进程。

Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。

public class MultiThread {
    public static void main(String[] args) {
        // 获取 Java 线程管理 MXBean
    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程 ID 和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        }
    }
}

上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):

[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口

从上面的输出内容可以看出:一个Java程序的运行时main线程和多个其他线程同时运行。

2.进程和线程的区别总结

**根本区别:**进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

**资源开销:**每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

**包含关系:**如果一个进程内有多个线程, 则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

**内存分配:**同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

**影响关系:**一个进程奔溃后,在保护模式下不会对其他进程产生影响,但是一个线程奔溃整个线程都会死掉。所以多进程要比多线程健壮

**执行过程:**每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

2.1图解进程和线程的关系

下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。

在这里插入图片描述

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的和**方法区 (JDK1.8 之后的元空间)*资源,但是每个线程有自己的*程序计数器虚拟机栈本地方法栈

2.1.1程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能过知道该线程上次运行到哪儿可。

需要注意的是,如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码是程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

2.1.2虚拟机栈和本地方法栈为什么是私有的?

**虚拟机栈:**每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等消息。从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机中入栈和出栈的过程。

本地方法栈:和虚拟机所发挥的作用相似,区别是:**虚拟机栈为迅疾执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。**在HotSpot虚拟机和Java虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟栈合本地方法栈都是私有的

2.1.3 一句话简单了解堆和方法区

堆和方法区是所以线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息。常量。静态变量。即使编译器编译后的代码等数据

3. 说说并发与并行的区别?

  • **并发:**同一时间段,多个任务都在执行(单位时间内不一定同时执行);
  • **并行:**单位时间内,多个任务同时执行。

4. 为什么要使用多线程呢?

先从总体上来说:

  • **从计算机底层来说:**线程可以比作是轻量级的进程, 是程序执行的最小单位,线程间的切换个调度的成本小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

再深入到计算机底层来探讨:

  • 单核时代: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
  • 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。

5. 使用多线程可能带来什么问题?

并发编程的目的是为了提高程序的执行效率提高程序运行速度,但是并发编程并不总是提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏,死锁,线程不安全等等

6.说说线程的生命周期和状态?

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。

Java 线程的状态

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):

Java 线程状态变迁

由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。

7.什么是上下文切换?

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过得线程计数器,栈信息等。当出现如下情况的时候,线程会从占用CPU状态中退出。

  • 主动让出CPU,比如调用了sleep(),wait()等
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求IO,线程被阻塞
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用CPU的时候恢复现场。并加载下一个将要占用CPU的线程上下文。这就是所谓的上下文切换

8.什么是线程死锁?如何避免死锁?

8.1 认识线程死锁

死锁的描述:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

例子:线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,所有这两个线程的就会互相等待而进入死锁状态。

线程死锁示意图

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();
    }
}

output

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程A通过synchronized(resource1)获得resource1的监听器锁,然后通过Thread.sleep(1000);让线程A休眠1s为的是让线程B得到执行然后获取到resource2的监听器锁。线程A和线程B休眠结束了都开始企图请求获得对方的资源,然后这两个线程就会陷入互相等待的状态,这就产生了死锁。上面的例子符合产生死锁的四个必要条件。

产生死锁必备的四个条件:

1.互斥条件:该资源任意一个时刻只由一个线程占用

2.请求与保持条件:一个进程因请求资源阻塞时,对已获得的资源保持不放

3.不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源

4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

8.2 如何预防和避免线程死锁?

**如何预防死锁?**破坏死锁的产生的必要条件即可:

**1.破坏请求与保持条件:**一次性申请所有的资源

**2.破坏不剥夺条件:**占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

**3.破坏循环等到条件:**靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放,破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态指的是

9.说说sleep()方法和wait()方法区别和共同点?

  • 两者最大的区别在于:sleep()方法没有释放锁,而wait()方法释放了锁
  • 两者都可以暂时线程的执行
  • wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行
  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法 。sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程自动苏醒。

10. 为什么我们调用start()方法时会执行run(),为什么我们不能直接调用run()方法?

new一个Thread,线程进入新建状态。调用start()方法,会启动一个线程并使线程进入就绪状态,当分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。但是,直接执行run()方法,会把run()方法当做一个main线程下的普通方法区执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结:调用start()方法方可启动线程并使线程进入就绪状态,直接执行run方法的话不会以多线程的方式执行

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值