欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

并发基础之实现多线线程的正确姿势

程序员文章站 2022-05-07 21:46:38
实现多线程的方式到底是几种? 针对于JAVA多线程的实现方式,不论是网上还是各种书籍都有同的观点,有的说的两种,有的说有四种,随便百度一下我们就能看到很多种答案: 那究竟是几种呢?下面我们来查找一下JAVA的API文档:https://docs.oracle.com/javase/9/docs/ap ......

实现多线程的方式到底是几种?

针对于java多线程的实现方式,不论是网上还是各种书籍都有同的观点,有的说的两种,有的说有四种,随便百度一下我们就能看到很多种答案:

并发基础之实现多线线程的正确姿势

那究竟是几种呢?下面我们来查找一下java的api文档:https://docs.oracle.com/javase/9/docs/api/java/lang/thread.html

从文档中我们可以清晰的看到,实现多线程有两种方式,一种是将一个类声明为的子类thread,还有一种是声明一个实现runnable接口的类。

并发基础之实现多线线程的正确姿势

两种实现方式对比

实现runnable接口:

/**
 * @author chen
 * @description 使用runnable接口实现多线程
 * @create 2019-11-04 21:38
 */
public class runnablestyle implements runnable{
    public static void main(string[] args) {
        thread thread = new thread(new runnablestyle());
        thread.start();
    }

    @override
    public void run() {
        system.out.println("使用runnable接口实现多线程");
    }
}

继承thread类:

/**
 * @author chen
 * @description 使用thread方式实现多线程
 * @create 2019-11-04 21:41
 */
public class threadstyle extends thread{
    public static void main(string[] args) {
        thread thread = new threadstyle();
        thread.start();
    }

    @override
    public void run() {
        system.out.println("使用thread类实现多线程");
    }
}

使用runable还是thread?

既然都可以实现多线程,那实际我们应该使用选择一种方式创建还是随便都可以呢?

答案是实现runable方式更好。

原因主要有以下几点:

1.从解耦的角度来说,多线程执行的任务(也就是run方法的内容)应该与thread类是解耦的,

而不是都写在一起。

2.如果使用继承thread类来实现多线程,我们每次想新创建一个任务只能新建一个独立的线程,而这样的损耗是比较大的,需要去创建,销毁等。而使用runnable接口的话后续我们可以使用线程池,这样可以减少新建线程带来的损耗。

3.因为java是单继承,一旦继承了thread就不能在去继承其他的类,限制了程序的可扩展性。

runable和thread创建线程区别?

使用runable的方式创建,我们是使用了一个thread类一个带参的构造方法,使用thread方法创建,是使用了thread类的一个无参的构造方法,这两种方法都重写了run方法,接下来我们来看一看thread类中的run方法是如何实现的:

    /* what will be run. */
    private runnable target;

    /**
     * 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();
        }
    }

以上是一部分thread类的源码,我们可以看到执行run方法的时候,会先判断是否传入runable接口的实现类,如果传入了,就执行接口实现类中的run方法,如果没传入,则会直接执行子类重写的run方法,所以本质上来说,这两种方法本质上都是执行了run方法,只不过run方法的来源不同。

错误观点

线程池创建线程也算是一种新建线程的方式

/**
 * @author chen
 * @description 使用线程池创建线程
 * @create 2019-11-05 9:40
 */
public class threadpoolstyle {
    public static void main(string[] args) {
        executorservice executorservice = executors.newcachedthreadpool();
        for (int i = 0; i <1000 ; i++) {
            executorservice.submit(new task());
        }
    }

    static class  task implements runnable{
        @override
        public void run() {
            try {
                thread.sleep(500);
            } catch (interruptedexception e) {
                e.printstacktrace();
            }
            system.out.println(thread.currentthread().getname());
        }
    }
}

下面简单看一下excutors内部是如何创建线程的:

可以看到在下面的代码中线程池的内部新建线程也是通过传入一个runnable,然后在new thread来创建的,所以虽然外表看起来创建的方式不太一样,但是原理都是通过runnable接口来实现的多线程。

    /**
     * the default thread factory
     */
    static class defaultthreadfactory implements threadfactory {
        private static final atomicinteger poolnumber = new atomicinteger(1);
        private final threadgroup group;
        private final atomicinteger threadnumber = new atomicinteger(1);
        private final string nameprefix;

        defaultthreadfactory() {
            securitymanager s = system.getsecuritymanager();
            group = (s != null) ? s.getthreadgroup() :
                                  thread.currentthread().getthreadgroup();
            nameprefix = "pool-" +
                          poolnumber.getandincrement() +
                         "-thread-";
        }

        public thread newthread(runnable r) {
            thread t = new thread(group, r,
                                  nameprefix + threadnumber.getandincrement(),
                                  0);
            if (t.isdaemon())
                t.setdaemon(false);
            if (t.getpriority() != thread.norm_priority)
                t.setpriority(thread.norm_priority);
            return t;
        }
    }

通过callable和futuretask创建线程也算是一种新建线程的方式

通过callablefuture

/**
 * @author chen
 * @description 使用callable、future以及futuretask
 * @create 2019-11-05 10:03
 */
public class futuretaskstyle {
    public static void main(string[] args) throws executionexception, interruptedexception {
        executorservice executorservice = executors.newcachedthreadpool();
        future<string> future = executorservice.submit(new callable<string>() {
            @override
            public string call() throws exception {
                thread.sleep(500);
                return "future result";
            }
        });
        system.out.println(system.currenttimemillis());
        system.out.println(future.get());
        system.out.println(system.currenttimemillis());
    }
}

通过callablefuturetask

/**
 * @author chen
 * @description 使用callable、future以及futuretask
 * @create 2019-11-05 10:03
 */
public class futuretaskstyle {
    public static void main(string[] args) throws executionexception, interruptedexception {
        executorservice executorservice = executors.newcachedthreadpool();
        futuretask<string> futuretask = new futuretask<string>(new callable<string>() {
            @override
            public string call() throws exception {
                thread.sleep(500);
                return "future result";
            }
        });
        new thread(futuretask).start();
        system.out.println(system.currenttimemillis());
        system.out.println(futuretask.get());
        system.out.println(system.currenttimemillis());
    }
}

使用ctrl+alt+u查看futuretask的继承关系图:可以看到runablefuture继承了futurerunnable接口,而futuretask又实现了runablefuture,所以futuretask也间接的实现了runable接口。所以说此种方式和使用runnable实现的方式原理是相同的。

并发基础之实现多线线程的正确姿势

通过定时器

/**
 * @author chen
 * @description 使用定时器创建新的线程
 * @create 2019-11-05 10:26
 */
public class timmerstyle {
    public static void main(string[] args) {
        timer timer = new timer();
        timer.scheduleatfixedrate(new timertask() {
            @override
            public void run() {
                system.out.println(thread.currentthread().getname());
            }
        },1000,1000);

    }
}

看看timertask也是实现了runnable接口,本质上也是使用runnable接口方式创建。

通过匿名内部类或lambda表达式

/**
 * @author chen
 * @description 使用匿名内部类 和lamada表达式实现多线程
 * @create 2019-11-05 10:36
 */
public class anonymousinnerclassstyle {
    public static void main(string[] args) {
        //匿名内部类
        new thread(new runnable() {
            @override
            public void run() {
                system.out.println(thread.currentthread().getname());
            }
        }).start();

        //lambda 表达式
        new thread(()-> system.out.println(thread.currentthread().getname())).start();

    }
}

这种形式只是语法改变了一下,其实匿名内部类和lambda 表达式实现的效果都是一样的,只是更换了一种写法。

总结

实现多线程到底有几种方法?从不同角度看,会有不同的答案,如果从实现多线程的本质上看,一般有两种,一种是实现runnable接口,一种是继承thread类;而从代码实现层面看,会有很多种,如线程池,futuretask匿名内部类和lambda表达式等。

其实看原理,本质上的两种实现方式也是一样的,都是调用了thread类的run方法,而run方法中有一个判断,如果传入的runnable不为空,就执行run方法本身的方法内的代码。

一般情况下我们会使用runable接口的方式新建线程,主要出于以下原因:

从程序耦合的角度来看,把要提交的任务代码和thread类中的代码分离开,实现低耦合;

从可拓展角度来看,因为java是单继承的,使用继承thead类的方式就无法在继承其他的基类,降低了程序的课拓展性;

从资源消耗的角度看,使用runnable的方式我们可以使用线程池来管理线程,可以减少新建线程销毁线程等带来的资源损耗。