一宿君

V1

2022/08/06阅读:8主题:默认主题

Java并发编程系列(四)线程的创建、启动和终止

上期文章:Java并发编程系列(三)线程的六种状态及上下文切换

本期内容包含以下几点:

  • 创建线程的三种方式
  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

1、继成Thread类

public class ExtendsThreadDemo {
    public static void main(String[] args) {
        new MyThread().start();
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("MyThread");
        }
    }
}

注意要调用start()方法后,该线程才算启动!

我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。

注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出IllegalThreadStateException异常。

来看下java.lang.Thread类的初始化源码:

//片段1
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize)
 
{
     init(g, target, name, stackSize, nulltrue);
}


private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals)
 
{
     //继承Thread类创建线程初始化时名字可以为空,为空系统会默认创建一个名字,在此处做校验
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
  //当前线程(指创建该线程的线程),就是该线程的父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }

            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
  //检测当前线程是够有权限调用该创建的线程
        g.checkAccess();

        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
  //统计未启动线程的个数,未启动的线程不会添加到线程组中,
        //因此如果它们从未启动,则可以收集它们,但必须对它们进行计数,
        //以防止它们中具有未启动线程的守护线程组被破坏
        g.addUnstarted();
  
     //把当前线程的线程组设置为该线程的线程组(父线程的线程组)
        this.group = g;
     //和父线程的属性保持一致
        this.daemon = parent.isDaemon();//是否为守护线程
        this.priority = parent.getPriority();//优先级
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        //将父线程的inheritThreadLocal复制过来
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        //分配一个线程id
        tid = nextThreadID();
    }

// 片段2 - 构造函数调用init方法
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

解释一下init方法中的参数:

  • g:线程组,指定这个线程是在哪个线程组下;
  • target:指定要执行的任务;
  • name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,见片段2;
  • inheritThreadLocals:可继承的ThreadLocal

这里涉及到SecurityManager这个类,它是Java的安全管理器,它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作是什么,以及是否在允许执行该操作的安全上下文中执行它。应用程序可以允许或不允许该操作。

比如引入了第三方类库,但是并不能保证它的安全性。

其实Thread类也有一个checkAccess()方法,不过是用来检测当前运行的线程是否有权限修改被调用的这个线程实例。(Determines if the currently running thread has permission to modify this thread.)

实际情况下,我们大多是直接调用下面两个构造方法:

Thread(Runnable target)
Thread(Runnable target, String name)

看一下被创建的线程和父线程(这里指的是main线程):

public class ExtendsThreadDemo {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
//        myThread.start();
        Thread currentThread = Thread.currentThread();
        System.out.println("main线程的所属线程组:" + currentThread.getName() + ",MyThread线程的名称:" + myThread.getName());
        System.out.println("main线程的所属线程组:" + currentThread.getThreadGroup().getName() + ",MyThread线程的所属线程组:"  + myThread.getThreadGroup().getName());
        System.out.println("main线程是否为守护线程:" + currentThread.isDaemon() + ",MyThread线程是否为守护线程:" + myThread.isDaemon());
        System.out.println("main线程的优先级:" + currentThread.getPriority() + ",MyThread线程的优先级:" + myThread.getPriority());
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("MyThread");
        }
    }
}

输出结果:

main线程的所属线程组:main,MyThread线程的名称:Thread-0
main线程的所属线程组:main,MyThread线程的所属线程组:main
main线程是否为守护线程:false,MyThread线程是否为守护线程:false
main线程的优先级:5,MyThread线程的优先级:5

Thread类的几个常用方法:

  • currentThread():静态方法,返回对当前正在执行的线程对象的引用;
  • start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
  • yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
  • sleep():静态方法,使当前线程睡眠一段时间;
  • join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;

2、实现Runnable接口

接口源码:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

示例:

public class ImplementsRunnableDemo {

    public static void main(String[] args) {

        Thread currentThread = Thread.currentThread();

        Thread myThread = new Thread(new MyThread());
        myThread.start();

        System.out.println("main线程的所属线程组:" + currentThread.getName() + ",MyThread线程的名称:" + myThread.getName());
        System.out.println("main线程的所属线程组:" + currentThread.getThreadGroup().getName() + ",MyThread线程的所属线程组:"  + myThread.getThreadGroup().getName());
        System.out.println("main线程是否为守护线程:" + currentThread.isDaemon() + ",MyThread线程是否为守护线程:" + myThread.isDaemon());
        System.out.println("main线程的优先级:" + currentThread.getPriority() + ",MyThread线程的优先级:" + myThread.getPriority());

        // Java 8 函数式编程
        new Thread(new MyThread() {{
            {
                System.out.println("MyThread---java8---" + currentThread.getName());
            }
        }}).start();
    }

    static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread---common---" + Thread.currentThread().getName());
        }
    }
}

输出结果:

main线程的所属线程组:main,MyThread线程的名称:Thread-0
main线程的所属线程组:main,MyThread线程的所属线程组:main
main线程是否为守护线程:false,MyThread线程是否为守护线程:false
main线程的优先级:5,MyThread线程的优先级:5
MyThread---java8---main
MyThread---common---Thread-0
MyThread---common---Thread-1

由输出结果可以看出,表面上的效果是和继承Thread一样的,看下执行顺序:

  • 先执行Java 8 函数式编程的线程Thread-1的sout语句,输出MyThread---java8---main
  • 再回头执行myThread线程Thread-0run()方法,输出MyThread---common---Thread-0
  • 然后在执行Java 8 函数式编程的线程Thread-1run()方法,输出MyThread---common---Thread-1

3、实现Callable接口

CallableRunnable类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型

接口源码:

@FunctionalInterface
public interface Callable<V{
    call() throws Exception;
}

那一般是怎么使用Callable的呢?Callable一般是配合线程池工具ExecutorService来使用的。我们会在后续章节解释线程池的使用。这里只介绍ExecutorService可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Futureget方法得到结果。

我们先用普通的创建方式:

public static void main(String[] args) {

        // 将callable作为参数创建FutureTask对象
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new TaskThread());
        // 将futureTask作为参数创建线程
        Thread thread = new Thread(futureTask);
        thread.start(); // 启动线程
        try {
            // 获取线程执行后的返回值
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class TaskThread implements Callable<Integer{
        @Override
        public Integer call() throws Exception {
            // 模拟计算需要一秒时间,返回数值2
            Thread.sleep(1000);
            return 2;
        }
    }

配合ExecutorService示例Demo:

public class ImplementsCallableDemo {

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        TaskThread taskThread = new TaskThread();
        Future<Integer> result = executorService.submit(taskThread);
        // 注意调用get方法会阻塞当前线程,直到得到结果。
        // 所以实际编码中建议使用可以设置超时时间的重载get方法。
        System.out.println("返回值:" + result.get(2, TimeUnit.SECONDS));
        result.cancel(true);
        System.out.println("到这线程仍然处于阻塞状态,程序不会退出!");
    }

    static class TaskThread implements Callable<Integer{
        @Override
        public Integer call() throws Exception {
            // 模拟计算需要一秒时间,返回数值2
            Thread.sleep(1000);
            return 2;
        }
    }
}

输出结果:

返回值:2
到这线程仍然处于阻塞状态,程序不会退出!

4、Future接口

Future接口只有几个比较简单的方法:

public abstract interface Future<V{
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException
;
}

cancel方法是试图取消一个线程的执行。

注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean类型的返回值是“是否取消成功”的意思。参数paramBoolean表示是否采用中断的方式取消线程执行。

所以有时候,为了让任务有能够取消的功能,就使用Callable来代替Runnable。如果为了具有可取消性而使用 Future,但又不提供可用的结果,则可以声明 Future<?>形式类型、并返回 null作为底层任务的结果。

5、FutureTask类

上面介绍了Future接口。这个接口有一个子实现类叫FutureTaskFutureTask是实现的RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口:

public class FutureTask<Vimplements RunnableFuture<V{
    /*
     * state可能的状态转变路径如下:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */

    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
}

state表示任务的运行状态,初始状态为NEW。运行状态只会在set、setException、cancel方法中终止。COMPLETING、INTERRUPTING是任务完成后的瞬时状态。

public interface RunnableFuture<Vextends RunnableFuture<V{
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */

    void run();
}

FutureTask类有什么用?为什么要有一个FutureTask类?前面说到了Future只是一个接口,而它里面的cancelgetisDone等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask类来供我们使用。

示例代码:

// 自定义Callable,与上面一样
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        // 模拟计算需要一秒时间,返回数值2
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]) throws Exception {
        // 使用方法
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        executor.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

使用上与第一个Demo有一点小的区别。首先,调用submit方法是没有返回值的。这里实际上是调用的submit(Runnable task)方法,而上面的Demo,调用的是submit(Callable<T> task)方法。

然后,这里是使用FutureTask直接取get取值,而上面的Demo是通过submit方法返回的Future去取值。

在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。这块有兴趣的同学可以参看FutureTask源码。


专注于Java基础、进阶、面试以及计算机基础知识分享🐳。偶尔认知思考、日常水文🐌。
公众号二维码
公众号二维码
随和的皮蛋桑
随和的皮蛋桑

分类:

后端

标签:

后端

作者介绍

一宿君
V1

Java开发工程师