java task和thread_【Java学习笔记-并发编程】线程与任务
发布日期:2021-06-24 17:55:12 浏览次数:2 分类:技术文章

本文共 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 提供思路。

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

ca5672534ee0

在这里插入图片描述

清楚的看到,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 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Java边缘填充_任意画一个多边形,用边缘填充算法填充
下一篇:java怎么使两个界面联系_怎么样用java编写界面实现两个数的加法运算

发表评论

最新留言

感谢大佬
[***.8.128.20]2024年04月26日 04时52分29秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

IDEA默认的快捷键整理!! 2019-04-28
超宽带雷达P440?雷达的快时间慢时间是什么意思? 2019-04-28
教育授权certificate used to sign the license 2019-04-28
从内存解释:方法传参(基本数据类型、引用数据类型(对象、数组、String类型)),原值是否改变? 2019-04-28
java中数组为空和数组长度为0的区别 2019-04-28
SecureCRT连接不上虚拟机 2019-04-28
为什么在创建线程时,更推荐用实现Runnable接口的方法?而不是继承Thread类的方法? 2019-04-28
从100万个数中找出最大的前100个数-最小堆、分块 2019-04-28
并发下的ArrayList、HashMap 2019-04-28
仿牛客社区项目2.2登录模块——开发注册功能 2019-04-28
仿牛客社区项目2.3登录模块——会话管理Cookie、Session 2019-04-28
仿牛客社区项目2.4登录模块——生成验证码 2019-04-28
JDK-JRE-JVM关系、Java语言跨平台 2019-04-28
仿牛客社区项目2.5登录模块———登录退出功能 2019-04-28
仿牛客社区项目2.6登录模块———显示登录信息(拦截器、ThreadLocal) 2019-04-28
仿牛客社区项目2.8登录模块——检查登录状态(自定义注解、拦截器) 2019-04-28
仿牛客社区项目2.7登录模块——账号设置(上传图片、响应图片) 2019-04-28
仿牛客社区项目3.1——过滤敏感词(前缀树) 2019-04-28
仿牛客社区项目3.2——发布帖子(异步通信技术AJAX) 2019-04-28
仿牛客社区项目3.3——帖子详情(普通功能) 2019-04-28