JAVA中创建线程的方式详解

现在的开发行业对程序员的水准可以说是越来越高,如果只是在小公司每天还竟是crud并且还不想转行,那么笔者劝你多学些技术,去外边多面试这样才会知道自己的不足去提升自己,跳槽涨工资是很香的。

今天主要是对Java的线程创建方式做一个讲解,后续还会对线程的方面做文章。


  • java线程的创建方式共4种。 

  1. extend Thread
  2. implement Runnable
  3. Callable和FutureTask组合
  4. 线程池ThreadPool

  •   第一种Thread的方式

  1. 通过new Thread();方式创建线程,如下图
1 Thread threadA = new Thread(); //调用Thread的无参构造方法创建一个线程threadA
2 threadA.setName("thread-A"); // threadA线程起个名字:thread-A, 通过setName方法给Thread的name属性赋值
3 threadA.start(); //调用Thread的start方法启动线程

但经过以上创建线程的方式是没有执行实例的,也就是说创建了一个空线程,线程启动后没有具体的执行逻辑

      2. 通过继承Thread的方式创建线程,如下列代码

public class Demo {
    
    static class ChildThread extends Thread {
        @Override
        public void run() { // 重写Thread中的run方法,
            System.out.println(Thread.currentThread().getName()); //打印子线程名称
        }
    }

    public static void main(String[] args) {
        ChildThread childThread = new ChildThread(); //创建线程
        childThread.start(); // 启动线程
    }
}

 如上代码所述,静态内部类ChildThread继承了Thread类,那么就可以通过new ChildThread()方式创建线程了,不同的是run方法可以写在ChildThread里,然后通过start方式去启动线程

这种方式有一点需要注意的,run方法不能直接调用,因为start方法才是启动线程的方法,也就是说start方法启动后创建的子线程就会和主线程并行,所以如果直接调run方法就不会启动子线程

如图  调用start方法的线程走势:start启动子线程,之后run方法也会被执行(是由子线程执行),此为异步执行
 

 如图 直接调用run方法的线程走势:没有调用start方法启动子线程,所以还是主线程执行的run方法

  • 第二种:实现Runnable的方式

        1. 实现Runnable接口方式

public class Demo {

    static class ChildRunable implements Runnable {//实现Runnable接口
        @Override
        public void run() { // 重写Thread中的run方法,
            System.out.println(Thread.currentThread().getName()); //打印子线程名称
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new ChildRunable());//创建线程
        t1.start();
        System.out.println(Thread.currentThread().getName());//打印主线程名称
    }
}

由上述代码所示,ChildThread类实现了Runnable接口并重写了run方法,run方法的内容是打印子线程名称,在主线程中依然是用new Thread创建了线程,不过其中的执行实例是ChildRunable类的实例,不过这里的run也是不可以直接调用的理由同上。

到这里可能会有人有疑问,为什么无论继承Thread还是实现Runnable都要重写run方法还有为什么创建线程都要用到Thread?

首先,无论是继承Thread还是Runnable都要重写run方法是因为Thread类是实现了Runnable接口的,在其中就重写了run方法,而这个run方发会去执行新建线程的执行实例的逻辑,如果像上面的ChildRunable 或ChildThread没有重写run方法,那么就等于是创建了空线程没有意义反而会占用cpu资源,看以下Thread源码就懂了,

public
class Thread implements Runnable //这是Thread类头部,实现了Runnable接口
public Thread() { //Thread的无参构造方法
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) { //Thread的有参构造方法,上面用到的创建线程的方式就用的这个方法
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
private Runnable target; // target可以理解为线程的执行实例
@Override
public void run() { // 这是Thread类中重写的Runnable的run方法,是执行实例的具体逻辑
    if (target != null) { // 线程的执行实力是否为空,这里指的是通过有参构造的Thread方法传的Runnable实例
        target.run();// 调用实例中的run方法执行具体逻辑
    }
}

然后讲下为什么每次创建线程都需要用到Thread,要知道Thread才是java中创建线程的核心,而Runnable接口中只有一个run方法,作用就是在实现类中重写run方法编写线程的执行逻辑,通过上面的Thread的run方法的源码可见会先判断线程执行实例是否为空,不为空则直接调用实例的run方法执行,不过在执行之前需要先调用Thread的start方法,才能启动新建的线程,线程启动后才会去执行run方法。

2.匿名函数的方式和lamda表达式写法

 //匿名类的写法
Thread threadA = new Thread(new Runnable() { // 调用了Thread的有参构造方法,参数是Runnable接口实例
            @Override
            public void run() { // 实现Runnable的run方法 target:线程执行实例
                System.out.println("创建了一个线程;");
            }
        }); 
  threadA.start();// 启动线程
//lamda表达式写法
Thread threadA = new Thread(() -> { // lamda表达式编写执行逻辑
            System.out.println(Thread.currentThread().getName());//打印子线程名称

        });
threadA.start(); //启动线程
System.out.println(Thread.currentThread().getName());//打印主线程名称

到这里先简单总结下Thread和Runnable,Thread和Runnable可以说是一个很好的组合(Thread中只支持Runnable的执行实例),由Thread创建线程和启动线程,由Runnable提供的run方法编写执行逻辑代码,但是Runnable的run方法是没有返回值的,那么这就导致线程异步执行的结果无法被获取,那这样的弊端如何消除呢,接着往下看。

  • 第三种 Callable和FutureTask创建线程

1.上面已经介绍过Thread和Runnable创建线程的方式,但是他们都有一个缺陷,就是没有返回值,不能获取异步执行后的结果

我们先简单了解下Callable接口

        (1)首先Callable接口,它是一个泛型接口,也是一个函数式接口,唯一的抽象方法call()有返回值,返回类型是Callable接口的泛型形参类型。而且call()方法有异常声明throw Exception,允许内部异常直接抛出。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception; // 返回值是泛型
}

虽然Callable有返回值,但是它并不能和Runnable实例一样,作为Thread线程实例的target来使用,因为Thread的target属性的类型是Runnable,而且Callable接口和Runnable之间没有任何继承关系。既然如此,那Callable接口怎么创建线程来执行呢?继续看下去

        (2)RunnableFuture接口

               这是一个在Callable和Thread之间起到衔接作用的接口,那具体是怎么衔接的呢,RunnableFuture接口实现了两个目标,既可以作为Thread线程实例的target,又可以获取异步执行结果。具体原因:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

通过RunnableFuture源码可以看出:它继承了Runnable、Future接口,跟它的名字一样RunnableFuture,继承Runnable保证了可以作为Thread线程实例的target目标;继承Future接口,保证了可以获取异步执行结果。

在这里再给大家介绍下Future接口

        (3)Future接口提供了这么几个功能

                        能够取消异步执行中的任务。

                        判断异步任务是否执行完成。

                        获取异步任务完成后的返回结果。

             Future接口源码和主要方法详细说明:

public interface Future<V> {

    //取消异步执行中的任务
    boolean cancel(boolean mayInterruptIfRunning); 

    //获取异步任务的取消状态。如果任务完成前被取消,就返回true。
    boolean isCancelled()+

    //获取异步任务的执行状态。如果任务执行结束,就会返回true
    boolean isDone();

    //获取异步任务执行结果,这个方法时阻塞性的。如果任务没有执行完成,异步结果获取线程会一直被阻塞,直到异步任务执行完成,结果返回给调用线程
    V get() throws InterruptedException, ExecutionException;

    //设置时限,跟上面的get方法一样,只是有了等待时间,到时间就会抛出异常。
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future是一个对异步任务进行交互、操作的接口。但是它只仅仅是一个接口,没有实现类肯定是没有办法完成对异步任务的操作的,所以jdk提供了一个实现类——FutureTask。

        (4)FutureTask类

                FutureTask类是Future接口的实现类,提供了对异步任务操作的具体实现。但是FutureTask不仅实现了Future,还有RunnableFuture接口。

 RunnableFuture接口既可以作为Thread线程实例的target目标,又可以获取并发任务执行的结果,是Thread和Callable之间一个非常重要的接口。但是RunnableFuture只是个接口,需要结合它的实现类FutureTask创建对象,也就是说真正衔接Callable和Thread的是FutureTask这个类。

Future类的继承关系图

 FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable和Future,所以FutureTask既能作为一个Runnable类型的执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。

FutureTask如何完成多线程的并发执行、任务结果的异步获取呢?FutureTask内部有一个Callable类型的成员callable实例属性,如下:

private Callable<V> callable;

callable实例属性来保存Callable类型的任务,并且callable实例属性需要在FutureTask实例构造时进行初始化。FutureTask实现了Runnable接口,在其run方法的实现中执行callable成员的call方法。

此外,FutureTask中内部还有一个非常重要的Object类型的成员outcome实例属性

private Object outcome;

这是用来保存callable成员call()方法的异步执行结果的。供FutureTask的get()方法获取

        (5)使用Callable和FutureTask创建线程

public class Test {

    //步骤1
    static class TaskTest implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("执行call方法");
            Thread.sleep(3000);
            return "call";
        }
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //步骤2
        TaskTest taskTest = new TaskTest();
        //步骤3
        FutureTask<String> futureTask = new FutureTask(taskTest);
        //步骤4
        Thread thread = new Thread(futureTask);
        thread.start();
        //步骤5
        String s = futureTask.get();
        System.out.println(s);

    }


}

步骤1:先创建一个实现Callable接口的TaskTest类,并实现call方法编写具体逻辑。

步骤2:使用Callable实现类TaskTest构造一个实例

步骤3:使用FutureTask构造一个实例作为Thread构造函数的入参

步骤4:调用Thread的start方法启动线程,启动新线程的run方法并发执行。其内部的执行过程:启动Thread实例的run方法并发执行后,会执行FutureTask实例的run方法,最终会并发执行Callable实现类的call方法。

步骤5:调用FutureTask对象的get方法阻塞性地获得并发线程的执行结果

  • 第四种:线程池创建线程

关于线程池的创建线程的方式,先给大家展示一个标准的创建方式。

创建一个继承ThreadPoolExecutor的类,属于自定义方式的线程池。

public class Test {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();
        //提交无返回值的任务,任务类型是Runnable
        threadPoolTest.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("无返回值的任务");
            }
        });
        //提交有返回值的任务,任务类型是Callable
        threadPoolTest.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "有返回值的任务";
            }
        });

    }

    //继承ThreadPoolExecutor类
    static class ThreadPoolTest extends ThreadPoolExecutor {
        public ThreadPoolTest() {
            //构建线程池初始参数,
            //从左到右:核心线程数、线程池线程数最大值、非核心线程的空闲时间、时间单位、任务队列类型ArrayBlockingQueue是数组类型的
            super(5, 10, 5, TimeUnit.MINUTES, new ArrayBlockingQueue(100));
        }
        
        //重写submit方法,有返回值的任务提交时用的方法
        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(task);
        }
        //重写executor方法,无返回值的任务提交时用的方法
        @Override
        public void execute(Runnable command) {
            super.execute(command);
        }
    }
}

后续会就线程池单独写一篇详细的文章,谢谢观赏!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值