创建线程的4种方式

创建一个空线程

public class EmptyThreadDemo {
    public static void main(String[] args) {
        //使用Thread类创建和启动线程
        Thread thread = new Thread();
        Print.tco("线程名称:"+thread.getName());
        Print.tco("线程Id:"+thread.getId());
        Print.tco("线程状态:"+thread.getState());
        Print.tco("线程优先级"+thread.getPriority());
        Print.tco(Thread.currentThread().getName()+"运行结束");
        thread.start();
    }
}

首先创建一个空线程,通过该线程在堆内存的引用地址获取到该线程的名称,ID,状态,优先级。
此时线程并没有启动,其线程状态是New。然后用thread.start()启动该线程,线程会去执行用户代码逻辑块,逻辑块的入口是run()方法,我们可以看看run方法的源码:

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

target是Thread类中的一个实例属性,它是这样定义的。

private Runnable target;

它是一个Runnable类型的属性,Runnable是一个接口类,里面有定义一个方法便是run(),这也意味着在新线程启动后,会以run()方法为代码逻辑块入口执行用户代码,而内部进一步调用了target目标实例执行类的run()方法,而我们并没有去实现这个方法,所以什么都没有执行,该线程也称空线程结束了,整个JVM进程也结束。

工具类

这些工具类的方法后续会用上,便于编码。

public class ThreadUtil{
    
    public static String getCurThreadName(){
        return Thread.currentThread().getName();
    }
    
    public static void sleepMillSeconds(int millsecond){
        LockSupport.parkNanos(millsecond*100L*100L);
    }

    public static void execute(String cfo){
        synchronized(System.out){
            System.out.println(cfo);
        }
    }
}

public class Print{
    public static void tco(Object s){
        String cfo = "["+ThreadUtil.getCurThreadName()+"]"+s;
        ThreadUtil.execute(cfo);
    }
}

通过继承Thread类的方式创建线程目标类

前面的例子向我们说明了线程start之后,如何执行用户定义的线程代码逻辑。因此我们想要线程去执行我们的代码就主要有两种方式:

  • 继承Thread类去重写run()方法。
  • 实现Runnable接口的run()方法,并将实现好的接口的实现类以构造参数的形式传入Thread的target实例属性中。

接下来我们来以代码诠释第一种方式

public class CreateDemo{
    private static final int MAX = 5;
    private static int treadNo = 1;
    static class DemoThread extends Thread{
          public DemoThread(){
              //调用父类的构造方法
              super("DemoThread-"+treadNo++);
        }
          @Override
          public void run(){
              for(int i = 0;i < MAX;i++){
                  Print.tco(getName()+", 轮次为:"+i);
            }
              Print.tco(getName()+" 运行结束.");
        }
    
    public static void main(String[] args){

          Thread thread = null;
          for(int i = 0;i < 2;i++){
              thread = new DemoThread();
              thread.start();
            }
          Print.tco(getCurThreadName()+" 运行结束.");
        }
    
    }
}

例子中,我们建了一个静态内部类去继承Thread类,调用其带String的构造方法构造该实现类,重写Thread类的run()方法,添加属于我们的逻辑代码块。

这里的代码逻辑是循环5次,每次输出当前运行线程的名字以及轮次。

至于为什么是静态内部类,主要是为了方便调用外部类的属性,而如果该内部类不是静态的话还需要new外部类才new当前内部类。当然将其写为外部类,依然不影响后面的输出结果。

通过实现Runnable接口创建target执行目标类来创建线程目标类

在我们用代码演示之前,我们可以来看一下Thread的构造方法有哪些?

图中我们可以看到,Thread给我们提供了样式丰富的构造方法,其中有Runnable的也居多。因此我们可以以Runnable为构造参数的形式给Thread实例类传入target实例属性。

构造参数String类型实则为所创建线程的名称。

接着我们来用代码真正实现

public class CreateDemo2 {

    public static final int MAX = 5;
    static int threadNo = 1;
    static class RunTarget implements Runnable{
        @Override
        public void run() {
            String name = getCurThreadName();
            for (int i = 0; i < MAX; i++) {
                Print.tco(name+",轮次:"+i);
            }
            Print.tco(name+" 运行结束.");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            Thread thread = new Thread(new RunTarget(),"RunnableThread-"+threadNo++);
            thread.start();
        }
        Print.tco(getCurThreadName()+",运行完毕.");
    }

}

这里我们可以看到我们实现了Rnnable接口的run()方法,将这个target目标执行类以构造参数的形式传入了我们所创建Thread实例类,当start()的时候,JVM就会启动线程运行用户逻辑代码,也就是我们实现Runnable接口run()方法的逻辑代码。

通过匿名类来创建Runnable线程目标类

通过优雅的实现方式来创建Runnable线程目标类

public class CreateDemo2_2 {
    public static final int MAX = 5;
    static int threadNo = 1;

    public static void main(String[] args) {
        Thread thread = null;
        for (int i = 0; i < 2; i++) {
            thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < MAX; j++) {
                        Print.tco(getCurThreadName()+",轮次:"+j);
                    }
                    Print.tco(getCurThreadName()+" 运行结束.");
                }
            },"RunnableThread-"+threadNo++);
            thread.start();
        }
        Print.tco(getCurThreadName()+" 运行结束.");
    }
}

通过Lambda表达式创建Runnable线程目标类

通过优雅的实现方式来创建Runnable线程目标类

public class CreateDemo2_3{
  
    private static final int MAX = 5;
    private static int treadNo = 1;
    
    public static void main(String[] args){
         
        Thread tread = null;
        for(int i = 0;i < 2;i++){
             tread = new Thread(()->{
                  for(int j = 0;j < MAX;j++){
                      Print.tco(getCurThreadName()+",轮次为:"+j);
                }
                  Print.tco(getCurThreadName()+" 运行结束.");
            },"RunnableThread-"+treadNo++);
            
            thread.start();
        }
        
        Print.tco(getCurThreadName()+" 运行结束.");
    }    

}


继承Thread类来创建线程目标类和通过实现Runnable接口创建线程目标类有什么不同吗?

  • 第一种方式创建线程目标类,由于每次创建类的内存地址都是不一样的,因此每个数据资源的内存地址都是不一样的,所以每个线程目标类都有其唯一的数据资源,在执行线程时,只是对着自己的数据资源进行业务处理,不会影响其他线程的数据资源。
    -- 第一种方式创建线程目标类的优点:由于是继承了Thread类,其子类便享有父类的getName()、getID()、getStatus()等方法,可以很轻松的访问当前线程的各种信息状态和对当前线程进行操作。
    -- 第一种方式的缺点: 由于一个类仅仅只能继承一个父类(不包括接口),所以在当前类继承了其他父类时,便用不了以继承Thread的方式来创建线程目标类了。
  • 第二种方式以实现Runnable接口的方式得到target目标类,在用这个target目标类以构造参数的形式传入Thread实例中,得以创建真正的线程。这里我们可以发现多个线程用的target目标执行实现类都是用的同一个引用地址,也即多个线程使用的数据资源都是同一个。也就是说使用实现Runnable接口来创建线程目标类,其多个线程业务逻辑并行处理同一个数据资源。
    -- 第二种方式创建线程目标类的优点:更好地体现了面向对象的设计思想。通过实现Runnable接口的方式设计多个target执行目标执行类可以更加方便,清晰地执行逻辑和数据存储的分离。
    -- 第二种方式创建线程目标类的缺点:由于数据资源是被多个线程共享的,所以对数据资源做共享操作的时候会出现线程安全的问题。而且由于target目标类不是继承Thread的,所以要得到当前线程的信息,只能以Thread.currentThread()来获取当前在cpu时间片运行的线程来获取信息。

通过创建FutureTask和实现Callable接口来创建线程目标类

前面的两种方式其实都有一个共同的缺陷:由于run()方法的返回值类型是void类型,我们在线程异步执行完成之后是拿不到线程执行完成后的结果,很多时候我们想要了解线程异步执行的时候的状态,结果,前面的两种方式并不足以满足我们的需求。

于是为了解决这个问题,在JDK1.5版本提供了一种新的多线程创建方法:便是使用Callable接口和FutureTask相结合来创建线程目标类。

首先我们先从Callable接口的源码定义来认识一下它

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

我们从源码认识到,Callable是一个函数式接口,其中有唯一方法为call()方法,其方法的返回值是Callable接口的泛型形参,方法还有一个Exception的异常声明,容许方法的实现可以有异常抛出,且不做捕获。

不难看出call()方法的功能比run()方法要丰富多了,它多了返回值,对了异常的声明,功能十分强大。但是其能代替Runnable实例作为Thread的target执行类吗?显然这是不能的,上文我们提到target实例的属性是Runnable,而其是Callable类型的,所以并不能作为target来运行。

那么我们要通过何种方式来让线程启动的时候,进入run()方法里面运行的是call()方法里的代码逻辑块呢?

接下来我们来认识一下RunnableFuture接口

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

我们可以看到RunnableFuture接口继承了Runnable接口,使其实现类可以作为target目标类,同时它还继承了Future接口,那么这个接口赋予了RunnableFuture什么接口方法呢?我们来查看一下Future接口。

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

通过查看其实现我们可以知道Future接口赋予了RunnableFuture五个接口方法,分别是:

  • cancel():取消异步任务的执行。
  • isCancelled():查看异步任务是否取消了。
  • isDone():查看异步任务是否执行完成。
  • get():阻塞性获取异步任务的执行的结果。
  • get(long timeout,TimeUnit unit):限时的阻塞性获取异步任务的执行的结果。

那么此时RunnableFuture接口就拥有了可以作为target实现类,可以获取线程的执行结果,执行状态的方法。那么最后就要实现该接口了,JDK已经帮我们实现好了,其名字叫做FutureTask

此时的FutureTask既能作为一个Runnable类型的target执行目标直接被Thread执行,有拥有着可以获取Callable执行结果,执行状态的能力。那么FutureTask是如何和Callable联系上的呢?我们可以查看FutureTask,其中有一个实例属性:
private Callable<V> callable;
其属性是用来保存并发执行的Callable类型的任务的,我们再来看看Future实现run方法的内部代码

    public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

    protected void set(V v) {
        if (STATE.compareAndSet(this, NEW, COMPLETING)) {
            outcome = v;
            STATE.setRelease(this, NORMAL); // final state
            finishCompletion();
        }
    }

此时我们恍然大悟,在run()方法中调用了Callable的call()方法,并将方法的返回值"set"起来了,那么它保存在哪呢?它是保存在属性outcome中,方便get()的获取。

最终我们可以捋一下Callable接口和FaskTask是怎么创建线程目标类的。

于是该线程的执行流程便是:

  • 首先线程start(),JVM会启动线程执行用户代码逻辑块,代码逻辑块的入口是run(),而run方法中调用了target执行类的run()方法,此时这个target便是我们已构造参数形式传入到Thread中的FutureTask,调用其run()方法,里面又调用了callable.call()方法,执行结果会保存在属性outcome中,静待调用线程调用。

我们用一个例子简单展现一下

public class CreateDemo3 {
    public static final int MAX_TURN = 5;
    public static final int COMPUTE_TIMES = 100000000;

    static class ReturnableTask implements Callable<Long>{
        @Override
        public Long call() throws Exception {
            long startTime = System.currentTimeMillis();
            Print.tco(getCurThreadName()+" 线程开始运行.");
            Thread.sleep(1000);
            for (int i = 0; i < COMPUTE_TIMES; i++) {
                int j = i * 10000;
            }
            long used = System.currentTimeMillis()-startTime;
            Print.tco(getCurThreadName()+" 线程运行结束.");
            return used;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReturnableTask task = new ReturnableTask();
        FutureTask<Long> futureTask = new FutureTask<Long>(task);
        Thread thread = new Thread(futureTask,"returnableThread");
        thread.start();
        Thread.sleep(500);
        System.out.println(getCurThreadName()+" 让子弹飞一会儿");
        System.out.println(getCurThreadName()+" 做一点自己的事情");
        for (int i = 0; i < COMPUTE_TIMES; i++) {
            int j = i * 10000;
        }
        System.out.println(ThreadUtil.getCurThreadName()+" 获取并发任务执行结果.");
        try {
            System.out.println(thread.getName()+" 线程占用时间:"+futureTask.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(ThreadUtil.getCurThreadName()+" 运行结束.");
    }
}

通过线程池来创建线程目标类

前面的许多例子所创建的Thread实例类都在执行完成之后销毁了,这些线程实例都是不可复用的。实际上线程的创建,销毁在时间成本上,资源成本(因为线程创建需要JVM分配栈内存等)上耗费都很高,在高并发的场景下,断然不能频繁的进行线程的创建和销毁,需要的是线程的可复用性。此时需要的是池技术,JAVA中提供了一个静态工厂来创建不同的线程池,该静态工厂为Executors工厂类。

接下来我们使用一个例子来实现线程池,以及线程的调度执行

/**
 * 第四种方式创建线程类:通过线程池创建线程
 */
public class CreateDemo4 {

    public static final int MAX = 5;

    //创建一个包含三个线程的线程池
    private static ExecutorService pool = Executors.newFixedThreadPool(3);

    static class DemoThread implements Runnable{
        @Override
        public void run() {
            for (int i = 1; i <= MAX; i++) {
                Print.tco(ThreadUtil.getCurThreadName()+",DemoThread轮次:"+i);
                    ThreadUtil.sleepMilliSeconds(10);
            }
        }
    }

    static class ReturnableTask implements Callable<Long>{

        //返回并发执行的时间
        @Override
        public Long call() throws Exception {
            long startTime = System.currentTimeMillis();
            Print.tco(ThreadUtil.getCurThreadName()+" 线程运行开始.");
            for (int i = 1; i <= MAX; i++) {
                Print.tco(ThreadUtil.getCurThreadName()+",Callable轮次:"+i);
                ThreadUtil.sleepMilliSeconds(10);
            }
            long used = System.currentTimeMillis() - startTime;
            Print.tco(ThreadUtil.getCurThreadName()+" 线程运行结束");
            return used;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        pool.execute(new DemoThread());//执行线程实例,无返回
        pool.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= MAX; i++) {
                    Print.tco(ThreadUtil.getCurThreadName()+",Runnable轮次:"+i);
                        ThreadUtil.sleepMilliSeconds(10);
                }
            }
        });
        Future<Long> submit = pool.submit(new ReturnableTask());
        Long res = submit.get();
        System.out.println("异步任务的执行结果为:"+res);
        Thread.sleep(10);
        System.out.println(ThreadUtil.getCurThreadName()+" 线程结束.");
    }
}

以上就是java中四种创建线程的方式,各有各的特点,不过在实际开发中线程池结合Runnable接口实现的技术会多点。就如SpringBoot的任务调度器其底层的原理其实就是运用了线程池的技术,在此篇文章就不叙述过多了。

posted @ 2021-09-25 16:08  蜡笔小新不吃青椒  阅读(426)  评论(0编辑  收藏  举报
Live2D