【Java学习笔记-并发编程】线程与任务

前言

最近在看一些Java15的并发、线程调度以及一些实现方案的东西,虽然很多东西还是 1.5 的,但还是很有收获。

一、线程与任务

Java中,要用线程来执行任务,线程可以说是任务的容器。没有线程的物理开启(start0),就不会有任务被执行。

如果看过 Thread 源码就能知道,Java 对线程的实现是非常封闭的,其机制来源于c的低级的p线程方法。源码中,通过 native 关键字,依托于JNI接口,调用其他语言来实现对底层的访问。

二、Java线程与任务的基础接口

在 concurrent 和 lang 包中,首先有几个基础接口需要了解:

  • Runnable
  • Callable
  • Future

Runnable

对于 任务或线程 而言,最基本和初级的功能就是运行,所以在 Java1.0 的时候,只有Runnable接口。Runnable 接口的规范也非常简单:

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

没有返回值也没有异常抛出,就是简简单单的执行,将任务执行的代码实现在 run 函数里就成。如果需要获得任务执行结果,必须在函数中写 回调函数(callback function)

实现 Runnable 接口,根据实际业务需求,抽象出具有个性化、简单化的、需要新线程执行的并行任务。

Thread 是实现 Runnable 的一个实现类,所以我个人理解,在 Java 的视角,线程实际上是一个特殊的任务。

  • Runnable 适合作为一个被实现的接口被任务类实现(因为 Thread 与 Executor 只能输入 Runnable)。

Callable

但如果仅仅是这样,可不满足我们对于任务管理的要求。线程执行任务所抛出的任务异常(注意不是线程异常,两者本质区别),以及返回的结果,我们想要更方便的获取。于是,在 Java1.5 中就有了 Callable 接口。

Callable 接口规范也不复杂:

public interface Callable<V> {
    
    
    V call() throws Exception;
}

和 Runnable 比起来,我们可以看到明显的改变。首先在线程执行任务的过程中,我们可以 catch 到任务抛出的异常。其次,我们可以拿到输入类型的返回值。

  • Callable 更适合作为一个任务内容被写到任务中,因为可以在 run 中轻松处理抛出异常。(这点在FutureTask 中会有所体现)

Future

在 Java1.5 中还提供了 Future 接口,来对任务进行更详细的管理。

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, TimeoutException;
}

在这个接口规范中,我们可以对线程的状态进行监控。首先,提供了手动终结线程的规范。其次,比较好用的是,有了 get 函数,意味着我们可以在任意时间与地方(任意行),阻塞获取线程的计算结果。

三、Java线程与任务的基础实现类

在了解完这些基础的接口后,来看几个 Java 线程的实现类(Thread)与经典任务(FutureTask)的实现类,看看 Java 是怎么运用这些规范的。

Thread

Thread 就是 Java 中最简单、最直接,也是最底层创建线程对象,开启一个线程的类。

注意,一定要区别 run 函数和 start 函数。start 函数是物理开启一个线程,run 函数只是调用的我们对 Runnable 的实现(线程中执行的代码,即任务)。

	//创建对象
    Thread thread = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            //代码实现
            //如果想让线程回传结果,只能在这里面写回调函数。
        }
    });
    //开启线程
    thread.start();

上面代码中,我们可以粗浅地理解为,将一个任务(Runnable 的实现)放到一个线程对象中,之后调用 start 函数让线程计算任务。

查看源码得知, Thread的构造器只能传入Runnable的实现。 所以,如果在不用线程池的情况下,在自己编写业务的任务类(Task)时,必须 implements Runnable。

我们看一下,上面提到的,实际开启线程的地方:

    public synchronized void start() {
    
    

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
            
        group.add(this);

        boolean started = false;
        try {
    
    
        	//这里是真正物理层开启线程的地方 start0() 函数~~~
            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 */
            }
        }
    }

    private native void start0();
    
    //仅仅是调用实现
    @Override
    public void run() {
    
    
        if (target != null) {
    
    
            target.run();
        }
    }

我们看到,真正在物理层开启线程的是 start() 中的 start0() 函数,就是上面说的JNI接口,调用其他语言来实现对底层的访问。也就是说,线程真正的被创建出来运行靠的是start()。

Runnable run 函数的实现,被位于 Thread 中的 run 函数调用,但是 Thread 的 run 是如何放到 start0 开启的线程中执行的,目前我还是不太清楚。需要接下来进一步的学习。

除了start0,还有很多操作是非 Java 实现的,比如:

    private native void setPriority0(int newPriority);
    private native void stop0(Object o);
    private native void suspend0();
    private native void resume0();
    private native void interrupt0();
    private static native void clearInterruptEvent();
    private native void setNativeName(String name);

FutureTask

FutureTask 是对 Runnable 和 Future 的基本实现,实际就是对一个异步任务的基本管理,我们可以大致阅读一下其中的实现细节,为我们实现自己 Task 提供思路。

我们先看一下,这个类的实现继承关系:

在这里插入图片描述
清楚的看到,FutureTask 实际上是 Runnable 和 Future 的组合实现。(之前说的 Task 概念在这里也有所体现)。

    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;

在这个类中,可以看到任务具有的状态 state(任务的状态),包含一个 Callable (任务内容),输出结果(任意对象),以及 Thread(执行任务的Thread) 和 WaitNode(这个以后再提)。

当然,实际业务中,一个简单的 Task ,可能只需要有一个区别的 id 、判断执行的 handle 以及锁 lock 就能满足基础功能。

在 FutureTask 的构造器中,可以清晰地看到对象的初始化过程,以及这个类的构建本质。

    public FutureTask(Callable<V> callable) {
    
    
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;
    }

    public FutureTask(Runnable runnable, V result) {
    
    
    	//Executors 运用了 RunnableAdapter 将 Runnable 转为 Callable<T>
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;   
    }

可以看到,即使是用第二个构造器,在内部也把 Runnable 转化成了 Callable。

再观察一下其中的 run 函数:

    public void run() {
    
    
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
    
    
            Callable<V> c = callable;
            if (c != null && state == NEW) {
    
    
                V result;
                boolean ran;
                //这里就是为什么推荐使用Callable作为输入,因为方便catch异常,
                //不然只能在 Runnable 的run中回调。
                try {
    
    
                	//执行自己实现的call函数
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
    
    
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
    
    
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

我们可以看到最基本的,在中断条件中,任务不能被重复执行,本对象不能执行其他线程。之后也是正常的实现流程。

FutureTask 的使用方法也很简单,这里建议输入 Callable:

    FutureTask<String> f = new FutureTask<>(new Callable<String>() {
    
    
        @Override
        public String call() throws Exception {
    
    
            return null;
        }
    });
    Thread t = new Thread(f);
    t.start();

这里体现的也十分明显,把一个任务放入一个线程中去执行,并且获取任务的各种状态。

四、总结

在 Java 多线程的学习中,必须要理解线程与任务的区别、Runnable 与 Callable 的本质区别(不是代码上表象的),以及他们之间的联系。

Callable 更适合作为一个任务内容被写到任务中(因为可以在 run 中轻松处理抛出异常),Runnable 适合作为一个被实现的接口被任务类实现(因为 Thread 与 Executor 只能输入 Runnable)。 这点在 FutureTask 这个类中体现的淋漓尽致,再来体会一下:

  • 为什么推荐 implements Runnable
	//新建任务
    FutureTask<String> f = new FutureTask<>(new Callable<String>() {
    
    
        @Override
        public String call() throws Exception {
    
    
            return null;
        }
    });
    //为什么推荐 implements Runnable
    Thread t = new Thread(f);
    //物理开启线程
    t.start();
  • 为什么推荐Callable输入到构造器。
    public void run() {
    
    
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
    
    
            Callable<V> c = callable;
            if (c != null && state == NEW) {
    
    
                V result;
                boolean ran;
                //这里就是为什么推荐使用Callable作为输入,因为方便catch异常,
                //不然只能在 Runnable 的run中回调。
                try {
    
    
                	//执行自己实现的call函数
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
    
    
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
    
    
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

要时刻记住, 线程是任务的容器,线程的物理启动和任务的实现代码是分开的。 这样,才能更深刻的理解 Java 多线程的本质,并且对于我们之后理解线程池是有帮助的。

猜你喜欢

转载自blog.csdn.net/weixin_43742184/article/details/113388988