美文网首页
Java中实现多线程的正确姿势(反驳网上的某些观点)

Java中实现多线程的正确姿势(反驳网上的某些观点)

作者: demoyuan | 来源:发表于2020-07-24 11:47 被阅读0次
“java中实现多线程到底有几种方式?为啥我去搜索答案说几种的都有。。。”最近朋友问我这样一个问题,当时我脑子里条件反射的答案是两种。但是他这么问,我也有点心虚,于是我也试着打开搜索引擎搜索了一下答案,发现说几种的都有:两种、三种、四种、六种。那到底应该是几种呢?:

于是我打开Oracle的java官方文档去寻找答案,官方的答案是两种。

  • 实现Runnable接口
public class ImplRunnableStyle implements Runnable {
    @Override
    public void run() {
        System.out.println("使用Runnable方式实现多线程");
    }

    public static void main(String[] args) {
        new Thread(new ImplRunnableStyle()).start();
    }
}
  • 继承Thread类
public class ExtendsThreadStyle extends Thread {
    @Override
    public void run() {
        System.out.println("使用继承Thread方式实现多线程");
    }

    public static void main(String[] args) {
        new ExtendsThreadStyle().start();
    }
}

在真正的编程环境中,推荐使用的方式还是实现Runnable接口的方式,原因这里有3点:

  1. 从架构角度来看,使用Runnable的方式可以将具体的任务(run方法)和创建、运行线程的机制(Thread类)解耦,代码结构更加优雅;
  2. 如果使用Thread继承的方式,那么每次想创建一个新的任务都必须创建一个新的独立线程,这样就会造成额外的资源损耗,如果使用Runnable和线程池就可以避免这样无谓的损耗;
  3. 由于Java语言不支持多继承,如果使用继承Thread的方式,就无法再继承其他的类,限制了可扩展性。

聊到这里我们是不是可以结束这个问题了?NO,NO,用电视剧里面比较拉仇恨的话讲,“就这?”
我们再来看一道程序执行逻辑的思考题:

public class BothImplRunnableAndExtendsThread {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行实现Runnable接口方式的代码块");
            }
        }) {
            @Override
            public void run() {
                System.out.println("执行继承Thread方式的代码块");
            }
        }.start();
    }
}

假如我们这两种方式一起使用,会执行哪个代码块?
咋一看这个题是不是感觉有点懵逼,正常开发的时候谁会这么写代码。。。但是我想说,如果你能不运行程序就知道这道题的答案,并且可以给出原因,那说明你对上面讲的两种实现线程的方式才有了真正的理解。
我先给出程序的执行结果:控制台会输出“执行继承Thread方式的代码块”,原因呢?我们去看一下Thread类run方法的源码就了解了:

/**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     *
     * @see     #start()
     * @see     #stop()
     * @see     #Thread(ThreadGroup, Runnable, String)
     */
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
代码很简单,如果target!=null就执行target.run()方法。那target又是什么?

看了Thread类的源码应该很明白了吧,target就是我们传过去的实现Runnable接口的对象,那刚才那道思考题的答案为什么是“执行继承Thread方式的代码块”,小伙伴们也应该很清楚了。因为当使用继承Thread方式的时候,我们重写了run方法,那么Thread类中run方法的代码逻辑肯定就执行不了了。所以只会执行我们重写的run方法代码中的逻辑。

下面我们再来聊一下网上某些博客中的“观点”:

  • “线程池创建线程也算是一种新建线程的方法”
public class ThreadPool {

    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        cachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程池的方式创建线程");
            }
        });
    }
}
上面是通过线程池创建线程的简单代码,国际惯例我们直接看线程池的源码是怎样创建线程的: 上面是ThreadPoolExecutor类的execute方法源码,直接看创建线程的关键代码addWorker(command, true): 应该已经很明显了吧,创建新线程肯定是Worker类中成员变量thread和firstTask一起完成的,这里thread是通过线程池的线程工厂去创建的,我们继续跟进源码: 眼熟不眼熟,是不是上面提到的通过实现Runnable方式创建线程,所以所谓的通过线程池方式创建线程只是线程池的相关类包装了一下而已,我们应该透过现象看本质。

网上还有很多其他的说法例如:“通过Callable和FutureTask创建线程”、“定时器创建线程”等等其实都是一种包装,用赵本山的话讲“你以为穿上马甲我就不认识你了?”

最后我们总结下:
我们通过新建Thread类的方式创建一个独立线程,但是类里面的run方法有两种方式来实现:第一种是重写Thread类的run方法;第二种是实现Runnable接口的run方法,然后再把该Runnable接口的实例传给Thread类。除此以外,所谓的线程池、定时器等工具类也可以创建线程,但是它们的本质都逃不过Java官方文档提到的两种方式。

相关文章

网友评论

      本文标题:Java中实现多线程的正确姿势(反驳网上的某些观点)

      本文链接:https://www.haomeiwen.com/subject/glyhlktx.html