【线程基础】【一】线程的创建方式

1  前言

本节开始我们来回顾下线程基础相关的东西,最近在复习所以来做一些笔记哈,这节我们来讲讲创建线程的方式。

线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可会释放原生线程和 Java 线程的所有资源。用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时,会释放原生线程和 Java 线程的所有资源。

2  创建分类

Java提供了两种线程的创建方法,第一种是继承Thread类;第二种是实现Runable接口,并将Runnable实例传递给Thread类。详细的可以参考官方文档哈:https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html

那么接下来我们就来看看这两种方式的优缺点哈,只有了解好坏我们才能在使用中悠然自得。

2.1  继承Thread类

优点 : 方便传参,可以在子类添加成员变量,通过方法设置参数或构造函数传参,也就是类就是一个线程类,对象创建出来start就完事了。

缺点:

  • 单继承的弊端,大家都懂的
  • 这种创建方式不便于线程池管理,像野孩子需要自己管理
  • 代码写法上可能相对于接口方式要麻烦
  • 无法获取线程运行结果
class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}
PrimeThread p = new PrimeThread(143);
p.start();

2.2  Runnable 接口

优点 : 此方式可以继承其他类。也可以使用线程池管理,节约资源。创建线程代码的耦合性较低。推荐使用此种方式创建线程。

缺点: 不方便传参,只能使用主线程中用final修饰的变量。其次是无法获取线程任务的返回结果。

// 第一种:常规写法
class PrimeRun implements Runnable {
    long minPrime;
    PrimeRun(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
// 第二种:lambda写法
new Thread(()->{}).start()

2.3  Callable 接口

此种方式创建线程底层源码也是使用实现Runnable接口的方式实现的,所以不是一种新的创建线程的方式,只是在实现Runnable接口方式创建线程的基础上,同时实现了Future接口,实现有返回值的创建线程

public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 封装一个Callable任务
        Callable<String> callTask = () -> {
            System.out.println("开始执行【Callable】任务");
            TimeUnit.SECONDS.sleep(2);
            // 执行结束后返回结果值
            return "我是结果";
        };
        // 创建异步任务
        FutureTask<String> task = new FutureTask<>(callTask);
        // 启动线程
        new Thread(task).start();
        // 等待结果
        String res = task.get();
        System.out.println(res);
    }
}

3  引申问题

3.1  如果同时使用Thread和Runnable两种方式创建线程并启动,会怎样?

public class CreateThread {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                System.out.println("通过Runnable接口创建线程");
            }
        }) {
            @Override
            public void run() {
                // 此方法会覆盖掉父类run方法,即父类run方法不再执行(java面向对象中的继承特性)
                System.out.println("通过Thread类创建线程");
            }
        }.start();
    }
}

分析:上述代码先通过Runnable方式创建一个线程,并实现接口中的run方法。然后在new Thread的方法体中再次重写Thread的run方法时,父类的run方法将不会再执行,即System.out.println("通过Runnable接口创建线程")此行代码不会被执行了,直接执行重写后的run方法System.out.println("通过Thread类创建线程")。原因是Java面向对象中继承后重写父类的方法时,该父类的方法将不再被执行,直接执行子类重写后的方法,所以结果只打印了“通过Thread类创建线程”此行输出。

3.2  线程的启动为什么不能直接调用run方法,而是调用start方法?

通过调用run()方法,是在主线程中执行任务,所以本质上并没有创建出新的线程,其实就是方法调用。
通过调用start()方法,是在主线程中创建一个子线程去执行任务,这才是创建新线程去执行。通过start()方法启动线程后,并不一定立即执行,而是由线程调度器决定何时运行,可能立刻就会运行,也可能稍后才会运行,也可能一直不运行(饥饿状态)。
两种方式都能成功运行并执行,只是直接调用run()方法,并没有使用新线程去运行任务,程序还是串行执行的,所以这种方式是不符合预期的。

3.3  如果线程连续调用两次start()方法,会怎样?

public class ThreadClass extends Thread {

    @Override
    public void run() {
        System.out.println("运行Thread线程");
    }

    public static void main(String[] args) {
        ThreadClass threadClass = new ThreadClass();
        threadClass.start();
        threadClass.start();

    }
}

原因分析:
查看start()方法源码:

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 */
        }
    }
}

通过源码中发现,之所以抛出java.lang.IllegalThreadStateException,是因为threadStatus不为0;threadStatus为0,表示线程刚初始化完成,还没有启动。若threadStatus不为0,说明线程已经被启动过了。所以第二次调用start()方法时,线程的状态threadStatus已改变,此时会抛出异常。threadStatus是voliate修饰的保证可见性顺序性,遵循happens-before原则,所以第二次启动发现状态不对直接抛异常。

4  小结

本节我们看了下创建线程的两种方式,即继承Thread类和实现Runnable接口。其他所有创建线程的方式,底层都是使用这两种方式中的一种实现的,比如通过线程池、通过匿名类、通过lambda表达式、通过Callable<V>接口等等,全是通过这两种方式中的一种实现的。有理解不对的地方欢迎指正哈。

posted @ 2023-04-18 20:37  酷酷-  阅读(53)  评论(0编辑  收藏  举报