동시 프로그래밍에 대한 자세한 설명이 하나의 기사에 담겨 있습니다.

동시 프로그래밍에 대한 자세한 설명

나는 최근에 다음을 연구했습니다: Binghe의 "고동시성 프로그래밍에 대한 심층적 이해" , "동시 프로그래밍의 예술", 동시
프로그래밍 지식의 후속 개선 및 통합을 촉진하기 위해 연구를 간략하게 요약합니다.
깊이에 대해서는 위의 참조 원본을 읽을 수 있습니다.

스레드 및 스레드 풀

프로세스

프로세스는 시스템에서 자원 할당의 기본 단위입니다. 최신 운영 체제는 프로그램을 실행할 때 해당 프로그램에 대한 프로세스를 생성합니다.

프로세스는 실행 중인 프로그램인 메모리에서 애플리케이션이 할당한 공간으로, 각 프로세스는 서로 간섭하지 않습니다.

프로세스 + CPU 타임 슬라이스 회전을 사용하는 운영 체제는 매크로 수준에서 동시에 여러 작업을 처리할 수 있습니다. 즉, 프로세스는 운영 체제의 동시성을 가능하게 합니다. 동시성은 거시적 수준으로 보이지만 여러 작업이 실행되고 있지만 실제로 단일 코어 CPU의 경우 특정 순간에 하나의 작업만 CPU 리소스를 점유합니다.

스레드는 CPU 스케줄링의 기본 단위입니다. 프로세스보다 작고 독립적으로 실행할 수 있는 단위입니다.

하나의 프로세스에서 여러 개의 스레드가 생성될 수 있으며, 이러한 스레드는 자신만의 전용 카운터, 스택 메모리 및 지역 변수를 가질 수 있으며 공유 메모리 변수에 액세스할 수 있습니다.

멀티스레딩

동일한 프로그램에서 여러 스레드를 동시에 실행하여 다양한 작업을 수행할 수 있으며, 이러한 스레드는 CPU의 여러 코어를 사용하여 동시에 실행할 수 있습니다.

멀티스레딩을 사용하는 이유는 무엇입니까? 가장 중요한 것은 멀티 스레드 프로그래밍이 멀티 코어 CPU 리소스의 사용을 극대화할 수 있다는 것입니다.

컨텍스트 스위치

컨텍스트 전환은 CPU가 한 프로세스(또는 스레드)에서 다른 프로세스(또는 스레드)로 전환하는 것을 의미합니다.

컨텍스트는 특정 순간의 CPU 레지스터 및 프로그램 카운터의 내용을 나타냅니다.

컨텍스트 전환은 일반적으로 계산 집약적입니다. 즉, 이 작업은 많은 CPU 시간을 소비하므로 더 많은 스레드가 항상 더 좋은 것은 아닙니다 . 시스템의 컨텍스트 스위치 수를 어떻게 줄이는지는 멀티스레딩 성능을 향상시키기 위한 중요한 주제입니다.

스레드 구현 방법

  1. Thread 클래스 상속

    public class ThreadDemo {
          
          
    
        public static class TestThread extends Thread {
          
          
            @Override
            public void run() {
          
          
                System.out.println("extends Thread");
            }
        }
    
        public static void main(String[] args) {
          
          
            TestThread testThread = new TestThread();
            testThread.start();
        }
    
        //console:extends Thread
    }
    
  2. Runnable 인터페이스 구현

    public class ThreadDemo {
          
          
    
        public static class TestThreadRun implements Runnable {
          
          
            @Override
            public void run() {
          
          
                System.out.println("extends Thread2");
            }
        }
    
        public static void main(String[] args) {
          
          
            Thread thread = new Thread(new TestThreadRun());
            thread.start();
        }
    
        //console:extends Thread2
    }
    
  3. 호출 가능 인터페이스 구현

    public class ThreadDemo {
          
          
    
        public static class TestThreadCall implements Callable<String> {
          
          
            @Override
            public String call() {
          
          
                System.out.println("extends Thread2");
                return "result:do success";
            }
        }
    
        public static void main(String[] args) {
          
          
            TestThreadCall call = new TestThreadCall();
            String result = call.call();
            System.out.println(result);
        }
    
        //console:
        // extends Thread2
        //result:do success
    }
    

스레드 우선순위

Java에서 스레드 우선순위는 1부터 10까지의 정수 멤버 변수 우선순위를 통해 제어됩니다. 값이 클수록 우선순위가 높아집니다. 기본값은 5입니다.

우선순위가 높은 스레드에 CPU 시간 조각이 할당되면 우선순위가 낮은 스레드보다 할당 확률이 높습니다.

참고: 프로그램 정확성을 위해 스레드 우선순위를 신뢰할 수 없습니다 (높은 우선순위가 낮은 우선순위보다 먼저 실행된다는 보장은 없습니다).

스레드 실행 순서

같은 방법으로 여러 스레드가 연속적으로 생성된 후에는 스레드의 start() 메서드가 호출되는 순서에 따라 스레드의 실행 순서가 결정되지 않습니다.

스레드의 실행 순서를 보장하는 방법은 무엇입니까?

  1. Thread 클래스의 Join() 메서드를 사용하면 스레드의 실행 순서를 확인할 수 있습니다. Join()은 실제로 메인 스레드가 현재 하위 스레드가 실행을 완료할 때까지 기다리게 만듭니다 .
  2. Join() 메소드는 내부적으로 로컬 wait() 메소드를 호출합니다. 스레드 wait() 메소드가 호출되면 메인 스레드는 실행 전에 하위 스레드가 실행을 완료하기를 기다리는 대기 상태가 됩니다.

스레드 수명주기

스레드 수명 주기의 몇 가지 중요한 상태:

  1. NEW: 스레드가 생성되었지만 start() 메서드가 호출되지 않았습니다.

  2. RUNNABLE: 준비 상태 및 실행 상태를 포함한 실행 가능한 상태입니다.

  3. BLOCKED: 차단된 상태 이 상태의 스레드는 다른 스레드가 잠금을 해제하거나 동기화에 들어갈 때까지 기다려야 합니다.

  4. WAITTING: 대기 상태 이 상태의 스레드는 다음 상태로 들어가기 전에 다른 스레드가 깨어나거나 작업을 중단할 때까지 기다려야 합니다.

    다음 세 가지 메서드를 호출하면 스레드가 대기 상태가 됩니다.

    1. Object.wait(): 다른 스레드가 깨어날 때까지 현재 스레드를 대기 상태로 둡니다.
    2. Thread.join(): 스레드가 실행을 완료할 때까지 기다립니다. 기본 호출은 Object의 wait() 메서드입니다.
    3. LockSupport.park(): 호출 권한이 부여되지 않는 한 스레드 예약을 위해 현재 스레드를 비활성화합니다.
  5. TIME_WAITTING: 지정된 시간 이후에 스스로 반환될 수 있는 타임아웃 대기 상태.

  6. TERMINATED: 스레드 실행이 완료되었음을 나타내는 종료 상태입니다.

스레드 수명주기 흐름도(약어):

여기에 이미지 설명을 삽입하세요.

참고: 프로덕션 환경에서 Java 스레드 예외 정보를 스레드로 분석하려면 jstack 명령과 결합된 jps를 사용하십시오.

Thread 클래스에 대한 깊은 이해

스레드 클래스 정의

public class Thread implements Runnable {
    
    ...}

@FunctionalInterface
public interface Runnable {
    
    
    public abstract void run();
}

Thread 클래스는 Runnable 인터페이스를 구현하고 Rnnnable 인터페이스에는 run() 메서드가 하나만 있으며 @FunctionalInterface 주석으로 수정됩니다.

로컬 리소스 로드

/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
    
    
    registerNatives();
}

이 메서드는 주로 일부 로컬 리소스를 로드하고 정적 코드 블록에서 이 로컬 메서드를 호출하는 데 사용됩니다.

스레드 멤버 변수

 //线程名称
 private volatile String name;
 //优先级
 private int            priority;
 private Thread         threadQ;
 private long           eetop;

 /* Whether or not to single_step this thread. */
 //是否单步线程
 private boolean     single_step;

 /* Whether or not the thread is a daemon thread. */
 //是否守护线程
 private boolean     daemon = false;

 /* JVM state */
 private boolean     stillborn = false;

 /* What will be run. */
 //实际执行体
 private Runnable target;

 /* The group of this thread */
 private ThreadGroup group;

 /* The context ClassLoader for this thread */
 private ClassLoader contextClassLoader;

 /* The inherited AccessControlContext of this thread */
 //访问控制上下文
 private AccessControlContext inheritedAccessControlContext;

 /* For autonumbering anonymous threads. */
 //为匿名线程生成名称的编号
 private static int threadInitNumber;
 private static synchronized int nextThreadNum() {
    
    
     return threadInitNumber++;
 }

 /* ThreadLocal values pertaining to this thread. This map is maintained
  * by the ThreadLocal class. */
 //与此线程相关的ThreadLocal
 ThreadLocal.ThreadLocalMap threadLocals = null;

 /*
  * InheritableThreadLocal values pertaining to this thread. This map is
  * maintained by the InheritableThreadLocal class.
  */
 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

 //当前线程请求的堆栈大小
 private long stackSize;
 //线程终止后存在的JVM私有状态
 private long nativeParkEventPointer;
 //线程ID
 private long tid;

 /* For generating thread ID */
 //用于生成线程ID
 private static long threadSeqNumber;

 /* Java thread status for tools,
  * initialized to indicate thread 'not yet started'
  */
 //线程状态,初始为0,表示未启动
 private volatile int threadStatus = 0;

 /**
  * The argument supplied to the current call to
  * java.util.concurrent.locks.LockSupport.park.
  * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
  * Accessed using java.util.concurrent.locks.LockSupport.getBlocker
  */
 volatile Object parkBlocker;

 /* The object in which this thread is blocked in an interruptible I/O
  * operation, if any.  The blocker's interrupt method should be invoked
  * after setting this thread's interrupt status.
  */
 //Interruptible中定义了中断方法,用来中断特定线程
 private volatile Interruptible blocker;
 //当前线程的内部锁
 private final Object blockerLock = new Object();
 //线程最小优先级
 public final static int MIN_PRIORITY = 1;
 //默认优先级
 public final static int NORM_PRIORITY = 5;
 //最大优先级
 public final static int MAX_PRIORITY = 10;

멤버 변수를 보면 Thread 클래스가 태스크가 아닌 실제 스레드 객체임을 알 수 있으며, 그 안에 있는 Runnable 타입의 대상 변수가 실제 태스크임을 알 수 있다.

스레드 상태 정의

public enum State {
    
    
    /**
     * Thread state for a thread which has not yet started.
     */
    //新建状态;线程被创建,但是还没有调用start()方法
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    //可运行状态;包括运行中状态和就绪状态
    RUNNABLE,

    //阻塞状态;此状态的线程需要等待其他线程释放锁,或者等待进入synchronized
    BLOCKED,

    //等待状态;此状态的线程需要其他线程对其进行唤醒或者中断状态,进而进入下一状态
    WAITING,

    //超时等待状态;可以在一定的时间自行返回
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    //终止状态;当前线程执行完毕
    TERMINATED;
}

Thread 클래스의 생성자 메서드

public Thread() {
    
    
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

public Thread(Runnable target) {
    
    
        init(null, target, "Thread-" + nextThreadNum(), 0);
}

public Thread(ThreadGroup group, Runnable target) {
    
    
        init(group, target, "Thread-" + nextThreadNum(), 0);
}

public Thread(String name) {
    
    
        init(null, null, name, 0);
}

public Thread(ThreadGroup group, String name) {
    
    
        init(group, null, name, 0);
}

public Thread(ThreadGroup group, Runnable target, String name) {
    
    
        init(group, target, name, 0);
}

Thread 클래스의 일반적으로 사용되는 여러 구성 방법을 통해 Thread 클래스의 초기화가 주로 init() 메서드를 통해 수행된다는 것을 발견했습니다.

초기화() 메서드

/**
 * Initializes a Thread.
 *
 * @param g the Thread group
 * @param target the object whose run() method gets called
 * @param name the name of the new Thread
 * @param stackSize the desired stack size for the new thread, or
 *        zero to indicate that this parameter is to be ignored.
 * @param acc the AccessControlContext to inherit, or
 *            AccessController.getContext() if null
 * @param inheritThreadLocals if {@code true}, inherit initial values for
 *            inheritable thread-locals from the constructing thread
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    
    
    if (name == null) {
    
    
        //名称为空
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread();
    //安全管理器
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
    
    
        /* Determine if it's an applet or not */

        /* If there is a security manager, ask the security manager
           what to do. */
        if (security != null) {
    
    
            //获取线程组
            g = security.getThreadGroup();
        }

        /* If the security doesn't have a strong opinion of the matter
           use the parent thread group. */
        if (g == null) {
    
    
            //线程组为空,从父类获取
            g = parent.getThreadGroup();
        }
    }

    /* checkAccess regardless of whether or not threadgroup is
       explicitly passed in. */
    g.checkAccess();

    /*
     * Do we have the required permissions?
     */
    if (security != null) {
    
    
        if (isCCLOverridden(getClass())) {
    
    
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();
    //当前线程继承父线程相关属性
    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    tid = nextThreadID();
}

Thread 클래스의 생성 메소드는 Thread 스레드를 생성하는 메인 스레드에 의해 호출되는데, 이때 생성 메소드를 호출하는 메인 스레드는 Thread의 부모 스레드이고, init() 메소드에서는 새로 생성된 Thread 스레드이다. 상위 스레드의 일부를 상속합니다.

실행() 메소드

@Override
public void run() {
    
    
    if (target != null) {
    
    
        target.run();
    }
}

Thread 메소드의 run() 메소드 구현은 비교적 간단하다는 것을 알 수 있는데, 실제로는 Runnable 타입 타겟에서 run() 메소드를 실행함으로써 구현된다.

Runnable 인터페이스의 run 메소드를 직접 호출한다고 해서 작업을 수행하기 위한 새 스레드가 생성되지는 않습니다. 작업을 수행하기 위해 새 스레드를 생성해야 하는 경우 Thread의 start() 메소드를 호출해야 합니다. 수업;

시작() 메서드

public synchronized void start() {
    
    
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);
    //标记线程是否启动
    boolean started = false;
    try {
    
    
        //调用本地方法启动
        start0();
        //变更线程启动标识
        started = true;
    } finally {
    
    
        try {
    
    
            if (!started) {
    
    
                //未启动,则线程组中标记为启动失败
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
    
    
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

소스코드를 보면 start() 메서드가 동기화됨으로 수정되어 이 메서드가 동기식임을 나타내며 스레드가 실제로 시작되기 전에 스레드의 상태를 확인합니다. 0이 아닌 경우(NEW 상태) , 직접 예외를 반환하므로 A 스레드는 한 번만 시작할 수 있습니다. 여러 번 시작하면 예외가 보고됩니다 .

start() 메소드를 호출한 후 새로 생성된 스레드는 준비 상태가 됩니다.(CPU에 의해 예약되지 않은 경우) CPU가 유휴 상태인 경우 CPU에 의해 실행되도록 예약됩니다. 스레드가 실행 중 상태이고 JVM은 작업을 수행하기 위해 스레드의 run() 메서드를 호출합니다.

sleep() 메소드

public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos) throws InterruptedException {
    
    
    if (millis < 0) {
    
    
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
    
    
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
    
    
        millis++;
    }

    sleep(millis);
}

sleep() 메소드는 스레드를 일정 시간 동안 휴면 상태로 만들며, 스레드를 휴면 상태로 만들기 위해 sleep() 메소드를 호출한 후에도 잠금이 해제되지 않는다는 점에 유의해야 합니다.

join() 방법

Join() 메서드의 사용 시나리오는 일반적으로 작업을 실행하기 위해 스레드를 시작하고, 실행 스레드의 Join() 메서드를 호출하고, 실행 스레드가 시간 초과되거나 실행이 종료될 때까지 작업을 실행하기를 기다리는 스레드입니다. 스레드가 종료됩니다.

interrupt() 방법

스레드 중단:

어떤 경우에는 스레드를 시작한 후 스레드를 실행할 필요가 없고 이 스레드를 중단해야 한다는 것을 알게 됩니다. 현재 JAVA에서는 스레드를 중지하는 안전하고 직접적인 방법이 없지만 JAVA는 스레드를 중단해야 하는 상황을 처리하기 위해 스레드 중단 메커니즘을 도입합니다.

스레드 중단 메커니즘은 협력 메커니즘입니다. 인터럽트 작업은 실행 중인 스레드를 직접 종료하지 않고, 중단된 스레드에게 스스로 처리하도록 알립니다.

스레드 중단을 처리하기 위해 Thread 클래스에는 여러 메서드가 제공됩니다.

  1. Thread.interrupt(): 스레드를 중단합니다. 여기서 인터럽트 스레드는 실행 중인 스레드를 즉시 종료하지 않고 스레드 인터럽트 플래그를 true로 설정합니다.
  2. Thread.currentThread.isInterrupt(): 현재 스레드가 중단되었는지 테스트합니다. 스레드의 인터럽트 상태는 이 메서드의 영향을 받습니다. 즉, 이 메서드를 한 번 호출하면 스레드의 중단 상태가 true로 설정되고, 두 번 호출하면 스레드의 중단 상태가 false로 재설정됩니다.
  3. Thread.isInterrupt(): 현재 스레드가 중단되었는지 여부를 테스트합니다. 위의 방법과 달리 이 방법은 스레드 중단 상태를 변경하지 않습니다.

이 메서드는 현재 스레드의 실행을 중단하는 데 사용되며 스레드의 인터럽트 플래그 비트를 설정하여 현재 스레드를 중단합니다. 이때 해당 스레드에 인터럽트 플래그 비트가 설정되어 있으면,

InterruptException이 발생할 수 있으며 현재 스레드의 인터럽트 상태가 지워집니다. 스레드를 중단하는 이 방법은 스레드를 강제로 닫는 중지와 달리 실행 중인 작업이 계속 완료되도록 하는 더 안전합니다.

public void interrupt() {
    
    
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
    
    
        Interruptible b = blocker;
        if (b != null) {
    
    
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

만료된 일시중단()/resume()/stop()

suspens()/resume()/stop() 세 가지 메소드는 간단히 스레드 일시정지, 재개, 종료로 이해될 수 있으며, 오래된 메소드이므로 사용을 권장하지 않습니다.

호출 가능 및 미래

호출 가능한 인터페이스

Callable 인터페이스는 스레드 실행 후 반환 결과를 얻을 수 있지만 Thread를 상속하고 Runnable 인터페이스를 구현하는 동안 실행 결과를 얻을 수 없습니다.

@FunctionalInterface
public interface Runnable {
    
    
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}


@FunctionalInterface
public interface Callable<V> {
    
    
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Callable 인터페이스는 Runnable 인터페이스와 유사하다는 점을 알 수 있으며, 역시 하나의 메소드만을 갖는 기능적 인터페이스이지만 Callable에서 제공하는 메소드는 반환값을 가지며 제네릭을 지원한다는 점에서 차이점이 있다.

Callable은 일반적으로 스레드 하위 도구인 ExecutorService 클래스와 함께 사용되며, 다음 장에서 스레드 풀의 사용에 대해 자세히 알아볼 것입니다.

여기서는 ExecutorService가 submit 메소드를 호출하여 Callable을 실행하고 Future를 반환할 수 있다는 점만 소개하고, 후속 프로그램은 Future의 get 메소드를 통해 실행 결과를 얻을 수 있습니다.

public class TestTask implements Callable<String> {
    
    
    @Override
    public String call() throws Exception {
    
    
        //模拟程序执行需要一秒
        Thread.sleep(1000);
        return "do success!";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        ExecutorService executorService = Executors.newCachedThreadPool();
        TestTask testTask = new TestTask();
        Future<String> submitRes = executorService.submit(testTask);

        //注意调用get方法会阻塞当前线程,知道得到结果
        //实际编码中建议使用设有超时时间的重载get方法
        String reslut = submitRes.get();
        System.out.println(reslut);
    }
    
    //console:
    // do success!
}

비동기식 모델

  1. 반환 결과가 없는 비동기 모델

    결과를 반환하지 않는 비동기 작업은 스레드나 스레드 풀에 직접 던져서 실행할 수 있는데, 이때 작업 실행 결과를 직접 얻을 수는 없다.한 가지 방법은 콜백 메소드를 통해 실행 결과를 얻는 방법이며 구현 방법은 유사하다. 옵저버 모드로.

  2. 결과를 반환하는 비동기 모델

    JDK는 비동기 결과를 직접 얻고 반환할 수 있는 솔루션을 제공합니다.

    1. Future를 사용하여 결과 얻기

      Future 인터페이스는 비동기 결과를 얻기 위해 스레드 풀과 함께 사용되는 경우가 많습니다.

    2. FutureTask를 사용하여 결과 얻기

      FutureTask 클래스는 Thread 클래스 또는 스레드 풀과 함께 사용할 수 있습니다.

미래 인터페이스

  1. 미래 인터페이스

    public interface Future<V> {
          
          
        
        boolean cancel(boolean mayInterruptIfRunning);
    
        boolean isCancelled();
    
        boolean isDone();
       
        V get() throws InterruptedException, ExecutionException;
        
        V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeotException;
    
    1. 부울 취소(부울)
      1. 불리언 타입의 파라미터를 받아 작업 실행을 취소하고, 취소에 성공하면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
      2. 작업이 완료 또는 종료되었거나 취소할 수 없습니다. 취소에 실패했음을 나타내는 false를 반환합니다.
      3. 작업이 시작되지 않은 경우 이 메서드를 호출하면 취소 성공을 나타내는 true가 반환됩니다.
      4. 작업이 시작된 경우 입력된 부울 매개변수에 따라 스레드를 중단하여 작업을 취소할지 여부가 결정됩니다.
    2. 부울 isCancelled()
      1. 판단 작업이 완료되기 전에 취소됩니다.
      2. 작업이 완료되기 전에 취소되면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
      3. 참고: 작업이 시작되지 않고 완료되기 전에 취소된 경우에만 True가 반환되고, 그렇지 않으면 False가 반환됩니다.
    3. 부울 isDone()
      1. 작업이 완료되었는지 확인
      2. 작업이 정상적으로 종료되거나, 예외와 함께 종료되거나, 취소되면 true를 반환하여 작업이 완료되었음을 나타냅니다.
    4. V get()
      1. 작업이 완료되면 작업의 결과 데이터가 직접 반환됩니다.
      2. 완료되지 않은 경우 작업이 완료될 때까지 기다렸다가 결과를 반환합니다.
    5. V get(긴,TimeUnit)
      1. 작업이 완료되면 작업 완료 결과가 직접 반환됩니다.
      2. 완료되지 않은 경우 타임아웃 기간 내에 반환 결과를 기다리며, 시간이 초과되면 TimeOutException 예외가 발생한다.
  2. RunnableFuture 인터페이스

    public interface RunnableFuture<V> extends Runnable, Future<V> {
          
          
        /**
         * Sets this Future to the result of its computation
         * unless it has been cancelled.
         */
        void run();
    }
    

    RunnabeleFuture 인터페이스는 Future 인터페이스뿐만 아니라 Runnable 인터페이스도 상속하므로 두 가지 모두의 추상 인터페이스를 갖습니다.

  3. FutureTask 클래스

    public class FutureTask<V> implements RunnableFuture<V> {
          
          
        //详细源码,后续分析
    }
    

    FutureTask 클래스는 RunnableFuture 인터페이스의 매우 중요한 구현 클래스로 RunnableFuture 인터페이스, Future 인터페이스, Runnnable 인터페이스의 모든 추상 메소드를 구현한다.

    1. FutureTask 클래스의 변수 및 상수

      먼저, 휘발성 수정 변수 상태가 정의됩니다. 휘발성 변수는 메모리 장벽과 재정렬 금지를 통해 스레드 안전성을 달성합니다.

      그런 다음 작업이 실행 중일 때 여러 상태 상수를 정의합니다.

      (코드 주석에는 여러 가지 가능한 상태 변경이 나와 있습니다.)

      	/* Possible state transitions:
           * NEW -> COMPLETING -> NORMAL
           * NEW -> COMPLETING -> EXCEPTIONAL
           * NEW -> CANCELLED
           * NEW -> INTERRUPTING -> INTERRUPTED
           */
          private volatile int state;
          private static final int NEW          = 0;
          private static final int COMPLETING   = 1;
          private static final int NORMAL       = 2;
          private static final int EXCEPTIONAL  = 3;
          private static final int CANCELLED    = 4;
          private static final int INTERRUPTING = 5;
          private static final int INTERRUPTED  = 6;
      

      다음으로 여러 멤버 변수가 정의됩니다.

      	/** The underlying callable; nulled out after running */
          private Callable<V> callable;
          /** The result to return or exception to throw from get() */
          private Object outcome; // non-volatile, protected by state reads/writes
          /** The thread running the callable; CASed during run() */
          private volatile Thread runner;
          /** Treiber stack of waiting threads */
          private volatile WaitNode waiters;
      
      1. 호출 가능: run() 메소드를 호출하여 특정 작업을 수행합니다.
      2. outcaom: get() 메소드를 통해 얻은 반환 결과 또는 예외 정보
      3. 러너: 호출 가능한 인터페이스를 실행하고 CAS를 통해 스레드 안전성을 보장하는 데 사용되는 스레드
      4. waiters: 대기 중인 스레드의 스택 파생 클래스에서 CAS와 이 스택은 실행 상태를 전환하는 데 사용됩니다.
    2. 공법

      매개변수 전달을 위한 두 가지 다른 구성 방법.

      
          public FutureTask(Callable<V> callable) {
              
              
              if (callable == null)
                  throw new NullPointerException();
              this.callable = callable;
              this.state = NEW;       // ensure visibility of callable
          }
      
          /**
           * Creates a {@code FutureTask} that will, upon running, execute the
           * given {@code Runnable}, and arrange that {@code get} will return the
           * given result on successful completion.
           *
           * @param runnable the runnable task
           * @param result the result to return on successful completion. If
           * you don't need a particular result, consider using
           * constructions of the form:
           * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
           * @throws NullPointerException if the runnable is null
           */
          public FutureTask(Runnable runnable, V result) {
              
              
              this.callable = Executors.callable(runnable, result);
              this.state = NEW;       // ensure visibility of callable
          }
      
    3. isCancelled() 및 isDone()

          public boolean isCancelled() {
              
              
              return state >= CANCELLED;
          }
      
          public boolean isDone() {
              
              
              return state != NEW;
          }
      

      두 가지 방법 모두 상태 값의 크기를 판단하여 취소 또는 완료 여부를 결정합니다.

      여기서 배울 수 있는 것은 앞으로 상태 값을 정의할 때 특정 변경 규칙을 따르도록 노력하라는 것입니다. 이렇게 상태가 자주 변경되는 시나리오의 경우 일반 상태 값은 다음과 같은 결과를 두 배로 얻는 효과가 있을 수 있습니다. 비즈니스 로직을 수행할 때 노력이 절반으로 줄어듭니다.

    4. cancel(boolean) 방법

      public boolean cancel(boolean mayInterruptIfRunning) {
              
              
              if (!(state == NEW &&
                    UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                        mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
                  return false;
              try {
              
                  // in case call to interrupt throws exception
                  if (mayInterruptIfRunning) {
              
              
                      try {
              
              
                          Thread t = runner;
                          if (t != null)
                              t.interrupt();
                      } finally {
              
               // final state
                          UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                      }
                  }
              } finally {
              
              
                  finishCompletion();
              }
              return true;
          }
      

      첫째, 상태 판단이나 CAS 연산 결과를 토대로 취소 가능 여부를 빠르게 판단하고, 상태가 NEW가 아니거나 CAS가 false를 반환하면 취소 실패를 바로 반환한다.

      그런 다음 try 코드 블록에서 먼저 중단하여 취소할 수 있는지 여부를 결정하고, 그렇다면 실행 중인 작업을 가리키는 참조를 정의하고 작업이 비어 있는지 확인합니다. 그렇지 않은 경우 인터럽트 메서드를 호출한 다음 실행 중인 작업을 수정합니다. 취소할 상태;

      마지막으로 finally 코드 블록에서 FinishCompletion() 메서드를 호출하여 실행 중인 작업을 종료합니다.

      /**
           * Removes and signals all waiting threads, invokes done(), and
           * nulls out callable.
           */
          private void finishCompletion() {
              
              
              // assert state > COMPLETING;
              for (WaitNode q; (q = waiters) != null;) {
              
              
                  if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
              
              
                      for (;;) {
              
              
                          Thread t = q.thread;
                          if (t != null) {
              
              
                              q.thread = null;
                              LockSupport.unpark(t);
                          }
                          WaitNode next = q.next;
                          if (next == null)
                              break;
                          q.next = null; // unlink to help gc
                          q = next;
                      }
                      break;
                  }
              }
              done();
              callable = null;        // to reduce footprint
          }
      

      finishCompletion() 메소드에서는 for 루프가 먼저 정의되어 대기자(스레드 대기 스택)를 루핑하고 루프 종료 조건은 대기자가 비어 있다는 것이며, 특정 루프 내에서는 먼저 CAS 작업의 성공 여부를 확인하고 성공하면 성공합니다. , 새로운 스핀 루프를 정의합니다. 스핀 루프에서 스택의 스레드는 작업을 완료하기 위해 깨어납니다. 완료 후 break는 루프에서 점프하고 마지막으로 done() 메서드가 호출되고 콜러블이 호출됩니다. 비어 있다;

    5. get() 메소드

      
          public V get() throws InterruptedException, ExecutionException {
              
              
              int s = state;
              if (s <= COMPLETING)
                  s = awaitDone(false, 0L);
              return report(s);
          }
      
          public V get(long timeout, TimeUnit unit)
              throws InterruptedException, ExecutionException, TimeoutException {
              
              
              if (unit == null)
                  throw new NullPointerException();
              int s = state;
              if (s <= COMPLETING &&
                  (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
                  throw new TimeoutException();
              return report(s);
          }
      

      매개변수가 없는 get() 메소드는 작업이 완료되지 않을 때 차단하고 작업이 완료될 때까지 기다립니다. 매개변수가 있는 get() 메소드는 차단하고 완료를 기다리지만 지정된 시간이 초과되면 TimeoutException이 발생합니다. 던져졌다;

      /**
           * Awaits completion or aborts on interrupt or timeout.
           *
           * @param timed true if use timed waits
           * @param nanos time to wait, if timed
           * @return state upon completion
           */
          private int awaitDone(boolean timed, long nanos)
              throws InterruptedException {
              
              
              final long deadline = timed ? System.nanoTime() + nanos : 0L;
              WaitNode q = null;
              boolean queued = false;
              for (;;) {
              
              
                  if (Thread.interrupted()) {
              
              
                      removeWaiter(q);
                      throw new InterruptedException();
                  }
      
                  int s = state;
                  if (s > COMPLETING) {
              
              
                      if (q != null)
                          q.thread = null;
                      return s;
                  }
                  else if (s == COMPLETING) // cannot time out yet
                      Thread.yield();
                  else if (q == null)
                      q = new WaitNode();
                  else if (!queued)
                      queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                           q.next = waiters, q);
                  else if (timed) {
              
              
                      nanos = deadline - System.nanoTime();
                      if (nanos <= 0L) {
              
              
                          removeWaiter(q);
                          return state;
                      }
                      LockSupport.parkNanos(this, nanos);
                  }
                  else
                      LockSupport.park(this);
              }
          }
      

      waitDone() 메서드는 주로 실행이 완료되거나 중단될 때까지 기다립니다.

      가장 중요한 것은 for spin 루프입니다. 루프에서는 먼저 인터럽트 여부를 확인합니다. 인터럽트가 발생하면 RemoveWaiter()를 호출하여 대기 스택을 제거하고 인터럽트 예외를 발생시킵니다. 인터럽트가 발생하지 않으면 로직이 실행되어 완료되었는지 여부를 확인합니다.

    6. set() 및 setException()

      
          protected void set(V v) {
              
              
              if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
              
              
                  outcome = v;
                  UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
                  finishCompletion();
              }
          }
      
          protected void setException(Throwable t) {
              
              
              if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
              
              
                  outcome = t;
                  UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
                  finishCompletion();
              }
          }
      

      두 가지 방법의 논리는 작업 상태를 설정할 때 하나는 NORMAL로 설정되고 다른 하나는 EXCEPTIONAL로 설정된다는 점을 제외하면 거의 동일합니다.

    7. run() 및 runAndReset()

      public void run() {
              
              
              if (state != NEW ||
                  !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                               null, Thread.currentThread()))
                  return;
              try {
              
              
                  Callable<V> c = callable;
                  if (c != null && state == NEW) {
              
              
                      V result;
                      boolean ran;
                      try {
              
              
                          result = c.call();
                          ran = true;
                      } catch (Throwable ex) {
              
              
                          result = null;
                          ran = false;
                          setException(ex);
                      }
                      if (ran)
                          set(result);
                  }
              } finally {
              
              
                  // runner must be non-null until state is settled to
                  // prevent concurrent calls to run()
                  runner = null;
                  // state must be re-read after nulling runner to prevent
                  // leaked interrupts
                  int s = state;
                  if (s >= INTERRUPTING)
                      handlePossibleCancellationInterrupt(s);
              }
          }
      

      Future나 FutureTask()를 사용하면 작업을 실행하기 위해 필연적으로 run() 메서드가 호출된다고 할 수 있습니다.

      run() 메소드에서는 먼저 NEW 상태인지 아니면 CAS 연산이 false를 반환하는지 확인하고 실행을 계속하지 않고 직접 반환합니다.

      다음 try 코드 블록에서는 콜러블의 call() 메서드가 실행되고 결과가 수신됩니다.

    8. RemoveWaiter() 메서드

      
          private void removeWaiter(WaitNode node) {
              
              
              if (node != null) {
              
              
                  node.thread = null;
                  retry:
                  for (;;) {
              
                        // restart on removeWaiter race
                      for (WaitNode pred = null, q = waiters, s; q != null; q = s){
              
              
                          s = q.next;
                          if (q.thread != null)
                              pred = q;
                          else if (pred != null) {
              
              
                              pred.next = s;
                              if (pred.thread == null) // check for race
                                  continue retry;
                          }
                          else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                                q, s))
                              continue retry;
                      }
                      break;
                  }
              }
          }
      

      이 방법은 주로 스핀 루프를 통해 WaitNode(대기 스택)의 스레드를 제거합니다.

ThreadPoolExecutor에 대한 심층 분석

자바의 스레드 하위 기술은 자바의 핵심 기술 중 하나로, 자바 고도동시성 분야에서는 결코 피할 수 없는 화두이다.

Thread가 스레드를 직접 생성하는 방식의 단점

  1. 재사용이나 성능 저하 없이 new Thread()가 사용될 때마다 새 스레드가 생성됩니다.
  2. 스레드에는 통합 관리가 부족하고 제한 없이 새 스레드가 생성될 수 있으므로 많은 양의 리소스를 차지하고 OOM 또는 충돌이 발생할 수 있습니다.
  3. 추가 실행, 정기 실행, 중단 등과 같은 추가 제어 작업 부족

스레드 풀 사용의 이점

  1. 기존 스레드를 재사용할 수 있어 새 스레드로 인한 오버헤드가 줄어들고 성능이 향상됩니다.
  2. 최대 동시성 수를 효과적으로 제어하고, 리소스 활용도를 향상시키며, 너무 많은 스레드로 인한 리소스 경쟁을 줄일 수 있습니다.
  3. 예약 실행, 주기적 실행, 단일 스레드, 동시성 제어 등의 기능을 제공합니다.
  4. 스레드 풀의 실행 상태를 실시간으로 모니터링할 수 있는 스레드 풀 모니터링 지원 방법을 제공합니다.

스레드 풀

  1. 집행자
    1. newCachedThreadPool: 버퍼링 가능한 스레드 풀을 생성합니다. 스레드 풀의 크기가 처리 요구 사항을 초과하면 유휴 스레드를 유연하게 재활용할 수 있습니다. 재활용할 스레드가 없으면 새 스레드가 생성됩니다.
    2. newFixedThreadPool: 최대 동시 스레드 수를 제어할 수 있는 고정 길이 스레드 풀을 생성합니다. 고정 길이를 초과하는 스레드는 대기열에서 대기합니다.
    3. newScheduledThreadPool: 정기적 및 주기적으로 실행될 수 있는 고정 길이 스레드 풀을 만듭니다.
    4. newSingleThreadPool: 단일 스레드 스레드 풀을 생성하고 고유한 스레드를 사용하여 스레드 작업을 실행함으로써 모든 작업이 지정된 순서대로 실행되도록 합니다.
    5. newSingleScheduleThreadPool: 정기적 및 주기적으로 실행될 수 있는 단일 스레드 스레드 풀을 만듭니다.
    6. newWorkStealingThreadPool: 병렬 수준으로 작업 도용 스레드 풀을 생성합니다.

스레드 풀 인스턴스의 여러 상태

  1. 달리기

    실행 중 상태, 새로 제출된 작업을 수락할 수 있고 차단 대기열의 작업도 처리할 수 있음

  2. 일시 휴업

    닫힌 상태에서는 더 이상 새로 제출된 작업을 수락하지 않지만 차단 대기열에 저장된 작업을 처리할 수 있습니다.

    실행 중 --> shutdown() --> 종료

  3. 멈추다

    중지된 상태에서는 새 작업을 수신할 수 없으며 차단 대기열의 작업을 처리할 수 없으므로 처리 중인 작업이 중단됩니다.

    실행 중/종료 --> shutdownNow() --> 중지

  4. 정리

    Clear 상태, 모든 작업이 종료되었으며 유효 스레드 수는 0입니다. (차단 대기열의 스레드 수는 0이고 스레드 풀에서 실행 중인 스레드 수도 0입니다.)

  5. 종료됨

    종료 상태, 정리 중 --> 종료() --> 종료됨

참고: 스레드 풀 상태는 특별한 처리가 필요하지 않으며 스레드 풀 상태는 메소드에 따라 스레드 풀 내부에서 정의되고 처리됩니다.

스레드 수를 올바르게 구성하기 위한 제안

  1. CPU를 압박해야 하는 CPU 집약적인 작업의 경우 스레드 수를 ncpu+1(CPU 수 + 1)로 설정할 수 있습니다.
  2. IO 집약적인 작업의 경우 숫자를 ncpu*2(CPU 수의 2배)로 설정할 수 있습니다.

스레드 풀의 핵심 클래스인 ThreadPoolExecutor

  1. 공법

    /**
         * Creates a new {@code ThreadPoolExecutor} with the given initial
         * parameters and default thread factory and rejected execution handler.
         * It may be more convenient to use one of the {@link Executors} factory
         * methods instead of this general purpose constructor.
         *
         * @param corePoolSize the number of threads to keep in the pool, even
         *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
         * @param maximumPoolSize the maximum number of threads to allow in the
         *        pool
         * @param keepAliveTime when the number of threads is greater than
         *        the core, this is the maximum time that excess idle threads
         *        will wait for new tasks before terminating.
         * @param unit the time unit for the {@code keepAliveTime} argument
         * @param workQueue the queue to use for holding tasks before they are
         *        executed.  This queue will hold only the {@code Runnable}
         *        tasks submitted by the {@code execute} method.
         * @throws IllegalArgumentException if one of the following holds:<br>
         *         {@code corePoolSize < 0}<br>
         *         {@code keepAliveTime < 0}<br>
         *         {@code maximumPoolSize <= 0}<br>
         *         {@code maximumPoolSize < corePoolSize}
         * @throws NullPointerException if {@code workQueue} is null
         */
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
          
          
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), defaultHandler);
        }
    

    이는 매개변수가 가장 많은 생성자이며, 다른 생성자는 이 메소드로 오버로드됩니다.

    1. corePoolSize: 코어 스레드 수
    2. maximumPoolSize: 최대 스레드 수
    3. workQueue: 차단 대기열, 실행 대기 중인 작업 저장

    위 세 매개변수의 관계는 다음과 같습니다.

    • 실행 중인 스레드 수가 corePoolSize보다 적으면 스레드 풀의 다른 스레드가 유휴 상태인 경우에도 새 스레드가 생성되어 직접 실행됩니다.
    • 실행 중인 스레드 수가 corePoolSize 이상, maximumPoolSize 미만인 경우 초과된 스레드 수는 workQueue에 들어가서 대기하며, workQueue가 가득 찼을 때만 새 스레드가 생성됩니다.
    • corePoolSize가 maximumPoolSize와 같으면 스레드 풀 크기는 이때 고정됩니다. 새 작업이 제출되고 workQueue가 가득 차지 않으면 workQueue에 들어가서 유휴 스레드가 workQueue에서 제거될 때까지 기다립니다. 실행.
    • 실행 중인 스레드 수가 maximumPoolSize를 초과하고 workQueue가 가득 찬 경우 거부 정책인 RejectHandler를 통해 정책이 처리됩니다.

    위의 매개변수를 기반으로 스레드는 다음과 같이 작업을 처리합니다.

    새로운 작업이 스레드 풀에 제출되면 스레드 풀은 현재 실행 중인 스레드 수에 따라 다양한 처리 방법을 수행합니다. 주요 처리 방법에는 직접 전환, 무제한 큐 사용, 제한된 큐 사용의 세 가지가 있습니다.

    • 일반적으로 사용되는 대기열을 직접 전환하는 것은 동기식 대기열입니다.
    • 무한 대기열을 사용한다는 것은 연결된 목록을 기반으로 하는 대기열을 사용한다는 의미입니다. 예를 들어 LinkedBlockingQueue는 이 큐를 사용할 때 스레드 풀에 생성되는 최대 스레드 수는 corePoolSize이며 maximumPoolSize는 작동하지 않습니다.
    • 제한된 대기열을 사용한다는 것은 배열 기반 대기열을 사용한다는 의미입니다. 예를 들어 ArrayBlockingQueue는 이 방법을 사용하여 스레드 풀의 최대 스레드 수를 maximumPoolSize로 제한할 수 있어 리소스 소비를 줄일 수 있지만 이 방법을 사용하면 스레드 풀 수와 스레드 수가 너무 많아 스레드 예약이 더 어려워집니다. 대기열은 모두 고정되어 있습니다.

    위의 매개변수를 기반으로 리소스 소비를 줄이기 위한 몇 가지 조치를 간단하게 도출할 수 있습니다.

    • 시스템 리소스 소비, 컨텍스트 전환 오버헤드 등을 줄이려는 경우 대기열 용량을 크게 설정하고 스레드 풀 용량을 작게 설정하면 스레드 처리 작업의 처리량이 줄어듭니다.
    • 제출된 작업이 자주 차단되는 경우 최대 스레드 수를 더 큰 수로 재설정할 수 있습니다.
    1. keepAliveTime: 스레드가 작업을 실행하지 않을 때 종료를 기다리는 최대 시간

      스레드 풀의 스레드 수가 corePoolSize를 초과하는 경우 새 작업이 제출되지 않으면 코어 스레드 수를 초과하는 스레드는 즉시 소멸되지 않고 keepAliveTime을 기다린 후 소멸됩니다.

    2. 단위: keepAliveTime의 시간 단위

    3. threadFactory: 스레드를 생성하는 데 사용되는 스레드 팩토리

      기본적으로 스레드 팩토리가 기본적으로 생성되며, 기본 팩토리에서 생성된 스레드는 우선 순위가 동일하고 데몬이 아닌 스레드 이름도 설정됩니다.

    4. RejectHandler: 거부 전략

      workQueue가 가득 차고 유휴 스레드가 없으면 거부 정책이 실행됩니다.

      스레드 풀은 총 4가지 거부 전략을 제공합니다.

      • 예외를 직접 발생시키는 것이 기본 전략이기도 합니다. 구현 클래스는 AbortPolicy입니다.
      • 호출자의 스레드를 사용하여 실행하고 구현 클래스는 CallerRunsPolicy입니다.
      • 대기열의 맨 앞에 있는 작업을 버리고 현재 작업을 실행합니다. 구현 클래스는 DiscardOldestPolicy입니다.
      • 현재 작업을 직접 삭제합니다. 구현 클래스는 DiscardPolicy입니다.
  2. 시작 및 중지 방법

    1. Execute(): 실행을 위해 작업을 스레드 풀에 제출합니다.
    2. submit(): 작업을 제출하고 실행+Future와 동일한 결과를 반환할 수 있습니다.
    3. shutdown(): 스레드 풀을 닫고 스레드가 실행을 완료할 때까지 기다립니다.
    4. shutdownNow(): 스레드 실행이 완료될 때까지 기다리지 않고 즉시 스레드 풀을 닫습니다.
  3. 모니터링에 적합한 방법

    1. getTaskCount(): 실행된 스레드와 실행되지 않은 스레드의 총 개수를 가져옵니다.
    2. getCompletedTaskCount(): 실행이 완료된 스레드 수를 가져옵니다.
    3. getCorePoolSize(): 코어 스레드 수를 가져옵니다.
    4. getActiveCount(): 또는 실행 중인 스레드 수를 가져옵니다.

스레드 풀의 최상위 인터페이스 및 추상 클래스에 대한 심층 분석

인터페이스 및 추상 클래스 개요

여기에 이미지 설명을 삽입하세요.

  • 실행자 인터페이스: 반환 값 없이 작업을 제출하는 방법을 제공하는 스레드 풀의 최상위 인터페이스입니다.
  • ExecutorService: Executor에서 상속되며 스레드 풀 닫기, 작업 제출 및 결과 반환, 스레드 풀에서 작업 깨우기 등과 같은 많은 기능을 확장합니다.
  • AbstractExecutotService: ExecutorService를 상속하고 하위 클래스가 호출할 수 있는 매우 실용적인 몇 가지 메서드를 구현합니다.
  • ScheduledExecutorService: ExecutorService를 상속하고 예약된 작업과 관련된 메서드를 확장합니다.

실행자 인터페이스

public interface Executor {
    
    
    void execute(Runnable command);
}

Executor 인터페이스는 상대적으로 간단하며 제출된 작업을 실행하기 위한 실행(Runnable 명령) 메서드를 제공합니다.

ExecutorService 인터페이스

ExecutorService 인터페이스는 예약되지 않은 작업 스레드 풀의 핵심 인터페이스로, 이 인터페이스를 통해 작업이 스레드 풀에 제출되고(반환 및 비반환 방식 모두 지원) 스레드 풀을 닫고 스레드 작업을 깨우는 등의 작업을 수행합니다.

public interface ExecutorService extends Executor {
    
    
    //关闭线程池,不再接受新提交的任务,但之前提交的任务继续执行,直到完成
    void shutdown();

    //立即关闭线程池,不再接受新提交任务,会尝试停止线程池中正在执行的任务
    List<Runnable> shutdownNow();

    //判断线程池是否已经关闭
    boolean isShutdown();

    //判断线程中所有任务是否已经结束,只有调用shutdown()或shutdownNow()之后,调用此方法才返回true
    boolean isTerminated();

    //等待线程中所有任务执行结束,并设置超时时间
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
        
    //提交一个Callable类型的任务,并返回一个Future类型的结果
    <T> Future<T> submit(Callable<T> task);

    //提交一个Runnable类型任务,并设置泛型接收结果数据,返回一个Futrue类型结果
    <T> Future<T> submit(Runnable task, T result);

    //提交一个Runnable类型任务,并返回一个Future类型结果
    Future<?> submit(Runnable task);

    //批量提交Callable类型任务,并返回他们的执行结果,Task列表和Future列表一一对应
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
        
    //批量提交Callable类型任务,并获取返回结果,并限定处理所有任务的时间
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    //批量提交任务,并获得一个已经成功执行任务的结果
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    //批量提交任务,并获得一个已经完成任务的结果,并限定处理任务时间
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

AbstractExecutorEservice

​ 이 클래스는 ExecutorEservice를 상속한 추상 클래스이며, 이를 기반으로 하위 클래스가 호출할 수 있는 몇 가지 실용적인 메서드를 구현합니다.

1. 새로운 태스크포()


    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    
    
        return new FutureTask<T>(runnable, value);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    
    
        return new FutureTask<T>(callable);
    }

FutureTask는 실행 결과를 얻는 데 사용됩니다. 실제 응용 프로그램에서는 종종 FutureTask의 하위 클래스를 사용합니다.

newTaskFor 메소드의 기능은 작업을 FutureTask 객체로 캡슐화한 다음 FutureTask 객체를 스레드 풀에 제출하는 것입니다.

2. doInvokeAny()

이 메서드는 스레드 풀 작업을 일괄적으로 실행하고 최종적으로 결과 데이터를 반환하는 핵심 메서드로, 이 메서드가 스레드 중 하나의 결과를 얻는 한 스레드 풀에서 실행 중인 다른 스레드를 취소합니다.

3. 호출Any()

이 메서드 내에서 doInvokeAny() 메서드는 스레드 배치를 제출하기 위해 계속 호출되며, 그 중 하나는 완료되어 결과를 반환하고 나머지 스레드는 작업을 취소합니다.

4. 호출모두()

InvokeAll() 메서드는 시간 제한 설정이 있거나 없는 논리를 구현합니다.

시간 초과 설정이 없는 메서드 논리는 제출된 일괄 작업을 RunnableFuture 개체로 캡슐화한 다음, Execute() 메서드를 호출하여 작업을 실행하고 결과 Future를 Future 컬렉션에 추가하는 것입니다. 그런 다음 Future 컬렉션을 순회하여 여부를 결정합니다. 작업은 실행이 완료되었습니다. 완료되지 않은 경우 get 메소드를 호출하여 결과를 얻을 때까지 차단하고 이때 예외는 무시됩니다. 마지막으로 모든 작업의 ​​완료 식별은 다음과 같습니다. 판단되어 완료되지 않으면 집행이 취소됩니다.

5. 제출()

이 메소드는 상대적으로 간단하며 작업을 RunnableFuture 객체로 캡슐화합니다.execute()를 호출한 후 Future 결과가 반환됩니다.

ScheduleExecutorService

​ 이 인터페이스는 ExecutorService를 상속하며, 상위 클래스의 메소드를 상속하는 것 외에도 예약된 작업을 처리하는 기능도 제공합니다.

스레드 풀 생성 방법에 대한 소스 코드 관점 분석

​ Executors.newWorkStealingPool: 이 메서드는 Java 8에서 스레드 풀을 생성하는 새로운 메서드로 스레드에 대한 병렬 수준을 설정할 수 있으며 동시성 및 성능이 더 높습니다. 또한 스레드 풀을 생성하는 다른 메서드를 모두 생성자라고 합니다. ThreadPoolExecutor의;

ThreadPoolExecutor는 스레드 풀을 생성합니다.

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    
    
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor 클래스의 소스 코드를 살펴보면 스레드가 궁극적으로 예외 생성 메서드를 호출하여 생성된다는 것을 알 수 있습니다. 다른 초기화 매개변수는 이전에 소개되었습니다.

ForkJoinPool 클래스가 스레드 풀을 생성합니다.

public static ExecutorService newWorkStealingPool(int parallelism) {
    
    
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    public static ExecutorService newWorkStealingPool() {
    
    
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

위 소스코드에서 볼 수 있듯이 Executors.newWorkStealingPool() 메소드는 스레드 풀을 생성할 때 실제로 ForkJoinPool을 호출하여 스레드 풀을 생성한다.

/**
     * Creates a {@code ForkJoinPool} with the given parameters, without
     * any security checks or parameter validation.  Invoked directly by
     * makeCommonPool.
     */
    private ForkJoinPool(int parallelism,
                         ForkJoinWorkerThreadFactory factory,
                         UncaughtExceptionHandler handler,
                         int mode,
                         String workerNamePrefix) {
    
    
        this.workerNamePrefix = workerNamePrefix;
        this.factory = factory;
        this.ueh = handler;
        this.config = (parallelism & SMASK) | mode;
        long np = (long)(-parallelism); // offset ctl counts
        this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    }

소스 코드를 살펴보면 ForkJoinPool의 다양한 구성 방법이 궁극적으로 위에서 언급한 비공개 구성 방법을 호출한다는 것을 알게 되었습니다.

초기화 매개변수는 다음과 같습니다.

  • 병렬성: 동시성 수준
  • Factory: 스레드를 생성하는 팩토리
  • 핸들러: 스레드의 스레드가 포착되지 않은 예외를 던지면 이 UncaughtExceptionHandler를 통해 이를 처리합니다.
  • 모드: 값은 FIFO_QUEUE 또는 LIFO_QUEUE를 나타냅니다.
  • WorkerNamePrefix: 작업 실행을 위한 스레드 이름 접두사

ScheduledThreadPoolExecutor는 스레드 풀을 생성합니다.

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
    
    
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }

ScheduledThreadPoolExecutor는 ThreadPoolExecutor를 상속하는데, 소스 코드를 보면 구성 메소드의 본질은 ThreadPoolExecutor의 구성 메소드를 호출하는 것이지만 큐는 DelayedWorkQueue를 전달한다는 것을 알 수 있습니다.

소스 코드 분석: ThreadPoolExecutor를 올바르게 실행하는 방법

ThreadPoolExecutor의 중요한 속성

  • ctl 관련 속성

    AomaticInteger 유형의 상수 ctl은 스레드 풀의 전체 수명 주기 동안 사용됩니다.

    	//主要用来保存线程状态和线程数量,前3位保存线程状态,低29位报存线程数量
    	private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    	//线程池中线程数量(32-3)
        private static final int COUNT_BITS = Integer.SIZE - 3;
    	//线程池中的最大线程数量
        private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
        // runState is stored in the high-order bits
    	//线程池的运行状态
        private static final int RUNNING    = -1 << COUNT_BITS;
        private static final int SHUTDOWN   =  0 << COUNT_BITS;
        private static final int STOP       =  1 << COUNT_BITS;
        private static final int TIDYING    =  2 << COUNT_BITS;
        private static final int TERMINATED =  3 << COUNT_BITS;
    
        // Packing and unpacking ctl
    	//获取线程状态
        private static int runStateOf(int c)     {
          
           return c & ~CAPACITY; }
    	//获取线程数量
        private static int workerCountOf(int c)  {
          
           return c & CAPACITY; }
        private static int ctlOf(int rs, int wc) {
          
           return rs | wc; }
    
        /*
         * Bit field accessors that don't require unpacking ctl.
         * These depend on the bit layout and on workerCount being never negative.
         */
    
        private static boolean runStateLessThan(int c, int s) {
          
          
            return c < s;
        }
    
        private static boolean runStateAtLeast(int c, int s) {
          
          
            return c >= s;
        }
    
        private static boolean isRunning(int c) {
          
          
            return c < SHUTDOWN;
        }
    
        /**
         * Attempts to CAS-increment the workerCount field of ctl.
         */
        private boolean compareAndIncrementWorkerCount(int expect) {
          
          
            return ctl.compareAndSet(expect, expect + 1);
        }
    
        /**
         * Attempts to CAS-decrement the workerCount field of ctl.
         */
        private boolean compareAndDecrementWorkerCount(int expect) {
          
          
            return ctl.compareAndSet(expect, expect - 1);
        }
    
        /**
         * Decrements the workerCount field of ctl. This is called only on
         * abrupt termination of a thread (see processWorkerExit). Other
         * decrements are performed within getTask.
         */
        private void decrementWorkerCount() {
          
          
            do {
          
          } while (! compareAndDecrementWorkerCount(ctl.get()));
        }
    
  • 기타 중요한 속성

    	//用于存放任务的阻塞队列
    	private final BlockingQueue<Runnable> workQueue;
    
    	//可重入锁
        private final ReentrantLock mainLock = new ReentrantLock();
    
        /**
         * Set containing all worker threads in pool. Accessed only when
         * holding mainLock.
         */
    	//存放线程池中线程的集合,访问这个集合时,必须先获得mainLock锁
        private final HashSet<Worker> workers = new HashSet<Worker>();
    
        /**
         * Wait condition to support awaitTermination
         */
    	//在锁内部阻塞等待条件完成
        private final Condition termination = mainLock.newCondition();
    
    	//线程工厂,以此来创建新线程
        private volatile ThreadFactory threadFactory;
    
        /**
         * Handler called when saturated or shutdown in execute.
         */
    	//拒绝策略
        private volatile RejectedExecutionHandler handler;
    	/**
         * The default rejected execution handler
         */
    	//默认的拒绝策略
        private static final RejectedExecutionHandler defaultHandler =
            new AbortPolicy();
    

ThreadPoolExecutor의 중요한 내부 클래스

  • 노동자

    private final class Worker extends AbstractQueuedSynchronizer implements Runnable
        {
          
          
            /**
             * This class will never be serialized, but we provide a
             * serialVersionUID to suppress a javac warning.
             */
            private static final long serialVersionUID = 6138294804551838833L;
    
            /** Thread this worker is running in.  Null if factory fails. */
            final Thread thread;
            /** Initial task to run.  Possibly null. */
            Runnable firstTask;
            /** Per-thread task counter */
            volatile long completedTasks;
    
            /**
             * Creates with given first task and thread from ThreadFactory.
             * @param firstTask the first task (null if none)
             */
            Worker(Runnable firstTask) {
          
          
                setState(-1); // inhibit interrupts until runWorker
                this.firstTask = firstTask;
                this.thread = getThreadFactory().newThread(this);
            }
    
            /** Delegates main run loop to outer runWorker  */
            public void run() {
          
          
                runWorker(this);
            }
    
            // Lock methods
            //
            // The value 0 represents the unlocked state.
            // The value 1 represents the locked state.
    
            protected boolean isHeldExclusively() {
          
          
                return getState() != 0;
            }
    
            protected boolean tryAcquire(int unused) {
          
          
                if (compareAndSetState(0, 1)) {
          
          
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            protected boolean tryRelease(int unused) {
          
          
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
    
            public void lock()        {
          
           acquire(1); }
            public boolean tryLock()  {
          
           return tryAcquire(1); }
            public void unlock()      {
          
           release(1); }
            public boolean isLocked() {
          
           return isHeldExclusively(); }
    
            void interruptIfStarted() {
          
          
                Thread t;
                if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
          
          
                    try {
          
          
                        t.interrupt();
                    } catch (SecurityException ignore) {
          
          
                    }
                }
            }
        }
    

    Work 클래스는 Runnable 인터페이스를 구현하며 run() 메소드를 다시 작성해야 하는데, Worker의 run 메소드의 핵심은 ThreadPoolExecutor의 runWorker() 메소드를 호출하는 것입니다.

  • 거부 정책

    스레드 풀에서 WorkQueue 대기열이 가득 차고 유휴 스레드가 없는 경우 새 작업이 제출되면 거부 정책이 실행됩니다.

    스레드 풀은 총 4가지 거부 전략을 제공합니다.

    • 예외를 직접 던지는 것도 기본 정책이며 구현 클래스는 AbortPolicy입니다.
    • 호출자 스레드를 사용하여 작업을 수행합니다. 구현 클래스는 CallerRunsPolicy입니다.
    • 대기열의 맨 앞에 있는 작업을 버리고 현재 작업을 실행합니다. DiscardOldestPolicy
    • 현재 작업을 직접 삭제합니다. 구현 클래스는 DiscardPolicy입니다.

    ThreadPoolExecutor에서는 해당 전략을 구현하기 위해 4개의 내부 클래스가 제공됩니다.

    public static class CallerRunsPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates a {@code CallerRunsPolicy}.
             */
            public CallerRunsPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
                if (!e.isShutdown()) {
          
          
                    r.run();
                }
            }
        }
    
        /**
         * A handler for rejected tasks that throws a
         * {@code RejectedExecutionException}.
         */
        public static class AbortPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates an {@code AbortPolicy}.
             */
            public AbortPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
                throw new RejectedExecutionException("Task " + r.toString() +
                                                     " rejected from " +
                                                     e.toString());
            }
        }
    
        /**
         * A handler for rejected tasks that silently discards the
         * rejected task.
         */
        public static class DiscardPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates a {@code DiscardPolicy}.
             */
            public DiscardPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
            }
        }
    
        /**
         * A handler for rejected tasks that discards the oldest unhandled
         * request and then retries {@code execute}, unless the executor
         * is shut down, in which case the task is discarded.
         */
        public static class DiscardOldestPolicy implements RejectedExecutionHandler {
          
          
            /**
             * Creates a {@code DiscardOldestPolicy} for the given executor.
             */
            public DiscardOldestPolicy() {
          
           }
    
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          
          
                if (!e.isShutdown()) {
          
          
                    e.getQueue().poll();
                    e.execute(r);
                }
            }
        }
    

    RejectedExecutionHandler 인터페이스를 구현하고 RejectedExecution() 메서드를 재정의하여 거부 정책을 사용자 정의할 수도 있습니다.

    스레드를 생성할 때 ThreadPoolExecutor 생성자를 통해 사용자 정의 거부 정책을 전달합니다.

소스 코드 분석 ThreadPoolExecutor 핵심 프로세스

ThreadPoolExecutor에는 작업자 스레드 컬렉션이 있으며 사용자는 스레드 풀에 작업을 추가할 수 있습니다. 작업자 컬렉션의 스레드는 작업을 직접 실행하거나 작업 큐에서 작업을 가져와 실행할 수 있습니다.

ThreadPoolExecutor는 생성, 작업 실행, 종료까지 전체 스레드 풀 프로세스를 제공합니다.

ThreadPoolExecutor에서는 스레드 풀의 로직이 주로 Execute(Runnable command), addWorker(Runnable firstTask, boolean core), addWorkerFailed(Worker w) 및 기타 메서드와 거부 전략에 반영되며, 다음으로 이러한 핵심 메서드를 심층적으로 분석하겠습니다. .

실행(실행 가능한 명령)

이 메소드의 기능은 Runnable 유형 작업을 스레드 풀에 제출하는 것입니다.

public void execute(Runnable command) {
    
    
        if (command == null)
            //若提交的任务为空,则提交空指针异常
            throw new NullPointerException();
       //获取线程池的状态,和线程池中线程数量
        int c = ctl.get();
    	//若线程池中线程数小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
    
    
            //重新开启线程执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
    	//若线程池处于RUNNING状态,则将任务添加到阻塞队列中
    	//只有线程池处于RUNNING状态时,才能添加到队列
        if (isRunning(c) && workQueue.offer(command)) {
    
    
            //再次获取线程次状态和线程池数量,用于二次检查
            //向队列中添加线程成功,但由于其他线程可能会修改线程池状态,所以这里需要进行二次检查
            int recheck = ctl.get();
            //如果线程池没有再处于RUNNING状态,则从队列中删除任务
            if (! isRunning(recheck) && remove(command))
                //执行拒绝策略
                reject(command);
            else if (workerCountOf(recheck) == 0)
                //若线程池为空,则新建一个线程加入
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            //任务队列已满,则新建一个Worker线程,若新增失败,则执行拒绝策略
            reject(command);
    }

addWorker(실행 가능한 firstTask, 부울 코어)

이 방법은 크게 세 부분으로 나눌 수 있는데, 첫 번째 부분은 주로 CAS가 작업자 스레드를 스레드 풀에 안전하게 추가하는 부분, 두 번째 부분은 새로운 작업자 스레드를 추가하는 부분, 세 번째 부분은 안전한 동시성을 통해 워커에 스레드를 추가하는 부분, 작업을 수행하기 위해 스레드를 시작합니다.

private boolean addWorker(Runnable firstTask, boolean core) {
    
    
    //循环标签,重试的标识
    retry:
    for (;;) {
    
    
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //检查队列是否在某些特定条件下为空
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
		//此循环中主要是通过CAS的方式增加线程个数
        for (;;) {
    
    
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //CAS的方式增加线程数量
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    //跳出最外层循环,说明已通过CAS增加线程成功
    //此时创建新的线程
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
    
    
        //将新建线程封装成Woker
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
    
    
            //独占锁,保证操作workers的同步
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
    
    
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
    
    
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //将Worker加入队列
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
    
    
                //释放独占锁
                mainLock.unlock();
            }
            if (workerAdded) {
    
    
                t.start();
                workerStarted = true;
            }
        }
    } finally {
    
    
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

addWorkerFailed(작업자 w)

addWorker(Runnable firstTask, boolean core) 메서드에서는 작업자 스레드 추가에 실패하거나 작업자 스레드 시작에 실패하는 경우 addWorkerFailed(Worker w) 메서드가 호출되는데 이 메서드가 더 간단하며 단독 잠금을 획득하고 제거합니다. 작업자로부터 작업을 수행하고 CAS를 통해 작업 수를 1개 줄이고 최종적으로 잠금을 해제합니다.

private void addWorkerFailed(Worker w) {
    
    
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        if (w != null)
            workers.remove(w);
        decrementWorkerCount();
        tryTerminate();
    } finally {
    
    
        mainLock.unlock();
    }
}
  • 거부 정책

    /**
     * Invokes the rejected execution handler for the given command.
     * Package-protected for use by ScheduledThreadPoolExecutor.
     */
    final void reject(Runnable command) {
          
          
        handler.rejectedExecution(command, this);
    }
    

    RejectExecutionHandler의 네 가지 구현 클래스는 정확히 스레드에서 제공하는 네 가지 거부 전략의 구현 클래스입니다.

    이 메서드의 구체적인 전략은 스레드 풀을 생성할 때 전달된 매개변수에 따라 결정되며 기본적으로 기본 거부 전략이 사용됩니다.

스레드 풀에서 Woker 실행 프로세스의 소스 코드 분석

워커 클래스 분석

클래스 구조 관점에서 Woker는 AQS(AbstractQueueSynchronizer) 클래스를 상속하고 Runnable 인터페이스를 구현하며, 기본적으로 Woker 클래스는 동기화 구성 요소이자 작업을 수행하는 스레드입니다.

private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
    
    
    /**
     * This class will never be serialized, but we provide a
     * serialVersionUID to suppress a javac warning.
     */
    private static final long serialVersionUID = 6138294804551838833L;

    /** Thread this worker is running in.  Null if factory fails. */
    final Thread thread;
    /** Initial task to run.  Possibly null. */
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;

    /**
     * Creates with given first task and thread from ThreadFactory.
     * @param firstTask the first task (null if none)
     */
    Worker(Runnable firstTask) {
    
    
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    /** Delegates main run loop to outer runWorker  */
    public void run() {
    
    
        runWorker(this);
    }

    // Lock methods
    //
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.

    protected boolean isHeldExclusively() {
    
    
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
    
    
        if (compareAndSetState(0, 1)) {
    
    
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
    
    
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        {
    
     acquire(1); }
    public boolean tryLock()  {
    
     return tryAcquire(1); }
    public void unlock()      {
    
     release(1); }
    public boolean isLocked() {
    
     return isHeldExclusively(); }

    void interruptIfStarted() {
    
    
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
    
    
            try {
    
    
                t.interrupt();
            } catch (SecurityException ignore) {
    
    
            }
        }
    }
}

Worker 클래스의 생성자에서 볼 수 있듯이 동기화 상태는 먼저 -1로 설정되는데 이는 runWorker 메서드가 실행되기 전에 중단되는 것을 방지하기 위한 것입니다.

이는 다른 스레드가 스레드 풀에서 shutdownNow() 메서드를 호출하는 경우 Worker 클래스의 상태가 > 0이면 스레드가 중단되고, 상태가 -1이면 스레드가 중단되지 않기 때문입니다.

Worker 클래스는 Runnable 인터페이스를 구현하고 실제로 ThreadPoolExecutor의 runWorker() 메서드를 호출하는 run 메서드를 재정의해야 합니다.

runWorker(작업자 w)

final void runWorker(Worker w) {
    
    
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    //释放锁,将state设置为0,允许中断
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
    
    
       //若任务不为空,或队列中获取的任务不为空,则进入循环
        while (task != null || (task = getTask()) != null) {
    
    
            //任务不为空,则先获取woker线程的独占锁
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            //若线程次已经停止,线程中断时未中断成功
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                //执行中断操作
                wt.interrupt();
            try {
    
    
                //任务执行前置逻辑
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
    
    
                    //任务执行
                    task.run();
                } catch (RuntimeException x) {
    
    
                    thrown = x; throw x;
                } catch (Error x) {
    
    
                    thrown = x; throw x;
                } catch (Throwable x) {
    
    
                    thrown = x; throw new Error(x);
                } finally {
    
    
                    //任务执行后置逻辑
                    afterExecute(task, thrown);
                }
            } finally {
    
    
                //任务执行完成后,将其置空
                task = null;
                //已完成任务数加一
                w.completedTasks++;
                //释放锁
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
    
    
        //执行Worker完成退出逻辑
        processWorkerExit(w, completedAbruptly);
    }
}

위의 소스 코드 분석을 통해 Woker가 스레드에서 얻은 작업이 비어 있으면 getTask() 메서드가 호출되어 대기열에서 작업을 가져오는 것을 알 수 있습니다.

getTask()

private Runnable getTask() {
    
    
    boolean timedOut = false; // Did the last poll() time out?
    //自旋
    for (;;) {
    
    
        int c = ctl.get();
        //获取线程池状态
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //检测队列在线程池关闭或停止时,是否为空
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    
    
            //减少Worker线程数量
            decrementWorkerCount();
            return null;
        }
        //获取线程池中线程数量
        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
    
    
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
    
    
            //从任务队列中获取任务
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                //任务不为空,直接返回
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
    
    
            timedOut = false;
        }
    }
}
  • beforeExecute(스레드 t, 실행 가능 r)

    protected void beforeExecute(Thread t, Runnable r) {
          
           }
    

    이 메서드의 본문은 비어 있습니다. 즉, ThreadPoolExecutor의 하위 클래스를 생성하여 이 메서드를 재정의할 수 있으므로 스레드 풀이 실제로 작업을 실행하기 전에 사용자 정의된 사전 논리가 실행될 수 있습니다.

  • afterExecute(실행 가능 r, 던질 수 있는 t)

    protected void afterExecute(Runnable r, Throwable t) {
          
           }
    

    위와 마찬가지로 서브클래스에서 이 메서드를 재정의할 수 있으므로 스레드 풀이 작업을 실행한 후에 사용자 정의 사후 처리 논리가 실행될 수 있습니다.

  • processWorkerExit(작업자 w, 부울이 갑자기 완료됨)

    이 메서드의 주요 논리는 Worker 스레드를 종료하는 논리를 실행하고 일부 정리 작업을 수행하는 것입니다.

  • 시도종료()

    final void tryTerminate() {
          
          
        //自旋
        for (;;) {
          
          
            //获取ctl
            int c = ctl.get();
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            if (workerCountOf(c) != 0) {
          
           // Eligible to terminate
                //若当前线程池中线程数量不为0,则中断线程
                interruptIdleWorkers(ONLY_ONE);
                return;
            }
            //获取线程池的全局锁
            final ReentrantLock mainLock = this.mainLock;
            //加锁
            mainLock.lock();
            try {
          
          
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
          
          
                    try {
          
          
                        terminated();
                    } finally {
          
          
                        //将线程状态设置为TERMINATED
                        ctl.set(ctlOf(TERMINATED, 0));
                        //唤醒所有因调用awaitTermination()而阻塞的线程
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
          
          
                //释放锁
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }
    
  • 끝내다()

    protected void terminated() {
          
           }
    

    이 메서드의 본문은 비어 있습니다. 하위 클래스에서 이 메서드를 재정의하여 tryTerminating() 메서드에서 사용자 정의된 메서드를 실행할 수 있습니다.

소스 코드 구문 분석 스레드 풀은 어떻게 정상적으로 종료됩니까?

일시 휴업()

스레드 풀을 사용할 때 shutdown() 메서드가 호출되면 스레드 풀은 더 이상 새로 제출된 작업을 허용하지 않으며 이미 실행 중인 스레드는 계속 실행됩니다.

이 메서드는 비차단 메서드로, 호출된 후 즉시 반환되며 모든 스레드 작업이 완료될 때까지 기다리지 않고 반환됩니다.

public void shutdown() {
    
    
    //获取线程池的全局锁
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        //检查是否有关闭线程池的权限
        checkShutdownAccess();
        //将当前线程池的状态设置为SHUTDOWN
        advanceRunState(SHUTDOWN);
        //中断woker线程
        interruptIdleWorkers();
        //调用ScheduledThreadPoolExecutor的钩子函数
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
    
    
        //释放锁
        mainLock.unlock();
    }
    tryTerminate();
}

지금 종료()

스레드 풀의 shutdownNow() 메서드가 호출되면 스레드는 더 이상 새로 제출된 작업을 수신하지 않고, workQueue 대기열의 스레드도 삭제되며, 실행 중인 스레드가 중단됩니다. 반환 결과는 대기열 workQueue에 있는 폐기된 작업 목록입니다.

public List<Runnable> shutdownNow() {
    
    
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        //清空、丢弃队列中任务
        tasks = drainQueue();
    } finally {
    
    
        mainLock.unlock();
    }
    tryTerminate();
    //返回任务列表
    return tasks;
}

waitTermination(긴 시간 초과, TimeUnit 단위)

스레드 풀이 waitTermination을 호출하면 호출자의 스레드를 차단하고 스레드 풀 상태가 TERNINATED로 변경되거나 시간 초과 기간에 도달할 때까지 반환되지 않습니다.

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    
    
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        for (;;) {
    
    
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
            if (nanos <= 0)
                return false;
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
    
    
        //释放锁
        mainLock.unlock();
    }
}

이 메서드의 전반적인 논리는 다음과 같습니다. 먼저 작업자 스레드의 배타적 잠금을 얻은 다음 회전하고 스레드 풀 상태가 TERMINATED로 변경되는지 확인합니다. 그렇다면 true를 반환하고, 그렇지 않으면 시간 초과되었는지 확인하고, 시간 초과되면 확인합니다. false를 반환하고 시간 초과되지 않으면 다시 시작합니다. 남은 시간 초과 기간을 설정합니다.

AQS의 주요 클래스

카운트다운래치

  • 개요

현재 스레드의 실행을 차단할 수 있는 동기화 도우미 클래스입니다. 즉, 하나 이상의 스레드는 다른 스레드의 실행이 완료될 때까지 기다릴 수 있습니다. 초기화를 위해 주어진 카운터를 사용하십시오. 카운터의 작동은 원자적 작동입니다. 즉, 하나의 스레드만이 동시에 카운터를 작동할 수 있습니다.

수정된 클래스의 wait() 메서드를 호출하는 스레드는 다른 스레드가 클래스의 countDown() 메서드를 호출하고 현재 카운터 값을 0으로 만들 때까지 기다립니다.

이 클래스의 countDown() 메서드가 호출될 때마다 카운터 값이 1씩 감소합니다.

카운터 값이 0으로 감소하면 차단되고 wait() 메서드를 호출하여 대기 중인 모든 스레드가 계속 실행됩니다. 카운터 값을 재설정할 수 없기 때문에 이 작업은 한 번만 발생할 수 있습니다.

카운트를 재설정하는 버전이 필요한 경우 CyclicBarrier 사용을 고려해보세요.

CountDownLatch는 지정된 시간 초과 기간 동안 대기하는 기능을 지원하며 지정된 시간 이상 기다리지 않습니다. 이를 사용할 때 wait() 메서드에서 지정된 시간만 전달하면 됩니다.

public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
           BrokenBarrierException,
           TimeoutException {
    
    
    return dowait(true, unit.toNanos(timeout));
}
  • 사용되는 장면

    일부 시나리오에서는 프로그램이 후속 작업을 계속 수행하기 전에 하나 이상의 조건이 완료될 때까지 기다려야 합니다. 일반적인 응용 프로그램은 병렬 컴퓨팅입니다. 계산량이 많은 작업을 처리할 때 여러 개의 작은 작업으로 분할할 수 있습니다. 모든 하위 작업이 완료될 때까지 기다린 후 상위 작업은 모든 하위 작업의 결과를 얻습니다. 그리고 그것들을 요약합니다.

  • 코드 예

    ExecutorService의 shutdown() 메서드를 호출하면 모든 스레드가 즉시 삭제되지 않지만 기존 스레드가 모두 실행될 수 있습니다.

    그런 다음 스레드 풀을 파괴합니다.

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Slf4j
    public class CountDownLatchExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        test(threadNum);
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    } finally {
          
          
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await();
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            Thread.sleep(100);
            log.info("{}", threadNum);
        }
    }
    

    특정 시간 동안 대기하는 것을 지원하는 코드는 다음과 같습니다.

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    @Slf4j
    public class CountDownLatchExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        test(threadNum);
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    } finally {
          
          
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.MICROSECONDS);
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            Thread.sleep(100);
            log.info("{}", threadNum);
        }
    }
    

신호기

  • 개요

    동시에 동시 스레드 수를 제어합니다. 세마포어를 제어하고 동시에 특정 리소스에 액세스하는 스레드 수를 제어할 수 있습니다.

    두 가지 핵심 메소드 제공: acquire() 및 release()

    acquire() 는 접근 권한을 얻는다는 뜻으로, 얻지 못하면 차단하고 대기하며, release() 는 완료 후 권한을 해제합니다.

    세마포어는 현재 접근 가능한 번호를 유지하며, 동시에 접근 가능한 번호를 제어하기 위해 동기화 메커니즘을 사용합니다.

    세마포어는 제한된 크기의 연결 목록을 구현할 수 있습니다.

  • 사용되는 장면

    세마포어는 접근이 제한된 리소스에 자주 사용됩니다.

  • 코드 예

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class SemaphoreExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire(); //获取一个许可
                        test(threadNum);
                        semaphore.release(); //释放一个许可
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            log.info("{}", threadNum);
            Thread.sleep(1000);
        }
    }
    

    한 번에 여러 라이센스 취득 및 해제

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class SemaphoreExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire(3); //获取多个许可
                        test(threadNum);
                        semaphore.release(3); //释放多个许可
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            log.info("{}", threadNum);
            Thread.sleep(1000);
        }
    }
    

    이러한 시나리오가 있다고 가정합니다. 현재 시스템에서 허용하는 최대 동시성 수가 3이라고 가정하고, 3을 초과하면 폐기해야 합니다. 이러한 시나리오는 Semaphore를 통해 실현될 수도 있습니다.

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class SemaphoreExample {
          
          
        private static final int threadCount = 200;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i < threadCount; i++) {
          
          
                final int threadNum = i;
                exec.execute(() -> {
          
          
                    try {
          
          
                        //尝试获取一个许可,也可以尝试获取多个许可,
                        //支持尝试获取许可超时设置,超时后不再等待后续线程的执行
                        //具体可以参见Semaphore的源码
                        if (semaphore.tryAcquire()) {
          
          
                            test(threadNum);
                            semaphore.release(); //释放一个许可
                        }
                    } catch (InterruptedException e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            log.info("finish");
            exec.shutdown();
        }
    
        private static void test(int threadNum) throws InterruptedException {
          
          
            log.info("{}", threadNum);
            Thread.sleep(1000);
        }
    }
    

순환 장벽

  • 개요

    스레드 그룹이 공통 장벽 지점에 도달할 때까지 서로를 기다릴 수 있도록 하는 동기화 보조 클래스로, 이를 통해 여러 스레드가 서로를 기다릴 수 있으며, 각 스레드가 준비된 경우에만 각 스레드가 계속 실행될 수 있습니다. .

    countDownLatch와 유사하게 모두 카운터를 사용하여 구현되는데, 스레드가 CyclicBarrier의 wait() 메소드를 호출하면 대기 상태로 진입하고 카운터는 증가 연산을 수행하며, 카운터 값이 설정된 초기값까지 증가하면 모든 스레드는 wait() 메서드로 인해 대기 상태에 들어간 개체는 깨어나 후속 작업을 계속 수행합니다. CyclicBarrier는 대기 스레드를 해제한 후 재사용할 수 있으므로 CyclicBarrier는 순환 장벽이라고도 합니다.

  • 사용되는 장면

    멀티 스레드가 데이터를 계산하고 최종적으로 계산 결과를 병합하는 시나리오에서 사용할 수 있습니다.

  • countDownLatch와 countDownLatch의 차이점

    1. countDownLatch의 카운터는 한 번만 사용할 수 있지만 CyclicBarrier의 카운터는 reSet()을 사용하여 재설정하고 주기적으로 사용할 수 있습니다.
    2. countDownLatch가 구현하는 것은 1개 또는 n개의 스레드가 실행을 계속하기 전에 다른 스레드가 완료될 때까지 기다리는 것입니다. 이는 다른 스레드를 기다리는 1개 이상의 스레드 간의 관계를 설명하며 CyclicBarrier는 주로 스레드 그룹의 스레드가 서로를 기다리는 것을 의미합니다. 각 스레드는 실행을 계속하기 전에 공통 조건을 충족하며 여러 스레드의 내부 대기 관계를 설명합니다.
    3. CyclicBarrier는 보다 복잡한 시나리오를 처리할 수 있으며, 계산 오류가 발생하면 카운터를 재설정하고 스레드를 다시 실행할 수 있습니다.
    4. CyclicBarrier는 대기 중인 스레드 수를 가져오는 getNumberByWaiting() 및 스레드가 중단되었는지 확인하는 isBroken() 메서드와 같은 보다 유용한 메서드를 제공합니다.
  • 코드 예

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Slf4j
    public class CyclicBarrierExample {
          
          
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
    
        public static void main(String[] args) throws Exception {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
          
          
                final int threadNum = i;
                Thread.sleep(1000);
                executorService.execute(() -> {
          
          
                    try {
          
          
                        race(threadNum);
                    } catch (Exception e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            executorService.shutdown();
        }
    
        private static void race(int threadNum) throws Exception {
          
          
            Thread.sleep(1000);
            log.info("{} is ready", threadNum);
            cyclicBarrier.await();
            log.info("{} continue", threadNum);
        }
    }
    

    대기 시간 제한을 설정하는 샘플 코드는 다음과 같습니다.

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.*;
    
    @Slf4j
    public class CyclicBarrierExample {
          
          
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
    
        public static void main(String[] args) throws Exception {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
          
          
                final int threadNum = i;
                Thread.sleep(1000);
                executorService.execute(() -> {
          
          
                    try {
          
          
                        race(threadNum);
                    } catch (Exception e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            executorService.shutdown();
        }
    
        private static void race(int threadNum) throws Exception {
          
          
            Thread.sleep(1000);
            log.info("{} is ready", threadNum);
            try {
          
          
                cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
            } catch (BrokenBarrierException | TimeoutException e) {
          
          
                log.warn("BarrierException", e);
            }
            log.info("{} continue", threadNum);
        }
    }
    

    CyclicBarrier를 선언할 때 Runnable도 지정할 수 있으며, 스레드가 Barrier에 도달하면 Runnable 메소드가 먼저 실행될 수 있습니다. 샘플 코드는 다음과 같습니다.

    package io.binghe.concurrency.example.aqs;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Slf4j
    public class CyclicBarrierExample {
          
          
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
          
          
            log.info("callback is running");
        });
    
        public static void main(String[] args) throws Exception {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
          
          
                final int threadNum = i;
                Thread.sleep(1000);
                executorService.execute(() -> {
          
          
                    try {
          
          
                        race(threadNum);
                    } catch (Exception e) {
          
          
                        e.printStackTrace();
                    }
                });
            }
            executorService.shutdown();
        }
    
        private static void race(int threadNum) throws Exception {
          
          
            Thread.sleep(1000);
            log.info("{} is ready", threadNum);
            cyclicBarrier.await();
            log.info("{} continue", threadNum);
        }
    }
    

AQS의 키 잠금

재진입 잠금

  • 개요

    Java에서 제공되는 잠금은 크게 두 가지 범주로 나누어집니다. 하나는 동기화된 수정 잠금이고, 다른 하나는 JUC에서 제공하는 잠금이며, JUC의 핵심 잠금은 ReentrantLock입니다.

    ReentrantLock과 동기화의 차이점:

    1. 재진입

      동일한 스레드가 두 스레드 모두에 한 번 진입하면 잠금 카운터가 1만큼 증가하고 잠금 카운터가 0으로 떨어지면 잠금이 해제됩니다.

    2. 잠금 구현

      동기화는 JVM 기반으로 구현되고, ReentrantLock은 JDK 기반으로 구현됩니다.

    3. 성능 차이

      동기화 최적화 이전에는 ReentrantLock보다 성능이 훨씬 나빴지만, JDK6 이후에는 동기화가 편향된 잠금과 경량 잠금(예: 스핀 잠금)을 도입한 후에는 성능이 거의 동일했습니다.

    4. 기능적 차이

      편의:

      동기화는 사용하기 더 편리하며 컴파일러는 잠금을 잠그고 해제합니다. ReentrantLock은 수동으로 잠그고 해제해야 하며 최종적으로 잠금을 해제하는 것이 가장 좋습니다.

      유연성과 세분성:

      여기서는 ReentrantLock이 동기화보다 낫습니다.

    ReentrantLock의 고유한 기능:

    1. ReentrantLock은 공정한 잠금 또는 불공정한 잠금을 지정할 수 있습니다. 동기화는 불공정 잠금만 사용할 수 있습니다. 공정한 잠금은 먼저 대기 중인 스레드가 먼저 잠금을 얻는다는 의미입니다.
    2. 그룹으로 깨워야 하는 스레드를 깨울 수 있는 Condition 클래스를 제공합니다. 동기화는 무작위로 하나의 스레드만 깨우거나 모든 스레드를 깨울 수 있습니다.
    3. 잠금을 기다리는 스레드를 중단하는 메커니즘인 lock.lockInterruptily()를 제공합니다. ReentrantLock 구현은 CAS 연산을 호출하여 잠금을 구현하는 스핀 잠금으로 커널 상태에 진입하는 스레드의 차단 상태를 방지하므로 성능이 더 좋습니다.
    4. 일반적으로 ReentrantLock은 동기화가 수행할 수 있는 모든 작업을 수행할 수 있습니다. 성능면에서는 ReentrantLock이 동기화된 것보다 낫습니다.

    동기화된 이점:

    1. 수동으로 잠금을 해제할 필요 없이 JVM이 자동으로 처리하며, 예외가 발생하면 JVM이 자동으로 잠금을 해제한다.
    2. JVM이 잠금 관리 요청 및 해제를 수행할 때 JVM은 스레드 덤프 생성 시 잠금 정보를 생성할 수 있으며, 이 정보는 교착 상태 및 기타 비정상적인 동작의 소스를 식별할 수 있으므로 디버깅에 매우 유용합니다. ReentrantLock은 일반적인 클래스이므로 JVM은 어떤 스레드가 잠금을 소유하고 있는지 알 수 없습니다.
    3. 동기화는 모든 버전의 JVM에서 사용할 수 있지만 버전 1.5 이전의 일부 JVM에서는 ReentrantLock이 지원되지 않을 수 있습니다.

    ReentrantLock의 일부 메소드에 대한 설명:

    1. boolean tryLock(): 호출 시 다른 스레드가 잠금을 보유하지 않은 경우에만 잠금을 획득합니다.
    2. boolean tryLock(long timeout, TimeUnit 단위): 지정된 시간에 다른 스레드가 잠금을 보유하지 않고 현재 스레드가 중단되지 않으면 이 잠금을 획득합니다.
    3. void lockInterruptible(): 현재 스레드가 중단되지 않으면 잠금을 획득하고, 중단되면 예외를 발생시킵니다.
    4. boolean isLocked(): 이 잠금이 스레드에 의해 유지되는지 쿼리합니다.
    5. boolean isHeldByCurrentThread(): 현재 스레드가 잠겨 있는지 여부를 쿼리합니다.
    6. boolean isFair(): 공정한 잠금인지 확인합니다.
    7. boolean hasQueuedThread(Thread thread): 지정된 스레드가 이 잠금을 획득하기 위해 대기하고 있는지 쿼리합니다.
    8. boolean hasQueuedThreads(): 이 잠금을 획득하기 위해 대기 중인 스레드가 있습니까?
    9. int getHoldCount(): 현재 스레드가 보유한 잠금 수를 쿼리합니다.
  • 코드 예

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    @Slf4j
    public class LockExample {
          
          
        //请求总数
        public static int clientTotal = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
        public static int count = 0;
        private static final Lock lock = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal; i++) {
          
          
                executorService.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
          
          
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count);
        }
    
        private static void add() {
          
          
            lock.lock();
            try {
          
          
                count++;
            } finally {
          
          
                lock.unlock();
            }
        }
    }
    

재진입 읽기쓰기 잠금

  • 개요

    읽기-쓰기 잠금은 읽기 잠금이 없을 때만 쓰기 잠금을 획득할 수 있으며, 쓰기 잠금을 획득할 수 없으면 쓰기 잠금 부족 상태가 됩니다.

    읽기가 많고 쓰기가 적은 시나리오에서는 ReentrantReadWriteLock의 성능이 ReentrantLock보다 훨씬 높으며 다중 스레드 읽기 중에 서로 영향을 미치지 않습니다.ReentrantLock과 달리 다중 스레드 읽기에서도 각 스레드가 읽기를 획득해야 합니다. 그러나 ReentrantLock과 마찬가지로 어떤 스레드라도 쓰기 작업을 수행할 때는 다른 스레드가 읽거나 쓰는지 여부에 관계없이 쓰기 잠금을 획득해야 합니다. 동일한 스레드가 읽기 잠금과 쓰기 잠금을 동시에 보유할 수 있다는 점에 유의해야 합니다.

  • 코드 예

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Map;
    import java.util.Set;
    import java.util.TreeMap;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    @Slf4j
    public class LockExample {
          
          
        private final Map<String, Data> map = new TreeMap<>();
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        private final Lock readLock = lock.readLock();
        private final Lock writeLock = lock.writeLock();
    
        public Data get(String key) {
          
          
            readLock.lock();
            try {
          
          
                return map.get(key);
            } finally {
          
          
                readLock.unlock();
            }
        }
    
        public Set<String> getAllKeys() {
          
          
            readLock.lock();
            try {
          
          
                return map.keySet();
            } finally {
          
          
                readLock.unlock();
            }
        }
    
        public Data put(String key, Data value) {
          
          
            writeLock.lock();
            try {
          
          
                return map.put(key, value);
            } finally {
          
          
                writeLock.unlock();
            }
        }
    
        class Data {
          
          
    
        }
    }
    

스탬프 잠금

  • 개요

StampedLock은 ReentrantReadWriteLock을 구현한 것입니다. 주요 차이점은 StampedLock이 재진입을 허용하지 않는다는 것입니다. 낙관적 읽기 기능을 추가하면 사용이 더 복잡해지지만 성능은 더 좋아집니다.

StampedLock은 읽기, 쓰기, 낙관적 읽기의 세 가지 잠금 모드를 제어합니다.

StampedLock의 상태는 버전과 모드의 두 부분으로 구성됩니다. 잠금 획득 방법은 티켓이라는 숫자를 반환하며 해당 잠금 상태를 사용하여 관련 액세스를 표시하고 제어합니다. 숫자 0은 쓰기 잠금에 액세스 권한이 없음을 나타냅니다. .

읽기 잠금은 비관적 잠금과 낙관적 잠금으로 나뉜다. 읽기 잠금이 있습니다. 프로그램은 읽은 후 쓰기로 변경되었는지 확인하고 후속조치를 취할 수 있어 프로그램의 처리량을 크게 향상시킬 수 있습니다.

간단히 말해서, 읽기 스레드가 점점 더 많아지는 시나리오에서 StampedLock은 프로그램 처리량을 크게 향상시킵니다.

import java.util.concurrent.locks.StampedLock;

class Point {
    
    
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) {
    
     // an exclusively locked method
        long stamp = sl.writeLock();
        try {
    
    
            x += deltaX;
            y += deltaY;
        } finally {
    
    
            sl.unlockWrite(stamp);
        }
    }

    //下面看看乐观读锁案例
    double distanceFromOrigin() {
    
     // A read-only method
        long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
        double currentX = x, currentY = y; //将两个字段读入本地局部变量
        if (!sl.validate(stamp)) {
    
     //检查发出乐观读锁后同时是否有其他写锁发生?
            stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
            try {
    
    
                currentX = x; // 将两个字段读入本地局部变量
                currentY = y; // 将两个字段读入本地局部变量
            } finally {
    
    
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    //下面是悲观读锁案例
    void moveIfAtOrigin(double newX, double newY) {
    
     // upgrade
	// Could instead start with optimistic, not read mode
        long stamp = sl.readLock();
        try {
    
    
            while (x == 0.0 && y == 0.0) {
    
     //循环,检查当前状态是否符合
                long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                if (ws != 0L) {
    
     //这是确认转为写锁是否成功
                    stamp = ws; //如果成功 替换票据
                    x = newX; //进行状态改变
                    y = newY; //进行状态改变
                    break;
                } else {
    
     //如果不能成功转换为写锁
                    sl.unlockRead(stamp); //我们显式释放读锁
                    stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
                }
            }
        } finally {
    
    
            sl.unlock(stamp); //释放读锁或写锁
        }
    }
}
  • 코드 예

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.StampedLock;
    
    @Slf4j
    public class LockExample {
          
          
        //请求总数
        public static int clientTotal = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
        public static int count = 0;
        private static final StampedLock lock = new StampedLock();
    
        public static void main(String[] args) throws InterruptedException {
          
          
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal; i++) {
          
          
                executorService.execute(() -> {
          
          
                    try {
          
          
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
          
          
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count);
        }
    
        private static void add() {
          
          
    //加锁时返回一个long类型的票据
            long stamp = lock.writeLock();
            try {
          
          
                count++;
            } finally {
          
          
    //释放锁的时候带上加锁时返回的票据
                lock.unlock(stamp);
            }
        }
    }
    

    다음과 같이 동기화 또는 ReentrantLock을 선택할지에 대한 예비 판단을 내릴 수 있습니다.

    1. 동기화는 경쟁자가 적을 때 좋은 범용 잠금 구현입니다.
    2. 경쟁사가 많지만 스레드의 성장 추세는 예측 가능하므로 이때 ReentrantLock을 사용하는 것이 좋은 일반 잠금 구현입니다.
    3. 동기화는 교착 상태를 일으키지 않습니다. 다른 잠금을 부적절하게 사용하면 교착 상태가 발생할 수 있습니다.

상태

  • 개요

    Condition은 여러 스레드 간의 통신을 조정하기 위한 도구 클래스로 이를 사용하면 더 나은 유연성을 제공할 수 있습니다. 예를 들어 다중 채널 알림 기능을 구현할 수 있습니다. 즉, Lock 객체에서 여러 Condition 인스턴스를 생성할 수 있으며 스레드는 스레드 알림을 선택적으로 수행하고 스레드 예약을 보다 유연하게 수행할 수 있도록 개체를 지정된 조건에 등록할 수 있습니다.

  • 특징

    1. Condition의 전제는 Lock이며 Condition 객체는 AQS의 newCondition() 메서드에 의해 생성됩니다.
    2. Condition의 wait() 메소드는 스레드가 AQS에서 삭제되고 스레드가 획득한 잠금을 해제하고 Condition 대기 큐에 들어가 알림을 기다리는 것을 나타냅니다.
    3. Condition의 signal() 메소드는 Condition 대기 큐에 있는 노드를 깨우고 잠금 획득을 준비하는 것을 의미합니다.
  • 코드 예

    package io.binghe.concurrency.example.lock;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    @Slf4j
    public class LockExample {
          
          
        public static void main(String[] args) {
          
          
            ReentrantLock reentrantLock = new ReentrantLock();
            Condition condition = reentrantLock.newCondition();
            new Thread(() -> {
          
          
                try {
          
          
                    reentrantLock.lock();
                    log.info("wait signal"); // 1
                    condition.await();
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
                log.info("get signal"); // 4
                reentrantLock.unlock();
            }).start();
            new Thread(() -> {
          
          
                reentrantLock.lock();
                log.info("get lock"); // 2
                try {
          
          
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
                condition.signalAll();
                log.info("send signal ~ "); // 3
                reentrantLock.unlock();
            }).start();
        }
    }
    

스레드로컬

  • 개요

ThreadLocal은 JDK에서 제공되며 스레드 로컬 변수를 지원합니다. 이는 ThreadLocal에 저장된 변수가 현재 스레드에 속하고 변수가 다른 스레드와 격리되어 변수가 현재 스레드에 고유하다는 것을 의미합니다.

ThreadLocal 변수를 생성하면 이 변수에 액세스하는 각 스레드는 이 변수의 로컬 복사본을 갖게 됩니다. 여러 스레드가 이 변수에 대해 작동할 때 실제로 변수의 로컬 복사본을 작동하므로 보안 질문에 대한 스레드가 필요하지 않습니다.

  • 사용예

    ThreadLocal을 사용하여 관련 변수 정보 저장 및 인쇄

    public class ThreadLocalTest {
          
          
        private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
        public static void main(String[] args) {
          
          
    		//创建第一个线程
            Thread threadA = new Thread(() -> {
          
          
                threadLocal.set("ThreadA:" + Thread.currentThread().getName());
                System.out.println("线程A本地变量中的值为:" + threadLocal.get());
            });
    		//创建第二个线程
            Thread threadB = new Thread(() -> {
          
          
                threadLocal.set("ThreadB:" + Thread.currentThread().getName());
                System.out.println("线程B本地变量中的值为:" + threadLocal.get());
            });
    		//启动线程A和线程B
            threadA.start();
            threadB.start();
        }
    }
    

    프로그램을 실행하면 인쇄된 정보는 다음과 같습니다.

    线程A本地变量中的值为:ThreadAThread-0
    线程B本地变量中的值为:ThreadBThread-1
    

    이때 스레드 A에 대한 삭제 변수 작업을 추가합니다.

    public class ThreadLocalTest {
          
          
        private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
        public static void main(String[] args) {
          
          
    		//创建第一个线程
            Thread threadA = new Thread(() -> {
          
          
                threadLocal.set("ThreadA:" + Thread.currentThread().getName());
                System.out.println("线程A本地变量中的值为:" + threadLocal.get());
                threadLocal.remove();
                System.out.println("线程A删除本地变量后ThreadLocal中的值为:" + threadLocal.get());
            });
    		//创建第二个线程
            Thread threadB = new Thread(() -> {
          
          
                threadLocal.set("ThreadB:" + Thread.currentThread().getName());
                System.out.println("线程B本地变量中的值为:" + threadLocal.get());
                System.out.println("线程B没有删除本地变量:" + threadLocal.get());
            });
    		//启动线程A和线程B
            threadA.start();
            threadB.start();
        }
    }
    

    인쇄된 정보는 다음과 같습니다.

    线程A本地变量中的值为:ThreadAThread-0
    线程B本地变量中的值为:ThreadBThread-1
    线程B没有删除本地变量:ThreadBThread-1
    线程A删除本地变量后ThreadLocal中的值为:null
    

    위 프로그램을 통해 스레드 A와 스레드 B가 ThreadLocal에 저장한 변수는 서로 간섭하지 않는다는 것을 알 수 있습니다. 스레드 A가 저장한 변수는 스레드 A만 접근할 수 있고 스레드 B가 저장한 변수는 스레드 스레드 B에서 액세스할 수 있습니다.

  • ThreadLocal 원리

    public class Thread implements Runnable {
          
          
        /***********省略N行代码*************/
        ThreadLocal.ThreadLocalMap threadLocals = null;
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    /***********省略N行代码*************/
    }
    

    위의 소스코드에서 볼 수 있듯이 ThreadLocal 클래스에는 threadLocals 와 InheritableThreadLocals 가 있는데, 두 변수 모두 ThreadLocalMap 형 변수이고 둘 다 초기값은 모두 null 이다. 현재 스레드가 처음으로 해당 세트를 호출할 때만 해당된다. () 메소드 또는 get( ) 메소드는 변수를 인스턴스화합니다.

    각 스레드의 지역변수는 ThreadLocal 인스턴스에 저장되지 않고, 호출한 스레드의 threadLocals 변수에 저장된다는 점에 유의해야 한다. 특정 호출 스레드의 메모리 공간에서 ThreadLocal은 지역 변수 값에 접근하기 위한 get() 및 set() 메소드만 제공하며, set() 메소드가 호출되면 설정할 값은 호출 스레드의 threadLocals에 저장됩니다. 호출 시 get() 메소드를 사용하여 값을 얻으면 threadLocals에 저장된 변수가 현재 스레드에서 얻어집니다.

    1. 세트()

      public void set(T value) {
              
              
          //获取当前线程
          Thread t = Thread.currentThread();
          //以当前线程为key,获取ThreadLocalMap对象
          ThreadLocalMap map = getMap(t);
          if (map != null)
              //获取的获取ThreadLocalMap不为空,则赋值操作
              map.set(this, value);
          else
              //获取ThreadLocalMap为空,则为当前线程创建并赋值
              createMap(t, value);
      }
      
      /**
           * Create the map associated with a ThreadLocal. Overridden in
           * InheritableThreadLocal.
           *
           * @param t the current thread
           * @param firstValue value for the initial entry of the map
           */
          void createMap(Thread t, T firstValue) {
              
              
              t.threadLocals = new ThreadLocalMap(this, firstValue);
          }
      
    2. 얻다()

      /**
       * Returns the value in the current thread's copy of this
       * thread-local variable.  If the variable has no value for the
       * current thread, it is first initialized to the value returned
       * by an invocation of the {@link #initialValue} method.
       *
       * @return the current thread's value of this thread-local
       */
      public T get() {
              
              
          //获取当前线程
          Thread t = Thread.currentThread();
          //获取当前线程ThreadLocalMap
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              
              
              //ThreadLocalMap不为空,则从中取值
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
              
              
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
          //ThreadLocalMap为空,则返回默认值
          return setInitialValue();
      }
      
    3. 제거하다()

      public void remove() {
              
              
          //获取当前线程中的threadLocals
          ThreadLocalMap m = getMap(Thread.currentThread());
          if (m != null)
              //若threadLocals不为空,则清除
              m.remove(this);
      }
      

      참고: 스레드가 종료되지 않으면 지역 변수는 항상 호출 스레드의 ThreadLocal에 존재하므로 지역 변수가 필요하지 않은 경우 ThreadLocal의 제거() 메서드를 호출하여 메모리 오버플로 문제를 방지하기 위해 삭제할 수 있습니다. .

  • ThreadLocal 변수는 전이적이지 않습니다.

    ThreadLocal을 사용하여 저장된 지역 변수는 전이적이지 않습니다. 즉, 동일한 ThreadLocal의 경우 상위 스레드에서 값을 설정한 후 하위 스레드에서 값을 얻을 수 없습니다.

    public class ThreadLocalTest {
          
          
            private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
            public static void main(String[] args) {
          
          
    			//在主线程中设置值
                threadLocal.set("ThreadLocalTest");
    			//在子线程中获取值
                Thread thread = new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        System.out.println("子线程获取值:" + threadLocal.get());
                    }
                });
    			//启动子线程
                thread.start();
    			//在主线程中获取值
                System.out.println("主线程获取值:" + threadLocal.get());
            }
        }
    

    위의 필드 코드를 실행한 후 결과는 다음과 같습니다.

    主线程获取值:ThreadLocalTest
    子线程获取值:null
    

    위의 예에서 볼 수 있듯이 상위 스레드에서 ThreadLocal 값을 설정한 후에는 하위 스레드에서 해당 값을 얻을 수 없습니다.

    하위 스레드에서 상위 스레드의 값을 얻을 수 있는 방법이 있습니까? InheritableThreadLocal을 통해 이를 달성할 수 있습니다.

  • 상속 가능한ThreadLocal

    InheritableThreadLocal 클래스는 ThreadLocal에서 상속되며, 이는 하위 스레드의 상위 스레드에 설정된 값을 얻을 수 있습니다.

    public class ThreadLocalTest {
          
          
            private static InheritableThreadLocal<String> threadLocal = 
                new InheritableThreadLocal<String>();
    
            public static void main(String[] args) {
          
          
    			//在主线程中设置值
                threadLocal.set("ThreadLocalTest");
    			//在子线程中获取值
                Thread thread = new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        System.out.println("子线程获取值:" + threadLocal.get());
                    }
                });
    			//启动子线程
                thread.start();
    			//在主线程中获取值
                System.out.println("主线程获取值:" + threadLocal.get());
            }
        }
    

    위 프로그램을 실행하면 결과는 다음과 같습니다.

    主线程获取值:ThreadLocalTest
    子线程获取值:ThreadLocalTest
    

    InheritableThreadLocal을 사용하면 하위 스레드가 상위 스레드에 설정된 지역 변수를 얻을 수 있음을 알 수 있습니다.

  • InheritableThreadLocal 원칙

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
          
          
        
        protected T childValue(T parentValue) {
          
          
            return parentValue;
        }
    
        /**
         * Get the map associated with a ThreadLocal.
         *
         * @param t the current thread
         */
        ThreadLocalMap getMap(Thread t) {
          
          
           return t.inheritableThreadLocals;
        }
    
        /**
         * Create the map associated with a ThreadLocal.
         *
         * @param t the current thread
         * @param firstValue value for the initial entry of the table.
         */
        void createMap(Thread t, T firstValue) {
          
          
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }
    

    InheritableThreadLocal이 ThreadLocal에서 상속되고 childValue()/getMap()/createMap() 메서드를 재정의하는 소스 코드에서 볼 수 있습니다.

    즉, ThreadLocal의 set() 메소드가 호출되면 threadLocals 변수 대신 현재 스레드의 InheritableThreadLocals 변수가 생성되는데, 이때 상위 스레드가 하위 스레드를 생성하면 Thread의 생성자에서 클래스, 상위 스레드의 InheritableThreadLocals 변수에 있는 지역 변수는 하위 스레드의 InheritableThreadLocals에 복사됩니다.

동시성 문제

가시성 문제

가시성 문제, 즉 한 스레드가 공유 변수를 수정하면 다른 스레드는 수정 사항을 즉시 볼 수 없으며 이는 CPU 추가 캐시로 인해 발생합니다.

단일 코어 CPU에는 가시성 문제가 없으며 다중 코어 CPU에만 가시성 문제가 있습니다. 단일 코어 CPU는 실제로 단일 코어 타임 슬라이스 스케줄링으로 인해 직렬로 실행되는 반면 다중 코어는 병렬성을 달성할 수 있습니다.

가시성 샘플 코드:

public class ConcurrentTest {
    
    

    private int num = 0;

    public static void main(String[] args) throws InterruptedException {
    
    
        ConcurrentTest concurrentTest = new ConcurrentTest();
        concurrentTest.threadsTest();
    }

    public void threadsTest() throws InterruptedException {
    
    
        Thread thread = new Thread("test-1") {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 20; i++) {
    
    
                    try {
    
    
                        addNum();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread thread2 = new Thread("test-2") {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 20; i++) {
    
    
                    try {
    
    
                        addNum();

                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };

        thread.start();
        thread2.start();

        thread.join();
        thread2.join();
        System.out.println("执行完毕");
    }

    private void addNum() throws InterruptedException {
    
    
        Thread.sleep(1000);
        num++;
        System.out.println(Thread.currentThread().getName() + ":" + num);
    }
}

원자성 문제

원자성은 CPU 실행 중에 하나 이상의 작업이 중단되지 않는 특성을 나타냅니다. 원자적 작업이 실행되기 시작하면 중단 없이 작업이 끝날 때까지 계속됩니다.

원자성 문제는 CPU 실행 중에 하나 이상의 작업이 중단되는 상황을 나타냅니다.

스레드가 작업을 수행할 때 CPU는 다른 작업을 수행하도록 전환하여 현재 작업이 중단되고 원자성 문제가 발생합니다.

JAVA에서는 동시성 프로그램이 멀티스레딩을 기반으로 작성되는데, 여기에는 스레드 간 CPU 전환 문제도 포함됩니다. 동시 프로그래밍에서 원자성 문제가 발생할 수 있는 것은 바로 스레드 간 CPU 전환 때문입니다.

주문 문제

질서는 코드가 실행 순서대로 실행된다는 것을 의미합니다.

명령어 재정렬: 프로그램 성능을 최적화하기 위해 컴파일러나 인터프리터가 프로그램의 실행 순서를 수정하는 경우가 있는데, 이로 인해 예상치 못한 문제가 발생할 수 있습니다.

단일 스레드의 경우 명령 재정렬을 통해 최종 결과가 프로그램의 순차적 실행 결과와 일치하도록 보장할 수 있지만 다중 스레드의 경우 문제가 발생할 수 있습니다.

질서 문제: 프로그램을 최적화하기 위해 CPU는 명령을 재정렬하는데, 이 때 실행 순서가 인코딩 순서와 일치하지 않아 질서 문제가 발생할 수 있습니다.

요약하다

동시 프로그래밍으로 인해 문제가 발생할 수 있는 세 가지 주요 이유는 캐시로 인한 가시성 문제, CPU 스레드 전환으로 인한 원자성 문제, 성능 최적화 명령 재정렬로 인한 순서 문제입니다.

Happens-Before 원칙

JDK1.5 버전에서는 JAVA 메모리 모델에 Happens-Before 원칙이 도입되었습니다.

샘플 코드 1:

class VolatileExample {
    
    
    int x = 0;
    volatile boolean v = false;

    public void writer() {
    
    
        x = 1;
        v = true;
    }

    public void reader() {
    
    
        if (v == true) {
    
    
            //x的值是多少呢?
        }
    }
}
  1. 프로그램 순서 규칙

    ​ 단일 스레드에서는 코드 순서에 따라 이전 작업이 후속 작업보다 먼저 발생합니다.

    ​ 예: 예시 1에서 코드 x = 1은 v = true 이전에 완료됩니다.

  2. 휘발성 변수 규칙

    휘발성 쓰기 작업의 경우 발생 - 후속 읽기 작업 전

  3. 배송 규칙

    ​ A가 발생하기 전에 - B가 발생하고, B가 발생하기 전에 - C가 발생하면 A가 발생하기 전에 - C가 발생합니다.

    원칙 1, 2, 3과 샘플 코드 1을 결합하여 다음과 같은 결론을 내릴 수 있습니다.

    • x = 1 발생 - v = true 이전, 원칙 1과 일치;
    • 쓰기 변수 v = true 읽기 전 발생 변수 v = true, 원칙 2와 일치
    • 그러면 원리 3에 따라 x = 1이 발생합니다. - 변수를 읽기 전에 v = true;
    • 즉, 스레드 B가 v = true를 읽으면 스레드 A가 설정한 x = 1이 B에 표시됩니다. 이는 스레드 B가 이때 x = 1에 액세스할 수 있음을 의미합니다.
  4. 잠금 규칙

    잠금 해제 작업은 잠금의 후속 잠금 작업 전에 발생합니다.

  5. 스레드 시작 규칙

    ​ 스레드 A가 스레드 B의 start()를 호출하여 스레드 B를 시작하면 스레드 B의 모든 작업 전에 start() 메서드가 발생합니다.

    //在线程A中初始化线程B
    Thread threadB = new Thread(()->{
          
          
        //此处的变量x的值是多少呢?答案是100
    });
    //线程A在启动线程B之前将共享变量x的值修改为100
    x = 100;
    //启动线程B
    threadB.start();
    
  6. 스레드 종료 규칙

    ​ 스레드 A는 스레드 B가 완료될 때까지 기다립니다(스레드 B의 Join() 메서드 호출). 스레드 B가 완료되면(스레드 B의 Join() 메서드 반환) 스레드 A는 스레드 B의 공유 변수 수정 사항에 액세스할 수 있습니다.

    Thread threadB = new Thread(()-{
          
          
    //在线程B中,将共享变量x的值修改为100 
            x = 100;
    });
    //在线程A中启动线程B
    threadB.start();
    //在线程A中等待线程B执行完成
    threadB.join();
    //此处访问共享变量x的值为100
    
  7. 스레드 중단 규칙

    ​ 스레드의 Interrupt() 메서드에 대한 호출은 중단된 스레드가 인터럽트 이벤트 발생을 감지하기 전에 발생합니다.

  8. 객체 마무리 규칙

    ​ Happens - 객체의 초기화가 완료된 후 객체의 finilize() 메서드 시작 전

ForkJoin 프레임워크

  • 개요

    Java 1.7에는 새로운 동시성 프레임워크인 Fork/Join 프레임워크가 도입되었습니다. 이는 주로 "분할 및 정복" 알고리즘, 특히 분할 및 정복 후에 재귀적으로 호출되는 함수를 구현하는 데 사용됩니다.

    Fork/Join 프레임워크의 핵심은 작업을 병렬로 실행하기 위한 프레임워크로, 큰 작업을 여러 개의 작은 작업으로 나누고 최종적으로 각 작은 작업의 결과를 집계하여 큰 작업의 결과를 얻을 수 있는 프레임워크입니다. Fork/Join 프레임워크는 ThreadPool과 공존하며 ThreadPool을 대체하기 위한 것이 아닙니다.

  • 원칙

    Fork/Join은 무한 큐를 이용해 실행해야 할 작업을 저장하고 생성자를 통해 스레드 수를 전달하는데, 스레드 수를 전달하지 않으면 현재 컴퓨터에서 사용할 수 있는 CPU 수는 기본적으로 스레드 수로 설정됩니다.

    ForkjoinPool은 주로 분할 정복 방법을 사용하여 문제를 해결합니다. 일반적인 응용 프로그램에는 빠른 정렬 알고리즘이 포함됩니다.

  • 프레임워크 구현

    1. 포크조인풀

      ForkJoin 프레임워크에 스레드 풀 구현

    2. 포크조인작업자스레드

      ForkJoin 프레임워크에서 스레드 구현

    3. ForkJoinTask

      이는 데이터와 해당 작업을 캡슐화하고 세분화된 데이터 병렬 처리를 지원합니다.

      ForkJoinTask에는 주로 작업 분할 및 병합을 실현하기 위한 두 가지 메서드인 fork() 및 Join()이 포함되어 있습니다.

      fork() 메서드는 Thread 메서드의 start() 메서드와 유사하지만 작업을 즉시 실행하지 않고 작업을 실행 대기열에 넣습니다.

      Join() 메소드는 Thread의 Join() 메소드와 달리 단순히 스레드를 차단하는 것이 아니라 작업자 스레드를 사용하여 다른 작업을 수행합니다. 작업자 스레드가 Join()을 호출하면 작업자 스레드가 Join()을 호출할 때까지 다른 작업을 처리합니다. 하위 스레드를 확인하고 실행이 완료되었습니다.

    4. 재귀 작업

      결과를 반환하는 ForkJoinTask는 Callable을 구현합니다.

    5. 재귀적 작업

      반환 결과가 없는 ForkJoinTask는 Runnalbe를 구현합니다.

    6. 계산완료

      작업 실행이 완료되면 사용자 정의 후크 기능이 실행되어 실행됩니다.

  • 샘플 코드

    public class ForkJoinTaskExample extends RecursiveTask<Integer> {
          
          
        public static final int threshold = 2;
        private int start;
        private int end;
    
        public ForkJoinTaskExample(int start, int end) {
          
          
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Integer compute() {
          
          
            int sum = 0;
    		//如果任务足够小就计算任务
            boolean canCompute = (end - start) <= threshold;
            if (canCompute) {
          
          
                for (int i = start; i <= end; i++) {
          
          
                    sum += i;
                }
            } else {
          
          
    			// 如果任务大于阈值,就分裂成两个子任务计算
                int middle = (start + end) / 2;
                ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
                ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
    			// 执行子任务
                leftTask.fork();
                rightTask.fork();
    			// 等待任务执行结束合并其结果
                int leftResult = leftTask.join();
                int rightResult = rightTask.join();
    			// 合并子任务
                sum = leftResult + rightResult;
            }
            return sum;
        }
    
        public static void main(String[] args) {
          
          
            ForkJoinPool forkjoinPool = new ForkJoinPool();
    		//生成一个计算任务,计算1+2+3+4
            ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
    		//执行一个任务
            Future<Integer> result = forkjoinPool.submit(task);
            try {
          
          
                log.info("result:{}", result.get());
            } catch (Exception e) {
          
          
                log.error("exception", e);
            }
        }
    }
    

v = true 이전에 완료됨;

  1. 휘발성 변수 규칙

    휘발성 쓰기 작업의 경우 발생 - 후속 읽기 작업 전

  2. 배송 규칙

    ​ A가 발생하기 전에 - B가 발생하고, B가 발생하기 전에 - C가 발생하면 A가 발생하기 전에 - C가 발생합니다.

    원칙 1, 2, 3과 샘플 코드 1을 결합하여 다음과 같은 결론을 내릴 수 있습니다.

    • x = 1 발생 - v = true 이전, 원칙 1과 일치;
    • 쓰기 변수 v = true 읽기 전 발생 변수 v = true, 원칙 2와 일치
    • 그러면 원리 3에 따라 x = 1이 발생합니다. - 변수를 읽기 전에 v = true;
    • 즉, 스레드 B가 v = true를 읽으면 스레드 A가 설정한 x = 1이 B에 표시됩니다. 이는 스레드 B가 이때 x = 1에 액세스할 수 있음을 의미합니다.
  3. 잠금 규칙

    잠금 해제 작업은 잠금의 후속 잠금 작업 전에 발생합니다.

  4. 스레드 시작 규칙

    ​ 스레드 A가 스레드 B의 start()를 호출하여 스레드 B를 시작하면 스레드 B의 모든 작업 전에 start() 메서드가 발생합니다.

    //在线程A中初始化线程B
    Thread threadB = new Thread(()->{
          
          
        //此处的变量x的值是多少呢?答案是100
    });
    //线程A在启动线程B之前将共享变量x的值修改为100
    x = 100;
    //启动线程B
    threadB.start();
    
  5. 스레드 종료 규칙

    ​ 스레드 A는 스레드 B가 완료될 때까지 기다립니다(스레드 B의 Join() 메서드 호출). 스레드 B가 완료되면(스레드 B의 Join() 메서드 반환) 스레드 A는 스레드 B의 공유 변수 수정 사항에 액세스할 수 있습니다.

    Thread threadB = new Thread(()-{
          
          
    //在线程B中,将共享变量x的值修改为100 
            x = 100;
    });
    //在线程A中启动线程B
    threadB.start();
    //在线程A中等待线程B执行完成
    threadB.join();
    //此处访问共享变量x的值为100
    
  6. 스레드 중단 규칙

    ​ 스레드의 Interrupt() 메서드에 대한 호출은 중단된 스레드가 인터럽트 이벤트 발생을 감지하기 전에 발생합니다.

  7. 객체 마무리 규칙

    ​ Happens - 객체의 초기화가 완료된 후 객체의 finilize() 메서드 시작 전

ForkJoin 프레임워크

  • 개요

    Java 1.7에는 새로운 동시성 프레임워크인 Fork/Join 프레임워크가 도입되었습니다. 이는 주로 "분할 및 정복" 알고리즘, 특히 분할 및 정복 후에 재귀적으로 호출되는 함수를 구현하는 데 사용됩니다.

    Fork/Join 프레임워크의 핵심은 작업을 병렬로 실행하기 위한 프레임워크로, 큰 작업을 여러 개의 작은 작업으로 나누고 최종적으로 각 작은 작업의 결과를 집계하여 큰 작업의 결과를 얻을 수 있는 프레임워크입니다. Fork/Join 프레임워크는 ThreadPool과 공존하며 ThreadPool을 대체하기 위한 것이 아닙니다.

  • 원칙

    Fork/Join은 무한 큐를 이용해 실행해야 할 작업을 저장하고 생성자를 통해 스레드 수를 전달하는데, 스레드 수를 전달하지 않으면 현재 컴퓨터에서 사용할 수 있는 CPU 수는 기본적으로 스레드 수로 설정됩니다.

    ForkjoinPool은 주로 분할 정복 방법을 사용하여 문제를 해결합니다. 일반적인 응용 프로그램에는 빠른 정렬 알고리즘이 포함됩니다.

  • 프레임워크 구현

    1. 포크조인풀

      ForkJoin 프레임워크에 스레드 풀 구현

    2. 포크조인작업자스레드

      ForkJoin 프레임워크에서 스레드 구현

    3. ForkJoinTask

      이는 데이터와 해당 작업을 캡슐화하고 세분화된 데이터 병렬 처리를 지원합니다.

      ForkJoinTask에는 주로 작업 분할 및 병합을 실현하기 위한 두 가지 메서드인 fork() 및 Join()이 포함되어 있습니다.

      fork() 메서드는 Thread 메서드의 start() 메서드와 유사하지만 작업을 즉시 실행하지 않고 작업을 실행 대기열에 넣습니다.

      Join() 메소드는 Thread의 Join() 메소드와 달리 단순히 스레드를 차단하는 것이 아니라 작업자 스레드를 사용하여 다른 작업을 수행합니다. 작업자 스레드가 Join()을 호출하면 작업자 스레드가 Join()을 호출할 때까지 다른 작업을 처리합니다. 하위 스레드를 확인하고 실행이 완료되었습니다.

    4. 재귀 작업

      결과를 반환하는 ForkJoinTask는 Callable을 구현합니다.

    5. 재귀적 작업

      반환 결과가 없는 ForkJoinTask는 Runnalbe를 구현합니다.

    6. 계산완료

      작업 실행이 완료되면 사용자 정의 후크 기능이 실행되어 실행됩니다.

  • 샘플 코드

    public class ForkJoinTaskExample extends RecursiveTask<Integer> {
          
          
        public static final int threshold = 2;
        private int start;
        private int end;
    
        public ForkJoinTaskExample(int start, int end) {
          
          
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Integer compute() {
          
          
            int sum = 0;
    		//如果任务足够小就计算任务
            boolean canCompute = (end - start) <= threshold;
            if (canCompute) {
          
          
                for (int i = start; i <= end; i++) {
          
          
                    sum += i;
                }
            } else {
          
          
    			// 如果任务大于阈值,就分裂成两个子任务计算
                int middle = (start + end) / 2;
                ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
                ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
    			// 执行子任务
                leftTask.fork();
                rightTask.fork();
    			// 等待任务执行结束合并其结果
                int leftResult = leftTask.join();
                int rightResult = rightTask.join();
    			// 合并子任务
                sum = leftResult + rightResult;
            }
            return sum;
        }
    
        public static void main(String[] args) {
          
          
            ForkJoinPool forkjoinPool = new ForkJoinPool();
    		//生成一个计算任务,计算1+2+3+4
            ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
    		//执行一个任务
            Future<Integer> result = forkjoinPool.submit(task);
            try {
          
          
                log.info("result:{}", result.get());
            } catch (Exception e) {
          
          
                log.error("exception", e);
            }
        }
    }
    

추천

출처blog.csdn.net/weixin_40709965/article/details/128160545