建立线程的4种方式

2021年11月22日 阅读数:2
这篇文章主要向大家介绍建立线程的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方法的源码:java

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

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

private Runnable target;安全

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

工具类

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

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实例属性中。

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

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次,每次输出当前运行线程的名字以及轮次。ide

至于为何是静态内部类,主要是为了方便调用外部类的属性,而若是该内部类不是静态的话还须要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的任务调度器其底层的原理其实就是运用了线程池的技术,在此篇文章就不叙述过多了。