Java 동시 고주파 면접 질문에 대한 철저한 이해

콘텐츠는 내 학습 웹사이트에서 가져온 것입니다: topjavaer.cn

50개의 Java 동시 고주파 인터뷰 질문을 공유합니다 .

스레드 풀

스레드 풀: 스레드를 관리하는 풀입니다.

쓰레드 풀은 왜 쓰레드 풀을 주로 쓰는데 쓰레드는 직접 새 쓰레드를 만드는게 좋지않나요?

스레드를 수동으로 생성하는 데는 두 가지 단점이 있습니다.

  1. 통제되지 않은 위험
  2. 빈번한 생성은 비용이 많이 듭니다

통제 불능인 이유는 무엇입니까 ?

시스템 리소스가 제한되어 있고, 누구나 다른 업무용 스레드를 수동으로 생성할 수 있으며, 생성된 스레드에 이름이 있는지 여부 등 스레드 생성에 대한 통일된 기준이 없습니다. 시스템이 실행 중일 때 모든 스레드가 리소스를 확보하고 규칙이 없으며 제어하기 어려운 혼란스러운 장면을 상상할 수 있습니다. 가장 포괄적인 Java 인터뷰 사이트

빈번한 수동 스레드 생성이 비용이 많이 드는 이유는 무엇입니까? new Object()와의 차이점은 무엇입니까?

Java의 모든 것이 객체이지만 스레드를 생성하는 new Thread()와 new Object() 사이에는 여전히 차이가 있습니다.

new Object() 프로세스는 다음과 같습니다.

  1. JVM은 메모리 M 블록을 할당합니다.
  2. 메모리 M의 개체를 초기화합니다.
  3. 메모리 M의 주소를 참조 변수 obj에 할당

쓰레드 생성 과정은 다음과 같습니다.

  1. JVM은 각 스레드 메서드 호출에 대한 스택 프레임을 보유하는 스레드 스택에 대한 메모리를 할당합니다.
  2. 각 스택 프레임은 지역 변수, 반환 값, 피연산자 스택 및 상수 풀의 배열로 구성됩니다.
  3. 각 스레드는 가상 머신에서 현재 실행 중인 스레드 명령 주소를 기록하는 데 사용되는 프로그램 카운터를 얻습니다.
  4. 시스템은 Java 스레드에 해당하는 기본 스레드를 생성합니다.
  5. JVM 내부 데이터 구조에 스레드 관련 설명자 추가
  6. 스레드 공유 힙 및 메서드 영역

스레드를 생성하려면 약 1M 공간이 필요합니다(Java8, 머신 사양 2c8G). 스레드를 수동으로 자주 생성/파기하는 비용이 매우 높다는 것을 알 수 있습니다.

스레드 풀을 사용하는 이유는 무엇입니까?

  • 자원 소비를 줄입니다 . 생성된 스레드를 재사용하여 스레드 생성 및 소멸 비용을 줄입니다.
  • 응답성 향상 . 작업이 도착하면 스레드가 생성될 때까지 기다리지 않고 즉시 실행할 수 있습니다.
  • 스레드 관리성을 향상시킵니다 . 시스템이 동일한 유형의 스레드를 대량으로 생성하여 메모리를 소비하는 것을 방지하기 위해 통합된 방식으로 스레드를 관리합니다.

스레드 풀 실행 원칙?

[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있으므로 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-nmwpJRVq-1685887928368)(http://img.topjavaer.cn/img/thread) 풀 실행 프로세스.png) ]

  1. 스레드 풀에서 살아남은 스레드 수가 코어 스레드 수보다 적으면 corePoolSize새로 제출된 작업에 대해 스레드 풀이 스레드를 생성하여 작업을 처리합니다. 스레드 풀에서 살아남은 스레드의 수가 코어 스레드의 수보다 적거나 같으면 corePoolSize스레드 풀의 스레드는 항상 살아남습니다.유휴 시간을 초과하더라도 스레드는 keepAliveTime파괴되지 않고 항상 유지됩니다. 거기에서 차단되고 작업 대기열의 작업이 실행될 때까지 기다립니다.
  2. 스레드 풀에서 살아남은 스레드 수가 corePoolSize와 같을 때 이것은 새로 제출된 작업이며 작업 큐 workQueue에 넣어 실행을 기다립니다.
  3. 스레드 풀에서 살아남은 스레드의 수가 같고 corePoolSize태스크 큐가 가득 찼을 때, maximumPoolSize>corePoolSize이때 새 태스크가 들어오면 스레드 풀은 계속해서 새 스레드를 생성하여 새 태스크를 처리한다고 가정합니다. 스레드가 한계에 도달했습니다 maximumPoolSize. 다시 생성되지 않습니다.
  4. 현재 스레드 수가 에 도달 maximumPoolSize하고 작업 대기열이 가득 차면 새 작업이 오는 경우 처리를 위해 거부 전략을 직접 사용하십시오. 기본 거부 전략은 RejectedExecutionException을 발생시키는 것입니다.

컴퓨터 파운데이션 , 자바 파운데이션, 멀티스레딩, JVM, 데이터베이스, Redis, Spring, Mybatis, SpringMVC, SpringBoot, 분산, 마이크로서비스, 디자인 패턴, 아키텍처, 학교 채용 및 소셜 채용 공유, 등 핵심 지식 포인트, 스타에 오신 것을 환영합니다 ~

Github 주소

Github에 접속할 수 없는 경우 gitee 주소로 접속할 수 있습니다.

기티 주소

스레드 풀 매개변수는 무엇입니까?

ThreadPoolExecutor의 일반 생성자:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

1. corePoolSize: 새로운 작업이 있을 때 스레드 풀의 스레드 수가 스레드 풀의 기본 크기에 도달하지 못하면 새 스레드를 생성하여 작업을 실행하고, 그렇지 않으면 해당 작업을 차단 상태로 만듭니다. 대기줄. 스레드 풀에서 살아남은 스레드 수가 항상 corePoolSize보다 크면 corePoolSize를 늘려야 합니다.

2. maximumPoolSize: 블로킹 큐가 가득 찼을 때 스레드 풀의 스레드 수가 최대 스레드 수를 초과하지 않으면 새 스레드가 생성되어 작업을 실행합니다. 그렇지 않으면 거부 정책에 따라 새 작업이 처리됩니다. 비코어 쓰레드는 일시적으로 빌려온 자원과 유사하며 유휴 시간이 keepAliveTime을 초과하면 자원 낭비를 피하기 위해 이 쓰레드를 종료해야 합니다.

3. BlockingQueue: 실행 대기 중인 작업을 저장합니다.

4. keepAliveTime: 비코어 스레드가 유휴 상태가 된 후 활성 상태를 유지하는 시간인 이 매개변수는 비코어 스레드에 대해서만 유효합니다. 0으로 설정하면 중복 유휴 스레드가 즉시 종료됨을 나타냅니다.

5. TimeUnit: 시간 단위

TimeUnit.DAYS
TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS

6. ThreadFactory: 쓰레드 풀이 새로운 쓰레드를 생성할 때마다 쓰레드 팩토리 방식을 통해 이루어진다. 스레드 풀이 새 스레드를 생성해야 할 때마다 호출되는 ThreadFactory에는 newThread 메서드 하나만 정의되어 있습니다.

public class MyThreadFactory implements ThreadFactory {
    
    
    private final String poolName;
    
    public MyThreadFactory(String poolName) {
    
    
        this.poolName = poolName;
    }
    
    public Thread newThread(Runnable runnable) {
    
    
        return new MyAppThread(runnable, poolName);//将线程池名字传递给构造函数,用于区分不同线程池的线程
    }
}

7. RejectedExecutionHandler: Queue와 Thread Pool이 가득 차면 거부 정책에 따라 새로운 작업을 처리합니다.

AbortPolicy:默认的策略,直接抛出RejectedExecutionException
DiscardPolicy:不处理,直接丢弃
DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务
CallerRunsPolicy:由调用线程处理该任务

스레드 풀 크기를 설정하는 방법은 무엇입니까?

스레드 풀의 스레드 수가 너무 적으면 처리해야 할 요청이 많을 때 시스템 응답이 느려져 사용자 경험에 영향을 미치고 많은 수의 작업도 프로세스에 누적됩니다. 태스크 큐, 결과적으로 OOM.

스레드 풀의 스레드 수가 너무 많으면 많은 수의 스레드가 동시에 CPU 리소스를 점유할 수 있으며, 이는 많은 수의 컨텍스트 전환을 유발하여 스레드의 실행 시간을 늘리고 실행 효율성에 영향을 미칩니다.

CPU 집약적 작업(N+1) : 주로 CPU 리소스를 소모하는 작업으로, 스레드 수를 1개로 설정할 수 있습니다 N(CPU 核心数)+1. 잠금을 기다리는 중) 및 영향. 스레드가 차단되면 CPU 리소스가 해제되고 이 경우 추가 스레드가 CPU의 유휴 시간을 최대한 활용할 수 있습니다.

I/O 집약적 작업(2N) : 시스템은 대부분의 시간을 IO 작업을 처리하는 데 사용하며, 이 때 CPU 자원을 해제하기 위해 스레드가 차단될 수 있으며, 이 때 CPU를 다른 스레드에 넘겨 사용할 수 있습니다. . 따라서 IO 집약적 작업의 응용 프로그램에서 더 많은 스레드를 구성할 수 있습니다.구체적인 계산 방법은 다음과 같습니다 最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (IO耗时/CPU耗时)). 일반적으로 2N으로 설정할 수 있습니다.

가장 포괄적인 Java 인터뷰 사이트

스레드 풀의 유형은 무엇입니까? 해당 장면?

일반적인 스레드 풀은 FixedThreadPool, SingleThreadExecutorCachedThreadPool입니다 ScheduledThreadPool. 이들은 모두 ExecutorService스레드 .

고정 스레드 풀

고정된 수의 스레드가 있는 스레드 풀입니다. 어느 시점에서든 최대 nThreads 스레드가 작업을 수행하기 위해 활성화됩니다.

public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

제한되지 않은 대기열 LinkedBlockingQueue(대기열 용량은 Integer.MAX_VALUE임)를 사용하면 실행 중인 스레드 풀이 작업을 거부하지 않습니다. 즉, RejectedExecutionHandler.rejectedExecution() 메서드가 호출되지 않습니다.

maxThreadPoolSize는 유효하지 않은 매개변수이므로 coreThreadPoolSize와 일치하도록 값을 설정하십시오.

keepAliveTime도 유효하지 않은 매개변수입니다. 0L로 설정하세요. 이 스레드 풀의 모든 스레드는 코어 스레드이고 코어 스레드는 재활용되지 않기 때문입니다(executor.allowCoreThreadTimeOut(true)이 설정되지 않은 경우).

적용 가능한 시나리오: CPU를 많이 사용하는 작업을 처리하는 데 적합하며 작업자 스레드가 CPU를 오랫동안 사용할 때 가능한 적은 수의 스레드를 CPU에 할당합니다. 즉, 장기 작업을 수행하는 데 적합합니다. FixedThreadPool은 작업을 거부하지 않으며 작업이 많을 때 OOM을 유발한다는 점에 유의해야 합니다.

SingleThreadExecutor

하나의 스레드만 있는 스레드 풀.

public static ExecutionService newSingleThreadExecutor() {
	return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

무제한 큐 LinkedBlockingQueue를 사용하십시오. 스레드 풀에는 실행 중인 스레드가 하나만 있고 새 작업은 작업 대기열에 배치됩니다. 스레드가 작업을 처리한 후 루프에서 실행하기 위해 대기열에서 작업을 가져옵니다. 작업이 순차적으로 실행되는지 확인합니다.

적용 가능한 시나리오: 작업을 순차적으로 실행하는 시나리오에 적용할 수 있으며, 한 작업은 한 번에 한 작업씩 실행됩니다. 작업이 많으면 OOM도 발생합니다.

캐시된 스레드 풀

필요에 따라 새 스레드를 생성하는 스레드 풀입니다.

public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

메인 스레드가 작업을 제출하는 속도가 스레드가 작업을 처리할 수 있는 속도보다 높으면 CachedThreadPool계속해서 새 스레드가 생성됩니다. 극단적인 경우 CPU 및 메모리 리소스가 고갈될 수 있습니다.

용량이 없는 SynchronousQueue를 스레드 풀 작업 대기열로 사용합니다 SynchronousQueue.offer(Runnable task).

적용 가능한 시나리오: 다수의 단기 소규모 작업을 동시에 실행하는 데 사용됩니다. CachedThreadPool생성이 허용된 쓰레드의 개수는 Integer.MAX_VALUE로, 많은 개수의 쓰레드가 생성되어 OOM이 발생할 수 있습니다.

ScheduledThreadPoolExecutor

지정된 지연 후 또는 주기적으로 작업을 실행합니다. 와 같은 다른 옵션이 있기 때문에 기본적으로 실제 프로젝트에서는 사용되지 않습니다 quartz.

사용된 작업 대기열은 하나를 DelayQueue캡슐화하여 PriorityQueue대기열 PriorityQueue의 작업을 정렬하고 시간이 가장 빠른 작업이 먼저 실행되고(즉, 작은 변수가ScheduledFutureTask 먼저 실행됨) 시간이 같으면 작업이 실행됩니다. 먼저 제출된 것이 먼저 실행됩니다( 더 작은 변수가 먼저 실행됨).timeScheduledFutureTasksquenceNumber

주기적인 작업 단계 실행:

  1. 스레드는 DelayQueue에서 ScheduledFutureTask(DelayQueue.take()). 적기 작업은 시간이 현재 시스템의 시간보다 크거나 같음을 ScheduledFutureTask의미 .
  2. 이것을 실행하십시오 ScheduledFutureTask;
  3. ScheduledFutureTask수정된 시간 변수는 다음에 실행할 시간입니다.
  4. 이 수정된 시간을 ScheduledFutureTask다시 DelayQueue( DelayQueue.add())에 넣습니다.

적용 가능한 시나리오: 태스크가 주기적으로 실행되는 시나리오 및 스레드 수를 제한해야 하는 시나리오.

프로젝트에서 여러 스레드 풀을 사용합니까, 아니면 하나의 스레드 풀을 사용합니까?

프로젝트에 스레드 풀을 사용해야 하는 여러 시나리오가 있는 경우 가장 좋은 방법은 각 비즈니스 시나리오에 대해 독립적인 스레드 풀을 사용하는 것입니다. 모든 장면이 스레드 풀을 공유하지 않도록 합니다.

1) 독립적인 선 도시는 서로의 작업 운영에 영향을 미치지 않으므로 이 작업의 독립성과 무결성을 보장하는 데 더 도움이 되며 낮은 결합 설계 아이디어와 더 일치합니다.

2) 모든 시나리오가 쓰레드 풀을 공유한다면 문제가 발생할 수 있는데, 예를 들어 쓰레드 풀을 공유하는 태스크 A, 태스크 B, 태스크 C의 3가지 태스크 시나리오가 있다. 작업 A에 대한 요청량이 급격히 증가하면 작업 B와 작업 C에 사용 가능한 스레드가 없어 오랫동안 리소스를 얻지 못할 수 있습니다. 예를 들어, 작업 A는 동시에 3000개의 스레드 요청을 가지고 있는데, 이때 작업 B와 작업 C는 리소스를 할당받지 못하거나 적은 수의 스레드 리소스를 할당받을 수 있습니다.

메모:

1. JDK와 함께 제공되는 클래스는 많은 스레드 풀을 사용합니다.
2. 많은 오픈 소스 프레임워크는 많은 스레드 풀을 사용합니다.
3. 자신의 애플리케이션도 여러 스레드 풀을 생성합니다.
4. 각 스레드 풀은 얼마나 많은 스레드 풀을 사용합니까? 얼마나 많은 쓰레드를 제공할 것인지를 상세하게 테스트해야 함

프로세스 스레드

프로세스는 메모리에서 실행되는 응용 프로그램을 말하며 각 프로세스는 자체적으로 독립된 메모리 공간을 가집니다.

스레드는 프로세스보다 작은 실행 단위로, 프로세스 내의 독립적인 제어 흐름이며, 프로세스는 여러 스레드를 시작할 수 있으며 각 스레드는 서로 다른 작업을 병렬로 실행합니다.

스레드 수명 주기

초기(NEW) : 스레드가 구성되고 start()가 아직 호출되지 않았습니다.

실행 중(RUNNABLE) : 운영 체제의 준비 및 실행 상태를 포함합니다.

차단(BLOCKED) : 일반적으로 수동적이며 리소스 선점 중에는 리소스를 얻을 수 없으며 수동적으로 메모리에서 일시 중단되고 리소스 해제가 해제되기를 기다립니다. 차단된 스레드는 메모리가 아닌 CPU를 해제합니다.

Waiting(WAITING) : 이 상태에 진입하는 스레드는 다른 스레드가 특정 작업(알림 또는 중단)을 수행할 때까지 기다려야 합니다.

타임아웃 대기(TIMED_WAITING) : 이 상태는 WAITING과는 달리 지정된 시간이 지나면 저절로 복귀할 수 있습니다.

종료됨(TERMINATED) : 스레드가 실행되었음을 나타냅니다.

이미지 출처: Java의 동시 프로그래밍 기술

스레드 중단에 대해 이야기합니까?

스레드 중단은 스레드가 실행 중 다른 스레드에 의해 중단되는 것을 의미하며 중지와 중지의 가장 큰 차이점은 중지는 시스템이 스레드를 강제로 종료하는 반면 스레드 중단은 대상 스레드에 인터럽트 신호를 보내는 것입니다. 대상 스레드가 스레드를 수신하지 않는 경우 인터럽트 신호는 스레드를 종료하지만 스레드는 종료하지 않습니다 종료할지 아니면 다른 논리를 실행할지 여부는 대상 스레드에 따라 다릅니다.

스레드 중단에는 세 가지 중요한 방법이 있습니다.

1、java.lang.Thread#인터럽트

interrupt()대상 스레드의 메서드를 호출 하고 대상 스레드에 인터럽트 신호를 보내면 해당 스레드에 인터럽트 표시가 표시됩니다.

2、java.lang.Thread#isInterrupted()

대상 스레드가 인터럽트되었는지 여부를 확인하기 위해 인터럽트 플래그가 지워지지 않습니다.

3、java.lang.Thread#interrupted

대상 스레드가 인터럽트되었는지 확인하기 위해 인터럽트 플래그가 지워집니다.

private static void test2() {
    
    
    Thread thread = new Thread(() -> {
    
    
        while (true) {
    
    
            Thread.yield();

            // 响应中断
            if (Thread.currentThread().isInterrupted()) {
    
    
                System.out.println("Java技术栈线程被中断,程序退出。");
                return;
            }
        }
    });
    thread.start();
    thread.interrupt();
}

스레드를 생성하는 방법은 무엇입니까?

  • 클래스를 확장하여 Thread여러 스레드 만들기
  • 인터페이스를 구현하여 Runnable여러 스레드 생성
  • Callable인터페이스를 구현 하고 FutureTask인터페이스를 통해 스레드를 생성합니다.
  • 프레임워크를 사용하여 Executor스레드 풀을 생성합니다.

Inherit Thread를 사용하여 다음과 같이 스레드 코드를 생성합니다 . run() 메서드는 운영 체제 수준의 스레드를 생성한 후 jvm에 의해 다시 호출되며 수동으로 호출할 수 없으며 수동 호출은 일반 메서드 호출과 동일합니다.

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:15
 */
public class MyThread extends Thread {
    
    
    public MyThread() {
    
    
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println(Thread.currentThread() + ":" + i);
        }
    }

    public static void main(String[] args) {
    
    
        MyThread mThread1 = new MyThread();
        MyThread mThread2 = new MyThread();
        MyThread myThread3 = new MyThread();
        mThread1.start();
        mThread2.start();
        myThread3.start();
    }
}

Runnable은 스레드 코드를 생성합니다 .

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:04
 */
public class RunnableTest {
    
    
    public static  void main(String[] args){
    
    
        Runnable1 r = new Runnable1();
        Thread thread = new Thread(r);
        thread.start();
        System.out.println("主线程:["+Thread.currentThread().getName()+"]");
    }
}

class Runnable1 implements Runnable{
    
    
    @Override
    public void run() {
    
    
        System.out.println("当前线程:"+Thread.currentThread().getName());
    }
}

Thread 클래스를 상속하는 것보다 Runnable 인터페이스를 구현하는 이점:

  1. Java에서 단일 상속의 제한을 피할 수 있습니다.
  2. 스레드 풀은 Runable 또는 Callable 클래스를 구현하는 스레드에만 배치할 수 있으며 Thread를 상속하는 클래스에는 직접 배치할 수 없습니다.

Callable은 스레드 코드를 생성합니다 .

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:21
 */
public class CallableTest {
    
    
    public static void main(String[] args) {
    
    
        Callable1 c = new Callable1();

        //异步计算的结果
        FutureTask<Integer> result = new FutureTask<>(c);

        new Thread(result).start();

        try {
    
    
            //等待任务完成,返回结果
            int sum = result.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
    
    
            e.printStackTrace();
        }
    }

}

class Callable1 implements Callable<Integer> {
    
    

    @Override
    public Integer call() throws Exception {
    
    
        int sum = 0;

        for (int i = 0; i <= 100; i++) {
    
    
            sum += i;
        }
        return sum;
    }
}

Executor를 사용하여 스레드 코드 생성 :

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:44
 */
public class ExecutorsTest {
    
    
    public static void main(String[] args) {
    
    
        //获取ExecutorService实例,生产禁用,需要手动创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //提交任务
        executorService.submit(new RunnableDemo());
    }
}

class RunnableDemo implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println("大彬");
    }
}

스레드 교착 상태란 무엇입니까?

스레드 교착 상태는 실행 중 리소스 경쟁으로 둘 이상의 스레드가 서로를 기다리는 현상을 말합니다. 외력이 없으면 앞으로 나아갈 수 없습니다.

아래 그림과 같이 스레드 A는 자원 2를 보유하고 스레드 B는 자원 1을 보유합니다. 둘 다 동시에 상대방이 보유한 자원을 신청하기를 원하므로 두 스레드는 서로를 기다리고 교착 상태.

[외부 링크 사진 전송 실패, 소스 사이트에 도난 방지 링크 메커니즘이 있을 수 있으므로 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-d7NVKug8-1685887928371)(http://img.topjavaer.cn/img/ 교착 상태.png)]

다음 예제는 스레드 교착 상태를 설명하며 코드는 동시 프로그래밍의 아름다움에서 비롯됩니다.

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

코드 출력은 다음과 같습니다.

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

synchronized스레드 A는 (resource1)을 통해 resource1에 대한 모니터 잠금을 획득하고 Thread.sleep(1000). 스레드 B가 실행될 수 있도록 스레드 A를 1초 동안 휴면 상태로 둔 다음 리소스 2의 모니터 잠금을 획득합니다. 쓰레드 A와 쓰레드 B의 수면이 끝나면 둘 다 상대방의 자원을 요청하기 시작하고 두 쓰레드는 서로를 기다리는 상태에 빠지게 되어 교착 상태가 발생합니다.

스레드 교착 상태는 어떻게 발생합니까? 그것을 피하는 방법?

교착 상태가 발생하기 위한 네 가지 필수 조건 :

  • 상호 배제: 한 번에 하나의 프로세스에서만 리소스를 사용할 수 있습니다.

  • 요청 및 보류: 리소스 요청으로 인해 프로세스가 차단된 경우 획득한 리소스가 해제되지 않음

  • 박탈 금지: 프로세스에서 얻은 자원을 사용하기 전에는 강제로 박탈할 수 없습니다.

  • 순환 대기: 프로세스 간에 자원을 주기적으로 대기

교착 상태를 피하는 방법 :

  • 잠금은 상호 배제를 보장하기 위한 것이므로 상호 배제 조건을 파괴할 수 없습니다.
  • 한 번에 모든 리소스를 신청하여 리소스를 점유하는 스레드를 피하고 다른 리소스를 기다리십시오.
  • 일부 리소스를 점유하고 있는 스레드가 다른 리소스에 추가로 지원할 때 애플리케이션을 획득할 수 없는 경우 점유한 리소스를 적극적으로 해제합니다.
  • 순차적으로 자원 신청

스레드 실행과 시작의 차이점은 무엇입니까?

  • 프로그램이 메서드를 호출하면 메서드의 코드를 start()실행하기 위해 새 스레드가 생성됩니다 . 일반 메서드와 마찬가지로 직접 호출하면 새 스레드가 생성되지 않습니다.run()run()run()
  • 스레드의 start()메서드는 한 번만 호출할 수 있으며 여러 번 호출하면 java.lang.IllegalThreadStateException이 발생합니다. run()방법은 제한되지 않습니다.

스레드에는 어떤 메서드가 있습니까?

시작

스레드를 시작하는 데 사용됩니다.

getPriority

스레드 우선순위 가져오기, 기본값은 5, 기본 스레드 우선순위는 5입니다. 수동으로 지정하지 않으면 스레드 A가 스레드 B를 시작하는 것과 같이 스레드 우선순위가 상속되고 스레드 B의 우선순위는 스레드의 우선순위와 동일합니다. ㅏ

우선 순위 설정

스레드 우선순위를 설정합니다. CPU는 우선 순위가 높은 스레드에 실행 리소스를 제공하려고 합니다.

방해하다

스레드에 중단해야 한다고 알리면 중단할지 계속 실행할지 여부는 알림을 받은 스레드 자체에서 처리합니다.

스레드에서 인터럽트()가 호출되는 경우 두 가지 경우가 있습니다.

  1. 스레드가 차단 상태(예: 절전, 대기, 조인 등)에 있는 경우 스레드는 즉시 차단 상태를 종료하고 InterruptedException을 throw합니다.

  2. 스레드가 정상적으로 활성이면 스레드의 인터럽트 플래그가 true로 설정됩니다. 단, 인터럽트 플래그가 설정된 스레드는 영향을 받지 않고 정상적으로 계속 실행될 수 있습니다.

interrupt()는 실제로 스레드를 중단할 수 없으며 호출된 스레드 자체의 협력이 필요합니다.

가입하다

다른 스레드가 종료될 때까지 기다리십시오. 현재 스레드에서 다른 스레드의 join() 메서드가 호출되면 현재 스레드는 다른 프로세스가 실행을 완료할 때까지 차단 상태로 전환되고 그 후 현재 스레드는 차단 상태에서 준비 상태로 전환됩니다.

생산하다

현재 실행 중인 스레드 개체를 일시 중단하고 우선 순위가 같거나 높은 스레드에 실행 기회를 줍니다.

스레드를 차단 상태로 만듭니다. millis 매개변수는 휴면 시간을 밀리초 단위로 설정합니다. 절전 모드가 끝나면 스레드는 자동으로 Runnable 상태로 전환됩니다.

휘발성의 기본 원리

volatilevolatile모든 스레드에 대한 변수의 가시성을 보장하고 원자성을 보장하지 않는 경량 동기화 메커니즘입니다 .

  1. volatile변수에 쓸 때 JVM LOCK은 프로세서에 접두사 명령을 보내 변수가 있는 캐시 라인의 데이터를 다시 시스템 메모리에 씁니다.
  2. 캐시 일관성 프로토콜로 인해 각 프로세서는 버스에서 전파되는 데이터를 스니핑하여 캐시가 만료되었는지 확인하고 프로세서가 자신의 캐시 라인에 해당하는 메모리 주소가 수정되었음을 발견하면 현재 프로세서의 캐시를 업데이트합니다. 라인이 유효하지 않은 상태로 설정됨 프로세서가 데이터를 수정하면 시스템 메모리에서 프로세서 캐시로 데이터를 다시 읽습니다.

캐시 일관성 프로토콜이 무엇인지 살펴보겠습니다.

캐시 정합성 프로토콜 : CPU가 데이터를 쓸 때 연산 중인 변수가 공유 변수, 즉 다른 CPU에 변수의 복사본이 있는 경우 다른 CPU에 캐시를 무효화하라는 신호를 보냅니다. 그래서 다른 CPU가 이 변수를 읽어야 할 때 메모리에서 다시 읽을 것입니다.

volatile키워드에는 두 가지 기능이 있습니다.

  1. 공유 변수에서 작동하는 다른 스레드의 가시성을 보장합니다 . 즉, 스레드가 변수 값을 수정하고 새 값이 다른 스레드에 즉시 표시됩니다.
  2. 명령어 재정렬은 금지되어 있습니다 .

명령어 재정렬은 JVM이 명령어를 최적화하고 프로그램 실행 효율성을 개선하며 단일 스레드 프로그램의 실행 결과에 영향을 미치지 않으면서 병렬성을 최대한 높이는 것입니다. 内存屏障Java 컴파일러는 프로세서 재정렬을 방지하기 위해 명령어 시리즈를 생성할 때 적절한 위치에 명령어를 삽입합니다 . 메모리 배리어를 삽입하는 것은 CPU와 컴파일러에게 이 명령 앞에 있는 것이 먼저 실행되어야 하고 이 명령 뒤에 오는 것이 나중에 실행되어야 한다고 말하는 것과 같습니다. 휘발성 필드에 대한 쓰기 작업의 경우 Java 메모리 모델은 쓰기 작업 후에 쓰기 장벽 명령을 삽입하여 이전에 기록된 값을 메모리에 플러시합니다.

동기화의 용도는 무엇입니까?

  1. 수정된 일반 방식 : 현재 객체 인스턴스에 작용하고 동기화 코드를 입력하기 전에 현재 객체 인스턴스의 잠금을 획득합니다.
  2. Modified static method : 현재 클래스에 작용하며, 동기화 코드를 입력하기 전에 현재 클래스 객체의 잠금을 획득한다.
  3. 수정된 코드 블록 : 잠금 객체를 지정하고, 주어진 객체를 잠그고, 동기화 코드 라이브러리에 들어가기 전에 주어진 객체의 잠금을 얻습니다.

동기화 기능은 무엇입니까?

원자성 : 동기화 코드에 액세스하여 스레드의 상호 배제를 보장합니다.

가시성 : 공유 변수의 수정 사항을 적시에 볼 수 있도록 합니다.

정돈 : 재주문 문제를 효과적으로 해결합니다.

동기화의 기본 구현 원칙은 무엇입니까?

동기화된 동기화된 코드 블록의 구현 monitorentermonitorexit명령을 통해 이루어지며 monitorenter명령은 동기화된 코드 블록의 시작 위치를 가리키고 monitorexit명령은 동기화된 코드 블록의 끝 위치를 나타냅니다. monitorenter이 명령어를 실행할 때 monitor쓰레드는 잠금 즉, 획득의 소유권을 획득하려고 시도한다(모니터 객체는 각 자바 객체의 객체 헤더에 존재하고, 동기화된 잠금은 이렇게 획득되는데, 그렇기 때문에 어떤 객체도 Java에서는 잠금 이유로 사용할 수 있음).

내부에 카운터를 포함하고 있으며 카운터가 0일 때 성공적으로 획득할 수 있으며 획득 후 잠금 카운터를 1로 설정, 즉 1을 더합니다. 그에 따라 monitorexit명령을 잠금 카운터를 0으로 설정하여
잠금이 해제되었음을 나타냅니다. 객체 잠금 획득에 실패하면 현재 스레드가 차단되고 다른 스레드가 잠금을 해제할 때까지 대기합니다.

synchronized에 의해 수정된 메소드는 monitorenter명령어 메소드가 동기 메소드임을 나타내는 로고 monitorexit로 대체되며 , JVM은 메소드가 동기 메소드로 선언되었는지 여부를 구별하기 위해 액세스 플래그를 사용합니다. 해당 동기 호출을 실행합니다 .ACC_SYNCHRONIZEDACC_SYNCHRONIZED

휘발성과 동기화의 차이점은 무엇입니까?

  1. volatile변수에서만 사용할 수 있으며 synchronized클래스, 변수, 메서드 및 코드 블록에서 사용할 수 있습니다.
  2. volatile가시성을 보장하기 위해 synchronized원자성과 가시성을 보장합니다.
  3. volatile명령어 재정렬을 비활성화합니다 synchronized.
  4. volatile방해를 일으키지 않을 synchronized것입니다.

ReentrantLock과 동기화의 차이점

  1. 동기화된 키워드를 사용하여 동기화를 달성합니다.스레드가 동기화 코드 블록을 실행한 후 잠금이 자동으로 해제되는 반면 ReentrantLock은 잠금을 수동으로 해제해야 합니다.
  2. Synchronized 불공평한 잠금 이며 ReentrantLock은 공정한 잠금으로 설정할 수 있습니다.
  3. ReentrantLock에서 잠금을 획득하기 위해 대기 중인 스레드는 중단 가능 하며 스레드는 잠금 대기를 포기할 수 있습니다. 그리고 동기화는 무기한 대기합니다.
  4. ReentrantLock은 잠금을 획득하기 위해 제한 시간을 설정할 수 있습니다 . 지정된 데드라인 이전에 잠금을 획득하고 데드라인까지 잠금이 획득되지 않은 경우 반환합니다.
  5. ReentrantLock의 tryLock() 메서드는 non-blocking 방식으로 잠금 획득을 시도할 수 있으며 , 이 메서드를 호출한 직후에 반환되며, 획득할 수 있으면 true를 반환하고 그렇지 않으면 false를 반환합니다.

wait()와 sleep()의 유사점과 차이점은 무엇입니까?

같은 점 :

  1. 둘 다 현재 스레드를 일시 중단하고 다른 스레드에 기회를 줄 수 있습니다.
  2. wait() 및 sleep() 호출 후 기다리는 동안 중단된 모든 스레드에 대해 발생합니다.InterruptedException

차이점 :

  1. wait()Object 슈퍼클래스의 메서드이며 스레드 Thread 클래스의 메서드 sleep()입니다.
  2. 잠금 장치가 다르면 wait()잠금이 해제되지만 sleep()잠금이 해제되지 않습니다.
  3. 깨우기 방법은 정확히 동일하지 않으며, 또는 , 중단 wait()에 의존 하고 깨우기 위해 지정된 시간에 도달하고 깨우기 위해 지정된 시간에 도달합니다.notifynotifyAll sleep()
  4. 호출은 먼저 wait()객체의 잠금을 획득해야 합니다.Thread.sleep()

Runnable과 Callable의 차이점은 무엇입니까?

  • Callable 인터페이스 메서드는 이고 call()Runnable 메서드는 입니다 run().
  • Callable 인터페이스의 call 메서드는 반환 값을 가지며 제네릭을 지원하지만 Runnable 인터페이스의 run 메서드는 반환 값이 없습니다.
  • 호출 가능한 인터페이스 call()메서드는 예외를 throw할 수 있지만 Runnable 인터페이스 run()메서드는 계속해서 예외를 throw할 수 없습니다.

스레드 실행 순서를 제어하는 ​​방법은 무엇입니까?

세 개의 스레드 T1, T2 및 T3이 있다고 가정할 때 T1이 실행된 후 T2가 실행되고 T2가 실행된 후 T3이 실행되도록 하려면 어떻게 해야 합니까?

조인 방법을 사용하여 이 문제를 해결할 수 있습니다 . 예를 들어 스레드 A에서 스레드 B의 조인 메서드를 호출하는 것은 **: A가 스레드 B의 실행이 완료될 때까지 기다렸다가(CPU 실행 권한을 해제함) 실행을 계속한다는 것을 의미합니다. **

아래와 같이 코드 쇼:

public class ThreadTest {

    public static void main(String[] args) {

        Thread spring = new Thread(new SeasonThreadTask("春天"));
        Thread summer = new Thread(new SeasonThreadTask("夏天"));
        Thread autumn = new Thread(new SeasonThreadTask("秋天"));

        try
        {
            //春天线程先启动
            spring.start();
            //主线程等待线程spring执行完,再往下执行
            spring.join();
            //夏天线程再启动
            summer.start();
            //主线程等待线程summer执行完,再往下执行
            summer.join();
            //秋天线程最后启动
            autumn.start();
            //主线程等待线程autumn执行完,再往下执行
            autumn.join();
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

class SeasonThreadTask implements Runnable{

    private String name;

    public SeasonThreadTask(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 1; i <4; i++) {
            System.out.println(this.name + "来了: " + i + "次");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

작업 결과:

春天来了: 1次
春天来了: 2次
春天来了: 3次
夏天来了: 1次
夏天来了: 2次
夏天来了: 3次
秋天来了: 1次
秋天来了: 2次
秋天来了: 3次

데몬 스레드란 무엇입니까?

데몬 스레드는 백그라운드에서 실행되는 특수 프로세스 입니다 . 제어 단말과는 독립적이며 주기적으로 어떤 작업을 수행하거나 어떤 이벤트가 발생할 때까지 기다립니다. 가비지 수집 스레드는 Java의 특수 데몬 스레드입니다.

스레드 간 통신 방법

1. Object 클래스의 wait()/notify()를 사용합니다 . Object 클래스는 다중 스레드 통신 의 기반이 되는 스레드 간 통신을 위한 메서드( wait(), notify(), )를 제공합니다. notifyAll()그 중 반드시 wait/notify함께 synchronized사용해야 , wait 방식은 잠금을 해제하고, notify 방식은 잠금을 해제하지 않습니다. 대기는 동기화 잠금에 들어간 스레드에서 일시적으로 동기화 잠금을 포기하여 이 잠금을 기다리고 있는 다른 스레드가 동기화 잠금을 획득하고 실행할 수 있도록 하는 것을 의미합니다. 다른 스레드만 호출합니다. 알림은 해제하지 않습니다. 그러나 잠금 을 획득하기 위한 경쟁에 참여하도록 호출 스레드에 지시하지만 잠금이 아직 다른 사람의 손에 있고 상대방이 잠금을 해제하지 않았기 때문에 즉시 잠금을 얻지는 않습니다. 하나 이상의 스레드가 호출합니다 notify(). 대기 상태를 해제하고 경쟁에 다시 참여합니다 . 프로그램이 다시 잠금을 얻을 수 있으면 계속 아래로 실행할 수 있습니다.wait()wait()

2. 휘발성 키워드를 사용합니다. 스레드 간 통신은 volatile 키워드를 기반으로 구현되며 기본 계층은 공유 메모리를 사용합니다. 쉽게 말해 여러 스레드가 동시에 하나의 변수를 모니터링하고 변수가 변경되면 스레드가 해당 비즈니스를 인식하고 실행할 수 있습니다.

3. JUC 도구 클래스 CountDownLatch를 사용합니다 . jdk1.5 이후에는 java.util.concurrent동시성 프로그래밍과 관련된 많은 도구들이 패키지에 제공되어 동시성 프로그래밍 개발이 간편해졌는데, CountDownLatchAQS 프레임워크를 기반으로 스레드간 공유 변수 상태를 유지하는 것과 같다.

4. LockSupport를 기반으로 스레드 간 차단 및 웨이크업을 구현합니다 . LockSupport스레드 간 차단 및 깨우기에 매우 유연한 도구입니다. 실.

스레드로컬

스레드 로컬 변수. ThreadLocal유지되는 변수를 사용할 때 ThreadLocal변수를 사용하는 각 스레드는 변수의 독립적인 복사본을 제공하므로 각 스레드는 다른 스레드에 영향을 주지 않고 독립적으로 자신의 복사본을 변경할 수 있습니다.

ThreadLocal의 원리

각 스레드는 ThreadLocalMap( ThreadLocal내부 클래스), Map 요소의 키 ThreadLocal및 스레드의 변수 복사본에 해당하는 값을 가집니다.

호출 threadLocal.set()–> 호출 getMap(Thread)–> 반환 ThreadLocalMap<ThreadLocal, value>–> 현재 스레드의 이 자체 map.set(this, value)입니다 . threadLocal소스 코드는 다음과 같습니다.

public void set(T value) {
    
    
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    
    
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    
    
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Invoke get()–> Invoke getMap(Thread)–> 현재 스레드로 돌아가기 ThreadLocalMap<ThreadLocal, value>–> map.getEntry(this), return value. 소스 코드는 다음과 같습니다.

    public T get() {
    
    
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
    
    
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
    
    
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

threadLocals각 스레드에는 및 와 같이 여러 변수가 있을 수 있으므로 ThreadLocalMap키의 유형은 object입니다 .ThreadLocalthreadLocallongLocalstringLocal

public class ThreadLocalDemo {
    ThreadLocal<Long> longLocal = new ThreadLocal<>();

    public void set() {
        longLocal.set(Thread.currentThread().getId());
    }
    public Long get() {
        return longLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        threadLocalDemo.set();
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(() -> {
            threadLocalDemo.set();
            System.out.println(threadLocalDemo.get());
        }
        );

        thread.start();
        thread.join();

        System.out.println(threadLocalDemo.get());
    }
}

ThreadLocal각 스레드의 리소스는 복사본일 뿐 공유되지 않기 때문에 공유 리소스에 대한 다중 스레드 액세스 문제를 해결하는 데 사용되지 않습니다. 따라서 ThreadLocal스레드 내에서 매개변수 전달을 단순화하기 위한 스레드 컨텍스트 변수로 적합합니다.

ThreadLocal 메모리 누수의 원인은 무엇입니까?

각 스레드에는 ThreadLocalMap내부 속성이 있으며 맵의 키는 ThreaLocal약한 참조로 정의되고 값은 강한 참조 유형입니다. 가비지 수집 중에 키는 자동으로 복구되며 값의 복구는 Thread 개체의 수명 주기에 따라 달라집니다. 일반적 으로 스레드는 리소스를 절약하기 위해 스레드 풀을 통해 재사용되므로 스레드 개체의 수명 주기가 상대적으로 길어지므로 항상 강력한 참조 체인 관계가 있습니다. --> –> –> , 작업 Thread실행 , 값이 점점 더 커지고 해제되지 않아 결국 메모리 누수가 발생할 수 있습니다.ThreadLocalMapEntryValue

해결 방법: 사용할 때마다 메서드를 ThreadLocal호출 하고 해당 키-값 쌍을 수동으로 삭제하여 메모리 누수를 방지합니다.remove()

ThreadLocal의 사용 시나리오는 무엇입니까?

장면 1

ThreadLocal은 각 스레드 전용 개체를 저장하고 각 스레드에 대한 복사본을 생성하여 각 스레드가 다른 스레드의 복사본에 영향을 주지 않고 자체 복사본을 수정할 수 있도록 하여 스레드 안전을 보장하는 데 사용됩니다.

이 시나리오는 일반적으로 스레드에 안전하지 않은 도구 클래스를 저장하는 데 사용되며 사용되는 일반적인 클래스는 SimpleDateFormat입니다.

요구 사항이 500개의 스레드가 SimpleDateFormat을 사용해야 하는 경우 스레드 풀을 사용하여 스레드 재사용을 실현하지 않으면 너무 많은 메모리와 기타 리소스를 소비합니다.각 작업에 대해 simpleDateFormat 개체를 생성하면 즉, 500 작업은 500에 해당합니다. simpleDateFormat 객체. 하지만 너무 많은 객체를 생성하는 것은 오버헤드가 있고, 메모리에 너무 많은 객체가 동시에 존재하는 것도 메모리 낭비입니다. simpleDateFormat 개체를 추출하여 정적 변수로 전환할 수 있지만 이렇게 하면 스레드 보안이 불안정해집니다. 우리가 원하는 효과는 너무 많은 메모리를 낭비하는 것이 아니라 스레드 안전성을 보장하는 것입니다. 이 시점에서 ThreadLocal을 사용하여 이 목적을 달성할 수 있으며 각 스레드에는 고유한 simpleDateFormat 개체가 있습니다.

장면 2

ThreadLocal은 다른 방법이 정보를 보다 편리하게 얻을 수 있도록 각 스레드 내에서 정보를 독립적으로 저장해야 하는 시나리오에서 사용됩니다. 쓰레드마다 획득하는 정보는 다를 수 있으며, 이전에 실행한 메소드가 정보를 저장한 후 후속 메소드는 글로벌 변수의 개념과 유사하게 파라미터 전달을 거치지 않고 ThreadLocal을 통해 직접 정보를 획득할 수 있다.

예를 들어 Java 웹 애플리케이션에서 각 스레드는 고유한 별도의 인스턴스를 가지며 다음을 사용하여 구현할 Session수 있습니다.ThreadLocal

AQS의 원리

추상 대기열 동기화 프로그램 인 AQS는 AbstractQueuedSynchronizer공유 리소스에 대한 다중 스레드 액세스를 위한 일련의 동기화 프레임워크를 정의합니다. 일반적으로 사용되는 ReentrantLock/Semaphore/CountDownLatch.

AQS는 volatileint형 멤버 변수를 사용하여 state동기화 상태를 나타내고 CAS를 통해 동기화 상태 값을 수정합니다. 스레드가 잠금 메서드를 호출할 때 state=0이면 어떤 스레드도 공유 리소스의 잠금을 보유하고 있지 않으며 잠금을 얻을 수 있고 1이 state추가됨을 . 0 이 state아니면 현재 어떤 쓰레드가 공유변수를 사용하고 있고, 다른 쓰레드들은 동기화 큐에 합류하여 대기해야 함을 의미한다.

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

동기화 장치는 내부 동기화 큐(FIFO 양방향 큐)에 의존하여 동기화 상태 관리를 완료합니다.현재 스레드가 동기화 상태를 얻지 못하면 동기화 장치는 현재 스레드와 대기 상태(독점 또는 공유)를 노드(노드)에 추가하고 동기화 대기열에 추가하고 스핀합니다. 동기화 상태가 해제되면 첫 번째 노드의 후속 노드에 해당하는 스레드가 깨어나 다시 동기화 상태를 얻으려고 시도합니다. .

ReentrantLock은 어떻게 재진입을 달성합니까?

ReentrantLock싱크로나이저 동기화를 내부적으로 커스터마이즈하고, 잠금 시 CAS 알고리즘을 통해 쓰레드 객체를 이중 연결 리스트에 넣고, 락을 획득할 때마다 현재 유지 중인 쓰레드 ID가 현재 요청된 쓰레드 ID와 일치하는지 확인하여 일치한다면, 동기화 상태가 1씩 증가하여 현재 스레드가 잠금을 여러 번 획득했음을 나타냅니다.

소스 코드는 다음과 같습니다.

final boolean nonfairTryAcquire(int acquires) {
    
    
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    
    
        if (compareAndSetState(0, acquires)) {
    
    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
    
    
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

자물쇠의 분류

공정한 잠금 및 불공평한 잠금

개체 잠금은 스레드 액세스 순서 대로 획득됩니다 . synchronized불공평한 잠금입니다. Lock기본값은 불공평한 잠금입니다.공평한 잠금으로 설정할 수 있습니다.공평한 잠금은 성능에 영향을 미칩니다.

public ReentrantLock() {
    
    
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

공유 및 배타적 잠금

공유형과 배타형 의 주요 차이점은 배타형 은 하나의 스레드 만 동시에 동기화 상태를 얻을 수 있는 반면, 공유형은 여러 스레드가 동시에 동기화 상태를 얻을 수 있다는 것입니다 . 예를 들어 읽기 작업은 동시에 여러 스레드에서 수행할 수 있는 반면 쓰기 작업은 한 번에 하나의 스레드에서만 수행할 수 있으며 다른 작업은 차단됩니다.

비관적 잠금과 낙관적 잠금

비관적 잠금, 리소스 에 액세스할 때마다 잠기고 동기화 코드가 실행된 후 잠금이 해제되며 비관적 잠금에 속합니다.synchronizedReentrantLock

낙관적 잠금은 리소스를 잠그지 않습니다. 모든 스레드는 동일한 리소스에 액세스하고 수정할 수 있습니다. 충돌이 없으면 수정이 성공하고 종료되며, 그렇지 않으면 루프에서 계속 시도합니다. 낙관적 잠금의 가장 일반적인 구현은 CAS.

적용 가능한 장면:

  • 비관적 잠금은 쓰기 작업이 많은 시나리오 에 적합합니다 .
  • 낙관적 잠금은 읽기 작업이 많은 시나리오 에 적합하며 잠금이 없으면 읽기 작업의 성능을 향상시킬 수 있습니다.

낙관적 잠금에 어떤 문제가 있습니까?

낙관적 잠금은 배타적 개체를 잠그는 비관적 문제를 피하고 동시성 성능을 향상시키지만 다음과 같은 단점도 있습니다.

  • 낙관적 잠금은 공유 변수 의 원자적 연산만 보장할 수 있습니다 .
  • 장시간 회전하면 높은 오버헤드가 발생할 수 있습니다 . CAS가 오랫동안 실패하고 계속 회전하면 CPU에 많은 오버헤드가 발생합니다.
  • ABA 문제 . CAS의 원리는 메모리 값이 예상 값과 동일한지 비교하여 메모리 값이 변경되었는지 여부를 판단하는 것이지만 다음과 같은 문제가 발생합니다. 메모리 값이 원래 A이면 B로 변경됩니다. 스레드에 의해 A 로 변경되면 CAS는 메모리 값이 변경되지 않았다고 생각합니다. 이 문제를 해결하기 위해 버전 번호를 도입할 수 있으며 버전 번호는 변수가 업데이트될 때마다 하나씩 증가합니다.

CAS 란 무엇입니까?

CAS의 전체 이름인 Compare And Swap비교 및 ​​교환은 낙관적 잠금의 주요 구현 방법입니다. CAS는 잠금을 사용하지 않고 여러 스레드 간의 변수 동기화를 실현합니다. ReentrantLock내부 AQS와 원자 클래스 모두 내부적으로 CAS를 사용합니다.

CAS 알고리즘에는 세 개의 피연산자가 포함됩니다.

  • 읽고 써야 하는 메모리 값 V.
  • 비교할 값 A입니다.
  • 쓸 새 값 B입니다.

V의 값이 A와 같은 경우에만 V의 값이 새로운 값 B로 원자적으로 업데이트되고, 그렇지 않으면 값이 성공적으로 업데이트될 때까지 계속 재시도합니다.

예를 들어 AtomicInteger, 메서드 AtomicInteger의 맨 아래 레이어 getAndIncrement()는 CAS 구현이고, 키 코드는 이고 compareAndSwapInt(obj, offset, expect, update), 그 의미는 obj내부 value합계 expect가 같으면 다른 스레드가 이 변수를 변경하지 않았 update음을 증명한 다음 로 업데이트하고, 같지 않으면 값이 성공적으로 업데이트될 때까지 계속해서 다시 시도합니다.

CAS에 문제가 있습니까?

CAS 세 가지 주요 질문:

  1. ABA 문제 . CAS는 값을 연산할 때 메모리 값이 변경되었는지 확인해야 하며 변경 사항이 없으면 메모리 값을 업데이트합니다. 그러나 메모리 값이 원래 A였다가 B로 변경되었다가 다시 A로 변경된 경우 CAS에서 확인하면 값이 변경된 것이 아니라 실제로 변경된 것으로 확인됩니다. ABA 문제에 대한 해결책은 변수 앞에 버전 번호를 추가하고 변수가 업데이트될 때마다 버전 번호에 1을 추가하여 변경 프로세스가 에서 변경되도록 하는 것 A-B-A입니다 1A-2B-3A.

    JDK는 ABA 문제를 해결하기 위해 1.5부터 AtomicStampedReference 클래스와 버전 번호가 있는 원자성 업데이트 참조 유형을 제공했습니다.

  2. 긴 주기 시간은 비용이 많이 듭니다 . CAS 작업이 오랫동안 실패하면 항상 회전하게 되어 CPU에 매우 큰 오버헤드가 발생합니다.

  3. 원자적 연산은 하나의 공유 변수에 대해서만 보장될 수 있습니다 . 공유 변수에 대한 작업을 수행할 때 CAS는 원자성 작업을 보장할 수 있지만 여러 공유 변수에 대한 작업을 수행할 때 CAS는 작업의 원자성을 보장할 수 없습니다.

    Java 1.5부터 JDK는 참조된 개체 간의 원자성을 보장하기 위해 AtomicReference 클래스를 제공했으며 CAS 작업을 위해 여러 변수를 하나의 개체에 배치할 수 있습니다.

원자 클래스

기본 유형 원자 클래스

기본 유형을 원자적으로 업데이트

  • AtomicInteger: 정수 원자 클래스
  • AtomicLong: 긴 정수 원자 클래스
  • AtomicBoolean: 부울 원자 클래스

AtomicInteger 클래스의 일반적으로 사용되는 메서드:

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicInteger 클래스는 주로 CAS(비교 및 교환)를 사용하여 원자성 작업을 보장하므로 잠금의 높은 오버헤드를 방지합니다.

배열 유형 원자 클래스

배열의 요소를 원자적으로 업데이트

  • AtomicIntegerArray: 정수 배열 원자 클래스
  • AtomicLongArray: 긴 정수 배열 원자 클래스
  • AtomicReferenceArray: 참조 유형 배열 원자 클래스

AtomicIntegerArray 클래스의 일반 메서드:

public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

참조 유형 원자 클래스

  • AtomicReference: 참조 유형 원자 클래스
  • AtomicStampedReference: 버전 번호가 있는 참조 유형 원자 클래스입니다. 이 클래스는 원자 업데이트 데이터와 데이터의 버전 번호를 해결하는 데 사용할 수 있는 참조와 정수 값을 연결하고 원자 업데이트에 CAS를 사용할 때 발생할 수 있는 ABA 문제를 해결할 수 있습니다.
  • AtomicMarkableReference : 표시로 원자적으로 업데이트되는 참조 유형입니다. 이 클래스는 부울 토큰을 참조와 연결합니다.

Executor 스레드 풀 프레임워크를 사용하는 이유는 무엇입니까?

  • 태스크가 실행될 때마다 new Thread()를 통해 스레드가 생성되어 성능을 소모하게 되는데, 스레드 생성에는 시간과 자원이 많이 소모된다.
  • new Thread()를 호출하여 생성된 쓰레드는 관리가 부족하여 제한 없이 생성이 가능하며, 쓰레드 간의 경쟁으로 인해 시스템 리소스가 과도하게 점유되어 시스템이 마비될 수 있음
  • new Thread()를 사용하여 직접 시작한 스레드는 타이밍 실행, 주기적 실행, 타이밍 및 주기적 실행, 스레드 중단 등과 같은 확장에 도움이 되지 않습니다. 구현하기 쉽지 않습니다.

실행 중인 스레드를 중지하는 방법은 무엇입니까?

  1. 공유 변수를 사용하는 방법. 공유 변수는 동일한 작업을 실행하는 여러 스레드에서 스레드 실행을 중지하는 신호로 사용할 수 있습니다.
  2. 인터럽트 방법을 사용하여 스레드를 종료하십시오. 쓰레드가 차단되어 비실행 상태일 때 메인 프로그램에서 쓰레드의 공유 변수를 true로 설정하더라도 이때 쓰레드는 루프 플래그를 전혀 확인할 수 없으며 당연히 인터럽트가 되지 않는다. 즉시. 이때 Thread에서 제공하는 interrupt() 메서드를 사용할 수 있는데, 이 메서드는 실행 중인 스레드를 방해하지는 않지만 차단된 스레드가 인터럽트 예외를 발생시켜 스레드가 차단 상태를 조기에 종료할 수 있기 때문입니다.

데몬 스레드란 무엇입니까?

백그라운드(데몬) 쓰레드는 프로그램이 실행될 때 백그라운드에서 일반적인 서비스를 제공하는 쓰레드를 말하며, 이 쓰레드는 프로그램의 필수적인 부분이 아니다. 따라서 백그라운드가 아닌 스레드가 모두 종료되면 프로그램이 종료되어 프로세스의 모든 백그라운드 스레드가 종료됩니다. 반대로 백그라운드가 아닌 스레드가 계속 실행 중인 한 프로그램은 종료되지 않습니다. 스레드가 백그라운드 스레드로 설정하기 시작하기 전에 setDaemon() 메서드를 호출해야 합니다.

참고: 백그라운드 프로세스는 finally 절을 실행하지 않고 run() 메서드를 종료합니다.

예를 들어 JVM의 가비지 수집 스레드는 Daemon 스레드이고 Finalizer도 Daemon 스레드입니다.

SynchronizedMap과 ConcurrentHashMap의 차이점은 무엇입니까?

SynchronizedMap은 스레드 안전을 보장하기 위해 한 번에 전체 테이블을 잠그므로 한 번에 하나의 스레드만 맵에 액세스할 수 있습니다.

JDK1.8 ConcurrentHashMap은 동시 보안을 보장하기 위해 CAS와 동기화를 사용합니다. 데이터 구조는 배열 + 연결 목록/빨간색 및 검은색 이진 트리를 채택합니다. synchronized는 현재 연결된 목록 또는 red-black 이진 트리의 첫 번째 노드만 잠그고 동시 액세스 및 수정을 지원합니다.
또한 ConcurrentHashMap은 다른 반복 방법을 사용합니다. Iterator가 생성되고 컬렉션이 다시 변경되면 ConcurrentModificationException은 더 이상 발생하지 않지만 Iterator가 변경되면 원래 데이터에 영향을 미치지 않도록 새로운 새 데이터로 대체됩니다.Iterator가 완료된 후 헤드 포인터는 새 데이터로 대체되어 반복자 스레드가 원래의 이전 데이터를 사용할 수 있고 쓰기 스레드도 동시에 변경을 완료할 수 있습니다.

스레드 풀의 작업이 실행되었는지 여부를 판단하는 방법은 무엇입니까?

몇 가지 방법이 있습니다.

1. 스레드 풀의 기본 함수 isTerminated()를 사용합니다 .

실행기는 스레드 풀의 모든 작업이 완료되었는지 여부를 확인하기 위해 네이티브 함수 isTerminated()를 제공합니다. 모든 작업이 완료되면 true를 반환하고 그렇지 않으면 false를 반환합니다.

2. 공통 카운트를 유지하기 위해 재진입 잠금을 사용합니다 .

모든 일반 작업은 카운터를 유지하며 작업이 완료되면 카운터가 1씩 증가합니다.(여기서는 잠금이 필요합니다.) 카운터의 값이 작업의 수와 같으면 모든 작업이 실행된 것입니다.

3. CountDownLatch를 사용합니다 .

그 원리는 두 번째 방법과 유사하며, CountDownLatch에 카운트 값을 주고 태스크가 실행된 후 countDown()을 호출하여 카운트 값 -1을 실행합니다. 마지막으로 실행된 작업은 호출 메서드의 시작 부분에서 await() 메서드를 호출하므로 계속 실행하기 전에 카운트 값이 0이 될 때까지 전체 작업이 차단됩니다.

이 방법의 단점은 작업의 수를 미리 알아야 한다는 것입니다.

4. submit은 작업을 스레드 풀에 제출하고 Future를 사용하여 작업의 실행 상태를 판단합니다 .

제출을 사용하여 스레드 풀에 작업을 제출하는 것은 실행으로 제출하는 것과 다릅니다. 제출은 Future 유형의 반환 값을 갖습니다. future.isDone() 메서드를 통해 작업 완료 여부를 알 수 있습니다.

선물이란 무엇입니까?

동시 프로그래밍에서는 스레드 클래스를 상속하든 실행 가능한 인터페이스를 구현하든 이전 실행 결과를 얻을 수 있다고 보장할 수 없습니다. Callback 인터페이스를 구현하고 Future를 사용하여 여러 스레드의 실행 결과를 수신합니다.

Future는 완료되지 않았을 수 있는 비동기 작업의 결과를 나타내며 작업이 성공하거나 실패한 후 해당 작업을 수행하기 위해 이 결과에 콜백을 추가할 수 있습니다.

예를 들어 아침을 먹으러 갈 때 빵과 냉채를 주문하면 빵은 3분, 냉채는 1분만 기다리면 된다. 동시에 찬 요리를 준비하는 동안 빵을 동시에 준비할 수 있으므로 3분만 기다리면 됩니다. Future는 후자의 실행 모드입니다.

Future 인터페이스는 주로 5가지 방법을 포함합니다.

  1. get() 메서드는 작업이 완료되면 결과를 반환할 수 있으며 호출 시 작업이 완료되지 않으면 작업이 완료될 때까지 스레드가 차단됩니다.
  2. get(long timeout, TimeUnit unit)은 시간 초과 시간을 기다린 후 결과를 반환합니다.
  3. cancel(boolean mayInterruptIfRunning) 메서드는 작업을 중지하는 데 사용할 수 있습니다. 작업을 중지할 수 있는 경우(mayInterruptIfRunning으로 판단) true를 반환할 수 있습니다. 작업이 완료되었거나 중지되었거나 작업을 중지할 수 없는 경우 작업이 중지됩니다. 거짓을 반환합니다.
  4. 현재 메서드가 완료되었는지 확인하는 isDone() 메서드
  5. isCancel() 현재 메서드가 취소되었는지 확인하는 메서드

마지막으로 C 언어, C++, Java, Python, 프런트 엔드, 데이터베이스, 운영 체제, 컴퓨터 네트워크, 데이터 구조를 포함하여 Dabin이 컴파일한 300개 이상의 고전 컴퓨터 책 PDF가 있는 Github 창고를 공유하고 싶습니다. 그리고 알고리즘, 머신러닝, 프로그래밍 라이프 등 별표를 붙일 수 있고 다음에 책을 직접 검색하면 창고가 계속 업데이트 됩니다~

Github 주소

추천

출처blog.csdn.net/Tyson0314/article/details/131038016