Runnable
Runnable 是一个接口,是线程的执行体。源码非常的简单。
public interface Runnable{
public abstract void run();
}
为了应对复杂的场景,需要对Runnable进行简单的自定义,下面就描述两种场景,已经我对这两种场景的思考。
场景1:获取线程(执行体)的执行结果。
场景2:优雅的停止线程。
获取线程(执行体)的执行结果
注意
本文中的执行体可能是Runnable 中的run方法或者Callable中的Call方法。线程的执行结果等价与执行体的执行结果。
Callable,Future
需要获取执行体run的执行结果,可以使用java提供的Callable,Future相关接口。实现例子如下
public class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
doSomething();
return "执行完成";
}
private String doSomething(){
return null;
}
}
public void test1(){
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
doOtherthing();
// 获取结果
try {
String ret = futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
通过Callable,Future相关类获取执行结果的好处有:
- 编写简单,使用java提供的原生接口。
- 如果执行体(Callable中的call 方法)有异常,可以在调用线程获取到异常。
- FutureTask 的 get 方法是阻塞方法,保证执行体一定能够执行完,并且获取到结果。
- 开启线程之后,可以执行其他任务,在需要线程结果的时候,调用FutureTask的get方法即可。例如例子中doOtherthing(),就是在开启线程后并行的任务。
缺点:
由于FutureTask 的 get 方法是阻塞的,因此在一些禁止执行耗时的线程中,调用这个方法是不明智的。例如在Android 系统中 UI 主线程中需要开启一个线程执行耗时任务,同时又想获取线程的执行结果,这时候调用FutureTask 的 get会造成UI 主线程的阻塞,容易造成ANR异常。
使用监听器回调,获取线程执行结果
为了能够在禁止执行耗时操作的线程中(比如说Android 中的UI 主线程)获取被调用线程的执行结果,可以自定义Runnable 或者 Callable来满足这个条件。示例代码如下
public class MyRunnable<T> implements Runnable{
final RunnableListener<T> listener;
public MyRunnable(RunnableListener<T> listener) {
this.listener = listener;
}
@Override
public void run() {
T result = null;
result = doSomething();
listener.onResult(result);
}
}
interface RunnableListener<T>{
void onResult(T result);
}
public void test2(){
new Thread(new MyRunnable<String>(new RunnableListener<String>() {
@Override
public void onResult(String result) {
System.out.println("获取到结果: "+result);
}
})).start();
// 调用者和开启线程是并发的,但是执行doOtherthing及其后续操作的时候,不能保证开启的线程已执行回调函数
doOtherthing();
}
使用监听器回调,获取线程执行结果的好处是:
- 避免了调用线程的阻塞。
- 在步骤式的场景中非常使用,保证了按步执行。
使用监听器回调,获取线程执行结果的好处是缺点是:
- 由于在监听中才能获取到结果,因此被调用线程无法确切知道线程执行结果的时间。
- 由于监听器是在线程中调用,因此监听器和被调用线程处于同一线程,如果监听结果需要在调用线程中执行,需要进行线程切换
- 此方法在步骤式场景中有较好的效果,但是不能适应复杂的场景,比如说获取异常,监听状态,虽然通过复杂的自定义Runnable可以实现,但是这个跟Callable的差不多,因此采用Callable更为合理。
在复杂场景中可以将两者结合使用,当然我们可以定义Runnable或者Callable,具体根据需求而定。自定义Runnable伪代码如下:
public class MyRunnable<T> implements Runnable{
final RunnableListener<T> listener;
public MyRunnable(RunnableListener<T> listener) {
this.listener = listener;
}
@Override
public void run() {
T result = null;
// 创建其他线程,执行耗时操作
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
// 线程中阻塞等待结构
new Thread(futureTask).start();
// 将结果通过监听回调返回到调用线程中。
listener.onResult(result);
}
}
其他方法获取线程执行结果
当然还有其他方式获取线程的执行结果,比如说采用线程通信方式,信号量,消息队列等等。
优雅的停止线程
停止线程的方式有三种:
- 调用Thread中的stop方法。这种方法不推荐,而且在Android 系统中直接抛出异常 UnsupportedOperationException
- 定义通过 标志位 来终止线程
- 通过 Thread.currentThread().isInterrupted() 来判断
方法2和方法3本质都是相同的,都是通过标志位来判断执行体是否需要继续执行。通过标志为来判断,简要代码示例如下
private static class Runner implements Runnable{
private long i;
private (volatile) boolean running =true;
@Override
public void run() {
System.out.println("current Thread Name:"+Thread.currentThread().getName());
while (running ){
i++;
try {
if(aa){
break;
}
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
}
}
System.out.println("Count i= "+i);
System.out.println("current Thread Name:"+Thread.currentThread().getName());
}
public void cancel(){
running =false;
System.out.println("running=false");
}
}
缺点:
- 问题就是一旦线程运行过了running ,那么需要再次运行到判断running的位置才能判断执行体是否需要执行。
- cancel 接口太独特,如果无法获取到Runner 的实例,则无法调用响应的接口。
通过 Thread.currentThread().isInterrupted() 来判断
class TestRunnable1 implements Runnable{
@Override
public void run() {
try{
while (!isInterrupted()){
Log.d(TAG,"time = "+ DateUtil.getCurrentTime("yyyy-MM-dd HH:mm:ss"));
Thread.sleep(10);
}
}catch (InterruptedException e){
System.out.println("线程中断,退出执行");
}
}
/**
* 在使用非当前线程资源的时候调用一下,检查当前线程的状态
* @return
* @throws InterruptedException
*/
private boolean isInterrupted() throws InterruptedException{
if (Thread.currentThread().isInterrupted()){
throw new InterruptedException();
}
return false;
}
}
// 调用
Thread thread = new Thread(new TestRunnable1 ());
thread.interrupt();
通过 Thread.currentThread().isInterrupted() 来判断,和 标志位判断差不多,但是这样做的好处有;
- Thread.currentThread().isInterrupted(),是java提供的方法。在Future 的 cancel ,线程组 ThreadGroup.interrupt() 都是调用这个方法,保证了统一性。
- InterruptedException 异常在线程的阻塞状态,等等状态都有抛出,这样可以进行一个统一处理。
- 总之使用Thread.currentThread().isInterrupted的好处就是保证了统一性。
注意
- 通过 Thread.currentThread().isInterrupted() 来停止线程,显然要比标示位优雅一些,但要防止条件判断永远无法生效。因此我个人觉得在调用非本线程资源是可以调用一下,因此,我封装了一个简单的接口isInterrupted()。提高了一点调用的方便性。
- 注意 interrupt,interrupted,isInterrupted 区别