Java并发基础学习(一)——实现并正确启动一个线程

前言

按照今年的学习计划,并发学习是一个关键内容,之前参考过很多讲Java并发的书籍,感觉国内的相关数据,讲的好的好像并不多,学了一些后来觉得甚是枯燥,没有相关实例就放弃了,但是没有针对这些进行总结,这次打算从头归零,从头开始学习,并做好总结。之前的并发总结文档过于混乱,已将其置为私密。这次的学习几乎参考了所有Java并发编程的书籍

实现多线程的方法

如果说对Java多线程稍微有一点了解的大佬,针对Java中实现多线程的方式有几种这个问题很熟悉,之前网上也讨论过很多,到底有几种实现多线程的方式,有的说1种,有的说2种,有的说4种。百度一下也答案各异

Java并发基础学习(一)——实现并正确启动一个线程

到底有几种实现多线程的方式,这个还是需要好好说道说道

正确答案——2种,这个是基于Oracle官网的结果,既然官网给出的答案是两种,我们就不要再被各种3种,4种,6种这种花里胡哨的结果给误导了,给出一张官网的截图。

Java并发基础学习(一)——实现并正确启动一个线程

有两种方法可以创建新的执行线程,一种是继承Thread类,另一种是实现Runnable接口,这是官网给的答案。

实现Runnable接口

/**
 * autor:liman
 * createtime:2021/9/7
 * comment:runnable创建线程
 */
@Slf4j
public class RunnableStyle implements Runnable {

    public static void main(String[] args) {
        //将当前实现了Runnable接口的类交给Thread
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }

    @Override
    public void run() {
        log.info("用runnable的方式实现线程");
    }
}

继承Thread类

/**
 * autor:liman
 * createtime:2021/9/7
 * comment:继承thread创建线程
 */
@Slf4j
public class ThreadStyle extends Thread{

    public static void main(String[] args) {
        //使用的是Thread不带任何参数的构造函数
        Thread thread = new ThreadStyle();
        thread.start();
    }
    @Override
    public void run() {
        log.info("继承Thread类的方式实现线程");
    }
}

两种方式的对比

一般推荐使用Runnable接口的方式创建线程。主要原因有以下几点:

1、从架构的角度来考虑,Runnable中的逻辑只是具体的实现,而Thread类中其实还做了线程的启动,暂停等操作。具体的实现应该需要独立出来,所以实现Runnable接口从某种层度上来说是解耦了。

2、继承Thread类,每次如果想新建一个任务,其实就是重新新建一个线程,即使很多线程执行的逻辑一样,也都是重新建立一个线程,这样CPU的性能损耗是很大的,而实现Runnable接口则不同,实现了Runnable接口可以利用线程池等工具,一定程度上降低这种损耗。

3、Java不支持多继承,如果一个类继承了Thread类,则无法再继承其他类,这在一定程度上限制了其可扩展性。

综合上述原因考虑,推荐采用实现Runnable接口。

关于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();
    }
}

其实run方法中底层调用的是target的run,而这个target其实就是一个Runnable接口类型的。会在构造函数的时候被传入,当我们实现Runnable接口启动线程的时候,实质是传入了一个Runnable接口的引用。采用实现Runnable接口的方式,Thread最终会执行target.run()方法,而我们集成Thread类,则是覆盖了Thread类中的整个run方法。

可以通过如下实例说明这一问题

/**
 * autor:liman
 * createtime:2021/9/7
 * comment:同时使用两种方式实现线程
 */
@Slf4j
public class BothRunnableThread  {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是Runnable方式创建的线程");
            }
        }){
            //由于本身的run方法被重写过,因此通过runnable对象创建的时候,父类Thread中的run方法已经被重写了,这里是重写run方法
            @Override
            public void run() {
                System.out.println("这是继承Thread创建的线程");//输出这一行
            }
        }.start();
    }
}

重写了run方法之后,并不会执行Runnable接口中的逻辑。

到这里,如果后续遇到Java中正常有几种这种问题,我们可以如下回答:

Java中创建线程的方式通常我们可以分为两类,Oracle官网也是这么介绍的。准确的讲,创建线程只有一种方式,就是构造Thread类,而实现线程的执行单元有两种方式:

1、实现Runnable接口的run方法,并把Runnable实例传递给Thread类。

2、重写Thread的run方法(继承Thread类)

至于网上说的其他实现线程的方式,都只不过是在这些方法的基础上,代码的写法不同,或者通过各种各样类的包装而成的,本质与上面两者并没有什么不同。

启动线程的方法

启动线程其实比较简单并不复杂,正确的就是调用start方法,但是还是深入到原理进行一个小小的总结。

正确启动线程的方法

正确的方式,当然是调用Thread中的start方法,即可启动线程,关于start的源码,内容如下:

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the <code>run</code> method of this thread.
 * <p>
 * The result is that two threads are running concurrently: the
 * current thread (which returns from the call to the
 * <code>start</code> method) and the other thread (which executes its
 * <code>run</code> method).
 * <p>
 * It is never legal to start a thread more than once.
 * In particular, a thread may not be restarted once it has completed
 * execution.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *               started.
 * @see        #run()
 * @see        #stop()
 */
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

可以看到,其最先判断的,就是线程状态,之后将其加入到一个线程组中,然后调用一个native的start0方法,只有调用这个方法,才会让线程经历所有的生命状态周期,如果直接调用run方法,线程是不会经历所有的线程生命周期的。

错误启动线程的方法

1、不能直接调用Runnable的run方法,如果直接调用run方法,则会当成一个普通方法进行执行

/**
 * autor:liman
 * createtime:2021/9/8
 * comment:对比start和run两种启动线程的方式
 */
@Slf4j
public class StartAndRunMethod {

    public static void main(String[] args) {
        //初始化一个Runnable
        Runnable runnable = ()->{
            log.info("当前线程的线程名为:{}",Thread.currentThread().getName());
        };
        //直接调用run方法,会当成一个普通方式执行
        runnable.run();

        //单独启动一个线程
        new Thread(runnable).start();
    }
}

运行结果:

Java并发基础学习(一)——实现并正确启动一个线程

可以看到,同一行代码,直接调用run和调用start输出的并不一样

2、start方法不能重复调用,因为线程状态已经改变

/**
 * autor:liman
 * createtime:2021/10/7
 * comment:启动两次的线程
 */
@Slf4j
public class StartTwiceDemo {

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

线程状态已经改变,重复调用start会抛错

Java并发基础学习(一)——实现并正确启动一个线程

总结

线程学习的开篇,只是总结到线程正常的启动方法。

上一篇:想要学会多线程超简单! 每天5分钟达成!(线程开启的其他方式)


下一篇:Runnable和Thread的区别