[고급 동시성] Future는 Callable 크로스 스레드 반환 결과 및 예외 발생의 원리 분석을 제어합니다.

  안녕하세요 여러분 코더입니다 지난 글에서 미래를 지배하는 퓨처의 어리석은 아들 러너블의 단점에 대해 이야기 해봤습니다 . 스레드의 시작 Thread.start()는 기본적으로 start()가 기본 메서드 start0()을 호출한 다음 시스템 스레드를 호출하고 시스템 스레드의 Runnable에서 run() 메서드를 다시 호출한다는 것을 알고 있습니다. 그리고 전체 프로세스가 비동기식이어서 Runnable에 두 가지 치명적인 결함이 있습니다. 첫 번째는 결과를 반환할 수 없다는 것이고 두 번째는 예외를 throw할 수 없다는 것입니다. 그래서 Java 아빠는 나중에 이 두 가지 결함을 해결하도록 설계된 Callable 인터페이스를 도입했습니다.

  Callable의 사용법은 이미 알고 계시리라 생각합니다. 선임 프로그래머로서 사용법을 아는 것만으로는 충분하지 않습니다. 우리도 그 이유를 알아야 합니다. 따라서 다음 질문에 대해 생각해 봅시다.

  1. call() 메서드도 run() 메서드와 같이 시스템 스레드를 통해 직접 호출됩니까?
  2. Callable은 결과를 메인 스레드에 어떻게 반환합니까?
  3. Callable은 어떻게 메인 스레드에 예외를 던집니까?

나는 앞으로 몇 분 안에 Callable에 대한 새로운 이해를 갖게 될 것이라고 믿습니다.

  실제로 이러한 기능의 구현은 Callable 인터페이스만으로는 달성할 수 없으며, 이 기능을 완성하기 위해 Future 및 FutureTask 클래스도 사용해야 합니다.

Three Stooges Future, FutureTask, Callable 소개

  소개하기 전에 한 번 생각해보자 스레드가 결과를 반환하는 기능을 구현하라고 한다면 어떻게 해야 할까?

아래 그림과 같이:f1.png

  메인 쓰레드에서 보듯이 쓰레드는 비동기적으로 시작되는데 Thread.start(), 실제로 쓰레드가 실행된 후에는 이미 메인 쓰레드가 종료된 것이다. 그래서 결과를 돌려주는 실의 기능을 구현하려면 곡선을 통해 나라를 구하는 방식으로 구현해야 하는데 그게 무슨 의미일까요?

  스레드가 비동기식이기 때문에 결과를 얻으려면 메인 스레드를 차단하고 스레드가 끝날 때까지 기다린 후 결과를 메인 스레드로 다시 호출해야 합니까?Java 1.5부터 Java Dad는 인터페이스를 제공했습니다. Future간단히 말해서 Future 클래스는 비동기 스레드의 미래 결과를 나타냅니다. 이 결과는 처리가 완료된 후 최종적으로 Future에 나타납니다. Future.get()이 방법은 차단 기능을 구현합니다. 자세한 내용은 이 그림을 참조하세요.

f2.png

  그림과 같이 메인 쓰레드는 FutureTask 태스크를 Thread에 전달하고, Thread.start()이를 시작한 후 run()해당 메소드에서 메소드를 호출하고, 메소드 를 통해 메인 쓰레드에 Callable.call()반환값을 반환한다 .Future.get()

전체 클래스 다이어그램을 살펴보십시오.

f3.png

  클래스 다이어그램에서 볼 수 있듯이 FutureTask는 Future와 Runnable의 구현 클래스이며 Thread와 Callable 인스턴스를 모두 가지고 있으며 FutureTask는 Future의 기능을 구현하므로 FutureTask 는 스레드 차단을 관리하여 결과를 얻을 뿐만 아니라(get ()), 스레드 취소 인터럽트(cancel()) 및 기타 기능.

결과를 저장하기 위해 FutureTask 에 Object outcome변수 가 정의되어 있습니다.

연결 목록은 waiters결과를 얻기 위해 대기 중인 모든 스레드를 저장하도록 정의됩니다.

사실 FutureTask는 Thread와 Callable(어댑터 모드)의 기능을 통합한 작업 관리 센터로 볼 수 있습니다. 그런 다음 코드를 결합하여 특정 논리를 분석합니다.

코드 분석 구현

위의 관계 다이어그램과 순서도를 사용하면 다음 코드를 보는 데 여전히 문제가 있습니다.

먼저 간단한 사용 과정을 살펴보고 여기서 자세히 설명하지는 않겠습니다.

  1. FutureTask 인스턴스를 만듭니다.
  2. 인스턴스를 스레드에 전달하고 시작
  3. 결과 가져오기 차단
public class FutureTest {
    public static void main(String[] args) {
        //1. 创建FutureTask并传入callable实例
        Future<String> stringFutureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(3000);
                return "FutureTask";
            }
        });
        //2. 把实例传递给Thread并启动
        new Thread(stringFutureTask).start();

        String s = "";
        try {
            //3. 阻塞获取结果
            s = stringFutureTask.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.printf(s);
    }
}

다음으로 이 세 단계를 소스 코드와 함께 분석합니다.

1. FutureTask를 만들고 호출 가능한 인스턴스를 전달합니다.

public FutureTask(Callable<V> callable) {
    ...
    this.callable = callable;
  	this.state = NEW;
}

콜러블을 FutureTask.callable 변수에 전달합니다.

2. 把实例传递给Thread并启动并调用run()方法

  执行new Thread(stringFutureTask).start(),这里启动后实际上调用的是Runnable.run()方法,具体为啥是调用run(),可参照线程的实现方式, 我们看一下FutureTask.run() 源码

@Override
public void run() {
    //把当前线程赋值给FutureTask.runner 实例
    runner = Thread.currentThread();
    try {
      // 赋值变量
      Callable<V> c = callable;
      //启动状态为NEW
      if (c != null && state == NEW) {
        // 定义结果变量
        V result;
        boolean ran;
        try {
          //1. 这里调用 callable.call() 方法,也就是第一步传入的callable. 并返回结果
          result = c.call();
          ran = true;
        } catch (Throwable ex) {
          //如果抛出异常,
          result = null;
          ran = false;
          //改变线程状态为 COMPLETING
          if (STATE.compareAndSet(this, NEW, COMPLETING)) {
            	//2. 把异常赋给 outcome 变量
              outcome = ex;
              // 改变线程状态为 EXCEPTIONAL
              STATE.setRelease(this, EXCEPTIONAL);
        	}
        }
        if (ran) {
          //设置结果并通知所有等待的线程
          set(result);
        }
      }
    } finally {
      ...
    }
}

protected void set(V v) {
    // 改变线程状态为 EXCEPTIONAL
    if (STATE.compareAndSet(this, NEW, COMPLETING)) {
      //3. 把结果赋给 outcome 变量
      outcome = v;
      // 改变线程状态为 NORMAL
      STATE.setRelease(this, NORMAL);
			// 4. 遍历阻塞等待的获取锁的线程,通知他们锁已释放
      for (WaitNode q; (q = waiters) != null;) {
        if (WAITERS.weakCompareAndSet(this, q, null)) {
          for (;;) {
            Thread t = q.thread;
            if (t != null) {
              q.thread = null;
              // 通知锁已释放
              LockSupport.unpark(t);
            }
            FutureTask.WaitNode next = q.next;
            if (next == null) {
              break;
            }
            q.next = null;
            q = next;
          }
          break;
        }
      }
      callable = null;
    }
}

上面的就是线程运行的源码,核心点有4个,

  1. 就是在这里调用 call()方法。
  2. 如果抛出异常把异常存到 Object outcome变量里面
  3. 如果正常返回结果,把结果存到 Object outcome中。至此线程运行完毕。
  4. 遍历阻塞等待的获取锁的线程,通知他们锁已释放

其实就是线程运行完后 把正常结果或者异常结果存到 Object outcome 对像中,释放锁并通知所有等待的线程。

到这里就可以回答开篇的第一个问题 1. call()方法是否也是和run()方法一样通过系统线程直接来调用的?,调用流程是:

Thread.start() --> native start0() --> run() -> call()

可以看出,call() 方法是通过 run() 来调用的,当然这也是在线程中。

3. 阻塞获取结果

这一步是调用 Future.get()方法阻塞线程,等待结果,我们看一下源码:

public V get() throws Exception {
    int s = state;
    if (s <= COMPLETING) {
    	// 1. 如果线程还在执行,就就到waiters 链表里面阻塞等待结果。
    	s = awaitDone();
    }
  	// 获取结果
    if (s == NORMAL) {
      	// 2. 如果正常就返回正常的结果 outcome
      	return (V)outcome;
    }
   	// 3. 如果异常就直接抛出 outcome
    throw new Exception((Throwable)outcome);
}

// 这个方法的意思就是,如果线程还在执行,就到waiters 链表里面等待,
// 一直到被 LockSupport.unpark() 唤醒
private int awaitDone() {
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        int s = state;
        if (s > COMPLETING) {
          return s;
        } else if (s == COMPLETING) {
          Thread.yield();
        } else if (q == null) {
          q = new WaitNode();
        }  if (!queued) {
          queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
        } else {
          LockSupport.park(this);
        }
    }
}

上面的就是阻塞获取结果的源码,核心点有3个,

  1. 如果线程还在执行,就就到waiters 链表里面阻塞等待结果。
  2. 如果线程执行完并正常,就返回正常的结果 outcome
  3. 如果异常就直接抛出 outcome。

看到这里,我们再来回顾一下开篇的几个问题,你是不是有了答案了。

最后

  到这里,Callable,Future 相关的都分析完了,源码解析都比较枯燥,写这么多也不容易,感谢大家看到这里,有什么意见或者建议可以留言一起讨论,看到后第一时间回复,也希望大家能给个赞,你的赞就是我写文章的动力,再次感谢。

GZH: TodoCoder, 저는 때때로 백엔드 관련 솔루션, Java, Go, Python, DevSecOps와 관련된 기술 기사를 공유합니다. 관심을 가져주신 모든 분들을 환영합니다. 감사합니다.

Nuggets Technology Community의 작성자 서명 프로그램 모집에 참여하고 있습니다. 링크를 클릭하여 등록하고 제출 하십시오.

추천

출처juejin.im/post/7117031279178842148