【多线程系列】线程的创建与启动详解

一、创建线程

下面两种方式相信一定非常的熟悉,这是我们常用的方式,下面就以它开头,对线程的创建和启动做一个详细的分析。也是希望通过分析这个过程来理解多线程。

1、继承Thread的方式创建线程

public class ThreadTest extends Thread {
    public void run(){
        System.out.println("重写run方法");
    }
    public static void main(String[] args) {
        //构造Thread子类对象并启动
        new ThreadTest().start();
    }
}

2、实现Runnable接口的方式创建线程

public class ThreadRunnableTest implements Runnable{
    @Override
    public void run() {
        System.out.println("do something here!");
    }

    public static void main(String[] args) {
        //创建一个类对象
        ThreadRunnableTest runnableTest = new ThreadRunnableTest();
        //创建一个线程对象
        Thread thread = new Thread(runnableTest);
        //启动线程
        thread.start();
    }
}

二、Runnable接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

用@FunctionalInterface注解标注,表示可以用Lambda表达式来创建接口实例,该接口只有一个抽象的run方法。

三、构造方法

1、创建一个Thread类实例,并调用他的start方法,要创建一个Thread类的实例自然要通过构造函数,因此下面来看下具体这些构造函数

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(ThreadGroup group, Runnable target) {
        init(group, target, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(String name) {
        init(null, null, name, 0);
    }

    public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }

    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }

    public Thread(ThreadGroup group, Runnable target, String name) {
        init(group, target, name, 0);
    }

    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }

2、通过上面的构造函数不难发现,每个构造函数都有一个init方法,而且这个init方法里面有四个参数,他们分别是:

  • ThreadGroup g(线程组)
  • Runnable target (Runnable 对象)
  • String name (线程的名字)
  • long stackSize (为线程分配的栈的大小,若为0则表示忽略这个参数)

3、而这个init方法又调用了另一个init方法,设置关于线程组和访问控制上下文的

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name.toCharArray();

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */
            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

初始化一个线程。

参数:

g – 线程组

target – 调用 run() 方法的对象

name - 新线程的名称

stackSize – 新线程所需的堆栈大小,或为零表示将忽略此参数。

acc – 要继承的 AccessControlContext,如果为 null,则为 AccessController.getContext()

推断注释:

参数g : @org.jetbrains.annotations.Nullable

参数name : @org.jetbrains.annotations.NotNull

参数acc : @org.jetbrains.annotations.Nullable

4、综合上面来看,其他不做详细描述,下面两个参数是我们常用到的

  • target – 调用 run() 方法的对象
  • name - 新线程的名称

5、线程的名字其默认值为"Thread-" + nextThreadNum()

  • nextThreadNum()就是一个简单的递增计数器,所以如果创建线程时没有指定线程名,那线程名就会是:Thread-0, Thread-1, Thread-2, Thread-3, ...
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

6、重要参数:

Runnable target (Runnable 对象):通过这里可以发现创建一个线程实例最重要的是传入一个Runnable类型的对象,既然是Runnable类型的,那他一定是实现了Runnable接口,也就是说该对象一定重写了run方法,Thread类本身也实现了Runnable接口,所以它必然也覆写了run方法,我们先来看看它的run方法:

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

这个run方法仅仅是调用了target对象的run方法,如果我们在线程构造时没有传入target(例如调用了无参构造函数),那么这个run方法就什么也不会做。

四、启动线程

当我们的线程创建完成后,下面就是启动线程,在Java中启动一个线程必须调用start方法,通过下面的代码可以看出,这个方法本质是调用了native的start0方法,这个方法使得线程开始执行,并由JVM来执行这个线程的run方法,结果就是有两个线程在并发执行,一个是当前线程,也就是调用了Thread#start方法的线程,另一个线程就是当前thread对象代表的线程,它执行了run方法。也就是说,这个Thread类实例代表的线程最终会执行它的run方法,而上面的分析中我们知道,它的run做的事就是调用Runnable对象的run方法,如果Runnable对象为null, 就啥也不做。

 public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {

            }
        }
    }

    private native void start0();

我们知道,Thread类从定义上看就是个普通的java类,是什么魔法让它从一个普通的java类晋升为一个可以代表线程的类呢?是native方法!

如果我们直接调用target对象的run方法,或者Thread类的run方法,那就是一个普通调用,因为run方法就是普普通通的类方法,与我们平时调用的其他类方法没有什么不同,这并不会产生多线程。

但是,如果我们调用了start方法,由于它内部使用了native方法来启动线程,它将导致一个新的线程被创建出来, 而我们的Thread实例, 就代表了这个新创建出来的线程, 并且由这个新创建出来的线程来执行Thread实例的run方法。

实例一:

public class CustomizedThread extends Thread {
    public static void main(String[] args) {
        System.out.println("{"+Thread.currentThread().getName()+"线程}:"+"我是定义main方法里的...");
        CustomizedThread thread = new CustomizedThread();
        thread.run();
    }
    public void run(){
        System.out.println("{"+Thread.currentThread().getName()+"线程}:"+"我是定义在CustomizedThread类中的run方法。");
    }
}

执行结果:

{main线程}:我是定义main方法里的...
{main线程}:我是定义在CustomizedThread类中的run方法。

Process finished with exit code 0

通过上面的执行结果可以看出,这个只有一个main线程,虽然执行了我们自己定义的run方法,但是他并没有产生新的线程,这个时候这里的run方法就是一个普通的方法。

public class CustomizedThread extends Thread {
    public static void main(String[] args) {
        System.out.println("{"+Thread.currentThread().getName()+"线程}:"+"我是定义main方法里的...");
        CustomizedThread thread = new CustomizedThread();
        thread.start();
    }

    public void run(){
        System.out.println("{"+Thread.currentThread().getName()+"线程}:"+"我是定义在CustomizedThread类中的run方法。");
    }
}

执行结果:

{main线程}:我是定义main方法里的...
{Thread-0线程}:我是定义在CustomizedThread类中的run方法。

Process finished with exit code 0

通过上面的执行结果可以看出这里非常明显就是两个线程,一个是main的线程,他执行了main方法,一个是Thread-0线程,他是我们自定义的线程,他执行了run方法。这里我们调用的是start方法,他的底层调用了native来启动线程。

疑问

不是说:创建一个线程最重要的是要传入一个Runnable对象吗?上面的实例为什么没有传入Runnable对象呢?

public class CustomizedThread extends Thread {
    ...
}

很明显,这里我的CustomizedThread类继承了Thread类,由于上面的实例并没有传入参数,他就默认调用了父类Thread的无参构造方法,通过下面的代码可以看出来这里的target是null,然后,我们使用了myThread.start(),因为我们在子类中没有定义start方法,所以,这个方法来自父类,而Thread类的start方法的作用我们已经讲过,它将新建一个线程,并调用它的run方法,这个新建的线程的抽象代表就是我们的CustomizedThread,所以它的(CustomizedThread的)run方法将会被调用。如果这里我们没有写run方法,那么当然就是调用父类的run方法了。而Thread类的run方法调用的又是target对象的run方法,而target对象现在为null, 所以这个方法啥也不做。

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

父类的run方法:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

小结:

创建一个线程最重要的是定义一个run方法,这个run方法要么通过继承Thread类的子类覆写,要么通过直接构造Thread类时传入一个Runnable的target对象。无论它由子类覆写提供还是由target对象提供,start方法最终都会新建一个线程来执行这个run方法。

实例二:

public class CustomizedThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("{" + Thread.currentThread().getName() + "线程}: " + "我是传递给Thread类的Runnable对象的run方法");
    }

    public static void main(String[] args) {
        System.out.println("{" + Thread.currentThread().getName() + "线程}: " + "我是main方法");
        Thread thread = new Thread(new CustomizedThread2());
        thread.start();
    }
}

执行结果:

{main线程}: 我是main方法
{Thread-0线程}: 我是传递给Thread类的Runnable对象的run方法

Process finished with exit code 0

上面的实例,通过新建Thread类的对象来创建线程,他的本质是传递一个Runnable对象给Thread的构造函数,通过myThread.start()来启动这个线程,start方法会调用run方法,而thread类的run方法最终会调用target对象的run方法

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

总结

在Java中创建一个线程有且仅有一种方式:就是通过创建一个Thread类的实例,并调用它的start方法,当然更重要的是还要定义一个run方法,说明这个线程具体要做什么。继承Thread类,覆写run方法或者实现Runnale接口,将它作为target参数传递给Thread类构造函数,启动一个线程一定要调用该线程的start方法,否则,并不会创建出新的线程来。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术蜗牛-阿春

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值