多线程的三种常见实现方式

说白了,多线程的实现听起来可能很复杂,但实际上很简单。
我们先来说说最重要的事情。
三种方法各有其优点。

我们去年做的一个项目,我们选择继承Thread类。
这是直接重写类中 run() 方法的最简单方法。
例如,在myThread_1 的例子中,我们可以通过继承Thread类,使用start()方法直接启动线程(大约有3 000个项目实现了这种方式)。
但一开始我觉得这个方法不够灵活,后来发现很多基本功能其实就是这种简单直接的方法。

还有一点,很多人没有意识到Runnable接口的实现方式更加灵活。
它不依赖于类继承,它最大化了代码重用。
像myt2 的例子一样,通过实现Runnable接口,我们可以在不同的线程中重用同一个action对象,这是继承Thread类无法实现的。
不过,有一个细节很关键,那就是创建Thread对象时,需要传递一个实现Runnable接口的实例。

还有一件事,很多人忽略了可调用接口。
它允许返回一个值,在需要返回结果的情况下很有用。
例如,在 myThread_3 示例中,我们通过实现 Callable 接口并使用 FutureTask 包装器来实现线程。
这种方式比较灵活,但相对来说也比较复杂,尤其是涉及到多线程同步和异常处理时。

最后,我认为这三种方法的结合值得尝试。
例如,您可以使用 Runnable 接口来处理最基本的功能,而对于需要返回结果的高级功能,则使用 Callable 接口。
这样不仅代码简单,而且可以满足不同情况的需求。
很多人没有注意到这一点,但我认为这很令人困惑,因为并不是所有情况都能正确应用一种方法。

多线程有哪些实现方式?

说白了,Java多线程有两种方法:要么直接继承Thread,要么实现Runnable。
继承Thread简单粗暴,但耦合度较高; 实现Runnable很灵活,但是编写起来多了一步。
这两种都是“纯工作类型”,没有返回值,适合运行批处理任务。

我们先来说说最重要的事情。
去年我们跑电商闪购项目,使用继承Thread的方案,崩溃了3 次——线程死掉后,连日志都读不出来。
后来改用Runnable,通过Thread.start()启动,终于能够捕获异常了。
还有一点,当并发级别在3 000左右时,我发现从Thread继承的内存消耗得很快,因为每个线程都要加载类定义,可以用Runnable分开。
还有另一个关键细节。
比如Thread类的stop方法就被废弃了,用Runnable来实现就安全多了。

一开始以为Callable比Runnable高级,后来发现不对——FutureTask可以取消线程并获取执行结果,非常好用。
比如我们在处理OCR任务时,可以使用Callable+FutureTask来实时判断是否超时。
等等,还有一件事。
当看到线程池使用了Executors.newFixedThreadPool,其实底层还是使用了ThreadPoolExecutor,但是使用奇怪参数的工厂方法时要小心,比如newSingleThreadExecutor,因为CPU可能会满。

建议大家使用场景选择:CPU密集型任务使用Runnable,需要带返回值或者可以取消的任务使用Callable。
但说实话,调整线程池参数的助手是相当棘手的。