JUC并发编程 -- 创建线程的3种方式详解 & 原理之Thread 与 Runnable 的关系

Posted Z && Y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程 -- 创建线程的3种方式详解 & 原理之Thread 与 Runnable 的关系相关的知识,希望对你有一定的参考价值。

准备工作:

  • 为了打印更加直观,我这里导入slf4j日志的依赖

pom.xml

    <dependencies>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>

在resource目录下面放入slf4j配置文件

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration
        xmlns="http://ch.qos.logback/xml/ns/logback"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
        </encoder>
    </appender>
    <logger name="c" level="debug" additivity="false">
        <appender-ref ref="STDOUT"/>
    </logger>
    <root level="ERROR">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

1. 方法一,直接使用 Thread

代码:

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test1")
public class Test1 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                log.debug("t1 running");
            }
        };
//        给线程起名字
        t.setName("t1");
//        启动t1线程
        t.start();
//        主线程打印消息
        log.debug("main running");
    }
}

运行结果:


2. 方法二,使用Runnable配合Thread

代码:

import lombok.extern.slf4j.Slf4j;

// 只有以c开始的消息才会打印
@Slf4j(topic = "c.Test2")
public class Test2 {
    public static void main(String[] args) {
//        r是任务对象
        Runnable r = new Runnable() {
            @Override
            public void run() {
                log.debug("t2 running");
            }
        };
//        t是线程对象, 构造方法的第一个参数丢入任务对象, 第二个参数设置线程的名称
        Thread t = new Thread(r, "t2");
        t.start();
//        主线程打印消息
        log.debug("main running");
    }
}

结果:


3. Java 8以后可以使用lambda精简代码

Runnable接口里面有一个@FunctionalInterface注解,说明这个接口可以被lamda表达式简化。(注意:@FunctionalInterface存在只有一个方法的接口中)

代码:

import lombok.extern.slf4j.Slf4j;

// 只有以c开始的消息才会打印
@Slf4j(topic = "c.Test2")
public class Test2 {
    public static void main(String[] args) {
//        只有一个语句 可以省略{}
        Runnable r = () -> log.debug("t1 running");
//        t是线程对象, 构造方法的第一个参数丢入任务对象, 第二个参数设置线程的名称
        Thread t1 = new Thread(r, "t1");
        t1.start();
//        Thread构造方法使用lambda表达式简化
        new Thread(() -> log.debug("t2 running"), "t2").start();
//        主线程打印消息
        log.debug("main running");
    }
}

运行结果:


4. 方法三,FutureTask 配合 Thread

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况

FutureTask:

  • 可以看见FutureTask 实现了RunnableFuture这个接口
  • 可以看见RunnableFuture这个接口实现了Runnable,所以它可以作为一个任务对象,另外它还实现了Future接口。
  • Future接口里面有一个get方法,可以返回任务的执行结果
  • 我们去查看Runnable接口的源码,发现它这个run方法是没有返回值的,所以Runable接口并不方便的实现线程之间的通信

Callable:

  • Callable接口有一个call方法,并且它可以抛出异常(Runnable的run方法不可以抛出异常)

代码:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

@Slf4j(topic = "c.test3")
public class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Integer是FutureTask的返回值类型
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("t1 Running");
                // 让当前线程睡眠1s 然后返回520
                Thread.sleep(1000);
                return 520;
            }
        });

//      启动线程
        new Thread(task, "t1").start();
//      获取FutureTask的返回结果 {}是一个占位符,用来打印后面的结果
        log.debug("task返回值: {}", task.get());
    }
}

运行结果:


5. 原理之Thread 与 Runnable 的关系

  • 我们先来看Thread类的构造方法,把Runnable接口实现类当作参数

  • 调用init方法

  • 点击init方法,发现又进入了一个重载的init方法

  • 再次点击init方法,发现就是把这个Runnable接口实现类赋值给Thread类一个名为target的成员变量。

  • 发现最终调用的Thread类的Run方法,所以方法2最终也是调用的Thread类的Run方法

结论:

  • Thread类继承了Runnable接口,重写了Run方法。方法一和方法二的本质其实都是走的方法一的run方法

6. 小结

  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
  • 推荐使用Runnable


以上是关于JUC并发编程 -- 创建线程的3种方式详解 & 原理之Thread 与 Runnable 的关系的主要内容,如果未能解决你的问题,请参考以下文章

JUC并发编程之CompletableFuture基础用法

JUC并发编程之CompletableFuture基础用法

JUC并发编程之CompletableFuture基础用法

JUC并发编程线程池及相关面试题 详解

JUC并发编程 详解锁与队列

尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类