>  기사  >  Java  >  Java 동시성 기본 일반적인 인터뷰 질문(요약)

Java 동시성 기본 일반적인 인터뷰 질문(요약)

青灯夜游
青灯夜游앞으로
2019-11-23 16:37:152301검색

이 글은 Java 동시성 기본에 대한 일반적인 인터뷰 질문을 요약한 것입니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

Java 동시성 기본 일반적인 인터뷰 질문(요약)

1.

#🎜 🎜#

1.1. 프로세스란?

프로세스는 프로그램의 실행 프로세스이며 시스템의 기본 단위입니다. 프로그램을 실행하므로 프로세스가 동적입니다. 시스템에서 프로그램을 실행하는 것은 프로세스의 생성, 운영, 종료까지의 과정입니다.

Java에서는 메인 함수를 시작하면 실제로 JVM 프로세스가 시작되는데, 메인 함수가 위치한 스레드가 이 프로세스의 스레드이며, 메인 스레드라고도 합니다.

아래 그림과 같이 Windows의 작업 관리자를 보면 현재 창에서 실행 중인 프로세스(.exe 파일의 실행)를 명확하게 확인할 수 있습니다.

Java 동시성 기본 일반적인 인터뷰 질문(요약)

1.2. 스레드란 무엇인가요? #🎜🎜 # 스레드는 프로세스와 유사하지만 스레드는 프로세스보다 더 작은 실행 단위입니다. 프로세스는 실행 중에 여러 스레드를 생성할 수 있습니다. 프로세스와는 달리 동일한 유형의 여러 스레드는 프로세스의

Heap

Method Area 리소스를 공유하지만 각 스레드에는 자체 프로그램 카운터가 있습니다. #🎜 🎜#, 가상 머신 스택로컬 메소드 스택이므로 스레드를 생성하거나 스레드 간 전환 시 프로세스보다 시스템의 부담이 훨씬 적습니다. 이 때문에 스레드를 경량 프로세스라고도 합니다. 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());
        }
    }
}
위 프로그램의 출력은 다음과 같습니다. (출력 내용은 다를 수 있습니다. 아래 각 스레드의 역할에 대해 너무 걱정하지 마세요. 메인 스레드가 기본 메서드를 실행한다는 점만 알아두세요. ):

[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口
위 출력에서 ​​볼 수 있습니다.

Java 프로그램 실행은 메인 스레드와 동시에 실행되는 여러 다른 스레드입니다

.

2. 스레드와 프로세스의 관계, 차이점, 장단점을 간략하게 설명해주세요.

JVM 관점에서 본 프로세스와 스레드의 관계

#🎜 🎜 #2.1.프로세스와 스레드의 관계 예시

다음 그림은 Java 메모리 영역입니다. 다음 그림을 통해 스레드와 스레드의 관계에 대해 이야기할 수 있습니다. JVM의 관점에서 프로세스를 살펴봅니다. Java 메모리 영역(런타임 데이터 영역)에 대해 잘 모른다면 다음 기사를 읽어보세요: "아마도 Java 메모리 영역에 대한 가장 명확한 기사"

Java 동시성 기본 일반적인 인터뷰 질문(요약)

위 그림에서 볼 수 있듯이 프로세스에는 여러 스레드가 있을 수 있으며 여러 스레드가 프로세스를 공유합니다.
힙 # 🎜🎜# 및
메소드 영역(JDK1.8 이후의 메타공간)

리소스, 그러나 각 스레드에는 자체

프로그램 카운터 , 가상 머신 스택#🎜🎜 # 및 로컬 메소드 스택. 요약: 스레드는 더 작은 실행 단위로 나누어진 프로세스입니다. 스레드와 프로세스의 가장 큰 차이점은 각 프로세스가 기본적으로 독립적이지만 동일한 프로세스의 스레드는 서로 영향을 미칠 가능성이 높기 때문에 각 스레드가 반드시 독립된 것은 아니라는 점입니다. 스레드 실행 오버헤드는 작지만 리소스 관리 및 보호에 도움이 되지 않는 반면 프로세스는 정반대입니다. 다음은 이 지식 포인트를 확장한 것입니다!

이 질문에 대해 생각해 봅시다: 왜 Program Counter,

Virtual Machine Stack

Local Method Stack

이 스레드 비공개인가요? 힙과 메소드 영역이 스레드에 의해 공유되는 이유는 무엇입니까? 2.2. 프로그램 카운터는 왜 비공개인가요?

프로그램 카운터에는 주로 다음 두 가지 기능이 있습니다. : 바이트코드 인터프리터는 프로그램 카운터를 변경하여 명령어를 순차적으로 읽습니다. 이를 통해 순차적 실행, 선택, 루프 및 예외 처리와 같은 코드 흐름 제어를 달성합니다.

멀티스레딩의 경우 프로그램 카운터를 사용하여 현재 스레드의 실행 위치를 기록하므로 스레드가 다시 전환될 때 스레드가 마지막으로 실행된 위치를 알 수 있습니다. .

    프로그램 카운터는 Java 코드가 실행될 때만 정의되지 않은 주소를 기록한다는 점에 유의해야 합니다.
  1. 따라서 프로그램 카운터를 비공개로 유지하는 목적은 주로
  2. 스레드 전환 후
  3. 올바른 실행 위치를 복원하는 것입니다.

2.3. 가상 머신 스택과 로컬 메서드 스택이 비공개인 이유는 무엇인가요?

  • 가상 머신 스택: 각 Java 메소드가 실행되면 지역 변수 테이블, 피연산자 스택 및 상수 풀 참조와 같은 정보를 저장하는 스택 프레임이 생성됩니다. 메소드 호출부터 실행 완료까지의 과정은 스택 프레임을 자바 가상 머신 스택에 push하고 pop하는 과정에 해당한다.
  • 로컬 메서드 스택: 가상 머신 스택이 수행하는 역할은 매우 유사하지만 차이점은 다음과 같습니다. 가상 머신 스택은 가상 머신이 Java 메서드(즉, 바이트코드)를 실행하는 역할을 하는 반면, 로컬 메서드 스택은 가상 머신에서 사용됨 기본 메소드 서비스로. HotSpot 가상 머신의 Java 가상 머신 스택과 결합됩니다.

따라서 스레드의 로컬 변수가 다른 스레드에서 액세스되지 않도록 하려면 가상 머신 스택과 로컬 메서드 스택이 스레드 전용입니다.

2.4 힙과 메소드 영역을 한 문장으로 간단히 이해하세요

힙과 메소드 영역은 모든 스레드가 공유하는 리소스로 프로세스에서 가장 큰 메모리 조각으로 주로 새로 저장하는 데 사용됩니다. 생성된 객체(모든 객체 모든 메모리가 여기에 할당됨) 메소드 영역은 주로 로드된 클래스 정보, 상수, 정적 변수, JIT(Just-In-Time) 컴파일러로 컴파일된 코드 및 기타 데이터를 저장하는 데 사용됩니다.

3. 동시성과 병렬성의 차이점에 대해 이야기해 보세요.

  • 동시성: 동일한 기간 동안 여러 작업이 실행됩니다(반드시 단위 시간당 동시에 실행될 필요는 없음). 병렬:
  • 단위 일정 시간 내에 여러 작업이 동시에 실행됩니다.
4. 멀티스레딩을 사용하는 이유

일반적으로 이야기해 보겠습니다.

컴퓨터 하단에서:
    스레드는 프로그램에 의해 실행되는 경량 프로세스와 비교할 수 있습니다. 가장 작은 단위로 스레드 간 전환 및 예약 비용이 프로세스 비용보다 훨씬 적습니다. 또한 멀티 코어 CPU 시대는 여러 스레드가 동시에 실행될 수 있어 스레드 컨텍스트 전환의 오버헤드가 줄어드는 것을 의미합니다.
  • 현대 인터넷 개발 추세의 관점에서 볼 때:
  • 오늘날의 시스템에는 종종 수백만 또는 심지어 수천만 개의 동시성이 필요하며 멀티스레딩 동시 프로그래밍은 높은 동시성 시스템을 개발하기 위한 기초입니다. 멀티스레딩 메커니즘을 사용하면 크게 향상될 수 있습니다. 시스템의 전반적인 동시성 및 성능.
  • 컴퓨터의 최하위 계층으로 더 깊이 들어가 보겠습니다.

싱글 코어 시대:
    싱글 코어 시대에 멀티 스레딩은 주로 CPU 및 IO 장치의 포괄적인 활용도를 향상시키는 것입니다. 예를 들어, 스레드가 하나만 있는 경우 CPU가 계산할 때 IO 장치는 유휴 상태가 되며 IO 작업을 수행할 때 CPU는 유휴 상태가 됩니다. 현재 둘 다의 활용률은 약 50%라고 간단히 말할 수 있습니다. 그러나 두 개의 스레드가 있는 경우에는 다릅니다. 한 스레드가 CPU 계산을 수행하면 다른 스레드가 IO 작업을 수행할 수 있으므로 이상적인 상황에서는 두 스레드의 활용률이 100%에 도달할 수 있습니다.
  • 멀티 코어 시대:
  • 멀티 코어 시대의 멀티 스레딩은 주로 CPU 활용률을 향상시키기 위한 것입니다. 예: 복잡한 작업을 계산하려는 경우 하나의 스레드만 사용하면 CPU의 CPU 코어 하나만 활용되지만 여러 스레드를 생성하면 여러 CPU 코어를 활용할 수 있으므로 성능 CPU 활용도가 향상됩니다.
5. 멀티스레딩을 사용하면 어떤 문제가 발생할 수 있나요?

동시 프로그래밍의 목적은 프로그램의 실행 효율성을 높이고 프로그램의 실행 속도를 높이는 것이지만 동시 프로그래밍은 그렇지 않습니다. 항상 프로그램의 실행 속도를 향상시키며, 동시 프로그래밍에서는 메모리 누수, 컨텍스트 전환, 교착 상태, 하드웨어 및 소프트웨어로 제한되는 유휴 리소스 문제와 같은 많은 문제가 발생할 수 있습니다.

6. 스레드의 수명 주기와 상태에 대해 이야기해 보세요.

Java 스레드는 실행 중인 수명 주기의 지정된 순간에 다음 6가지 상태 중 하나에만 있을 수 있습니다(사진 출처 "The Art of" Java 동시 프로그래밍' 》4.1.4항).

스레드는 수명주기 동안 특정 상태로 고정되지 않지만 코드가 실행될 때 다른 상태로 전환됩니다. Java 스레드의 상태 변경은 아래 그림에 나와 있습니다(그림 출처 "The Art of Java Concurrent 프로그래밍" 섹션 4.1.4). Java 동시성 기본 일반적인 인터뷰 질문(요약)

위 그림에서 볼 수 있듯이 스레드가 생성된 후 Java 동시성 기본 일반적인 인터뷰 질문(요약)NEW(새)

상태에서 호출됩니다. 스레드는

메서드 이후에 실행을 시작하고 스레드는 READY(실행 가능) 상태입니다. 실행 가능 상태의 스레드는 CPU 타임 슬라이스(timeslice)를 얻은 후 start()RUNNING 상태가 됩니다.

操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJavaJava Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

Java 동시성 기본 일반적인 인터뷰 질문(요약)

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

7. 什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

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

8.1. 认识线程死锁

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

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

Java 동시성 기본 일반적인 인터뷰 질문(요약)

下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):

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. 如何避免线程死锁?

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件

一次性申请所有的资源。

破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件

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

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

        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");
                }
            }
        }, "线程 2").start();

Output

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

Process finished with exit code 0

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

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

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

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

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

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

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

推荐教程:java教程

위 내용은 Java 동시성 기본 일반적인 인터뷰 질문(요약)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제