本文共 6450 字,大约阅读时间需要 21 分钟。
前言
最近在看一些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 call() throws Exception;
}
和 Runnable 比起来,我们可以看到明显的改变。首先在线程执行任务的过程中,我们可以 catch 到任务抛出的异常。其次,我们可以拿到输入类型的返回值。
Callable 更适合作为一个任务内容被写到任务中,因为可以在 run 中轻松处理抛出异常。(这点在FutureTask 中会有所体现)
Future
在 Java1.5 中还提供了 Future 接口,来对任务进行更详细的管理。
public interface Future {
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 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 callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
//Executors 运用了 RunnableAdapter 将 Runnable 转为 Callable
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 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 f = new FutureTask<>(new Callable() {
@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 f = new FutureTask<>(new Callable() {
@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 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 多线程的本质,并且对于我们之后理解线程池是有帮助的。
转载地址:https://blog.csdn.net/weixin_34237125/article/details/114789254 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!