日常开发中,多线程编程是个难以避免的话题,开发者可以小心翼翼、谨慎地、严谨地编程来编写出高效的、安全的多线程程序,但是在长时间的维护中,难免因为其中某个人的某个疏忽而导致出现预料之外的并发问题,比如下面这个简单的类:

class TestActivity extends Activity{

    public void refreshView(){
        //...
    }
}

非常常见的一个类,在Android中只有在主线程可以更新界面,可能一开始写的时候并没有考虑其他线程调用refreshView方法。然而随着项目推进,终于有一天,某段其他线程的代码出于方便直接调用了这个方法,恰好编写调用的开发者没有来检查这个方法是否线程敏感。

解决这类问题很简单,发现问题的时候补上线程检查或线程切换的代码就好了,但这样没法避免下次犯同样的错误。建立良好的代码规范、谨慎编写并及时review可以减少这类问题的出现,但现实开发中有时候会比较仓促,需要一种比较强制性的、便利的、安全的做法来处理这类隐患。

以前在学习Actor多线程模型的时候,了解到将Actor和线程相关联是一个简单且安全的方案,但如果像Actor模型中那样Actor之间利用消息通信在处理这个问题时就显得有些麻烦,差不多需要重构模块间通信方式了。

那么有没有一种简单粗暴的方法可以像Actor模型那样使得“某个类的方法只在某个线程中执行”并且在“进入这个类的方法时自动切换到对应线程”呢?。本文就介绍一种利用动态代理来完成这个任务。

示例场景

Android开发中有两种非常常见的线程敏感场景:1. 只有主线程可以更新界面;2. 不能在主线程进行IO操作或复杂长时间计算。

第一种场景比如下面这段(这里的showSum是一个线程敏感方法,必须在主线程调用):

public interface ViewActor {
    void showSum(int sum);
}

class ViewActorImpl implements ViewActor {
    TextView tvSum;

    @Override
    public void showSum(int sum) {
        tvSum.setText("sum:" + sum);
    }
}

第二种场景就像下面这段(这里的readFileContent需要在子线程中执行):

public interface WorkerActor {
    void readFileContent(String path, Consumer<String> consumer);
}

class WorkerActorImpl implements WorkerActor {

    @Override
    public void readFileContent(String path, Consumer<String> consumer) {
        String content = FileIOUtils.readFile2String(path);
        consumer.accept(content);
    }
}

然而当我们把这些方法标记为public后,这些方法就可能在任意线程执行。

利用代理来确保在正确的线程调用

上面程序都按照“面向接口编程”来写的,这样我们就很容易创建出一个静态代理提供出去,而不是将实现类提供出去,比如:

class ViewActorProxy implements ViewActor {
    private final ViewActor impl;
    private final Handler mainHandler;

    public ViewActorProxy(ViewActor impl) {
        this.impl = impl;
        mainHandler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void showSum(int sum) {
        mainHandler.post(() -> impl.showSum(sum));
    }
}

但如果这个类有10个需要在主线程执行的方法呢?总不能一个个写mainHandler.post吧,正好Java提供了动态代理,原有程序上,我们可以这样写:

public interface ViewActor {

    static ViewActor newInstance() {
        Handler mainHandler = new Handler(Looper.getMainLooper());
        ViewActorImpl impl = new ViewActorImpl();
        return (ViewActor) Proxy.newProxyInstance(ViewActor.class.getClassLoader(), new Class[]{ViewActor.class}, (proxy, method, args) -> {
            mainHandler.post(() -> {
                try {
                    method.invoke(impl);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            });
            return null;//记得处理equals hashCode等Object方法的调用
        });
    }

    void showSum(int sum);
}

class ViewActorImpl implements ViewActor {
    TextView tvSum;

    @Override
    public void showSum(int sum) {
        tvSum.setText("sum:" + sum);
    }
}

外部都通过newInstance方法来获取ViewActor实例,这样对ViewActorImpl的调用将被全部切换到主线程中。

注意:实际开发中,更建议使用依赖注入的方式来创建代理,而不是这种直接使用静态方法,这里仅为了方便展示才这么写。

同理,前面的WorkActor的示例就可以改为:

public interface WorkerActor {

    static WorkerActor getInstance() {
        ExecutorService executorService = Executors.newCachedThreadPool();
        WorkerActorImpl impl = new WorkerActorImpl();
        return (WorkerActor) Proxy.newProxyInstance(WorkerActor.class.getClassLoader(), new Class[]{WorkerActor.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                executorService.submit(() -> method.invoke(impl, args));
                return null;
            }
        });
    }

    void readFileContent(String path, Consumer<String> consumer);
}

class WorkerActorImpl implements WorkerActor {

    @Override
    public void readFileContent(String path, Consumer<String> consumer) {
        String content = FileIOUtils.readFile2String(path);
        consumer.accept(content);
    }
}

虽说上述示例中将创建代理的代码放在对应接口中,但其实可以把这些代码提取出来封装成工具,例如创建主线程对象的代理可以是:

public class MainThreadProxy {
    public static <T> T newProxyFor(Class<T> clazz, T impl) {
        Handler mainHandler = new Handler(Looper.getMainLooper());
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, (proxy, method, args) -> {
            mainHandler.post(() -> {
                try {
                    method.invoke(impl);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            });
            return null;//记得处理equals hashCode等Object方法的调用
        });
    }
}

这样在其他地方就可以通过一行代码直接得到主线程的代理对象:

ViewActor viewActor = MainThreadProxy.newProxyFor(ViewActor.class, new ViewActorImpl());

viewActor.showSum(1);//这行代码无论在哪个线程调用,最终都在主线程执行

写在最后

Java的动态代理还是有些局限,比如它只能针对接口来创建代理,为了使用代理,有时候我们需要额外定义一个接口。

第二个局限是动态代理对应的接口方法在线程切换时不能带返回值,如果要传递返回值,需要通过CPS/回调的方式,比如:

public interface WorkerActor {
    void readFileContent(String path, Consumer<String> consumer);
}

另外,这个方式切换线程默认是以对象为单位的,如果要精确到方法,就需要注解的辅助了,比如:

@ThreadType(ThreadType.IO)
public interface WorkerActor {
    void readFileContent(String path, Consumer<String> consumer);

    @AnyThread
    void enableLog();
}

相应注解的处理实现另外再写文章阐述。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部