JAVA 线程的创建 - 面试题引发的思考

337 阅读3分钟

1.JAVA创建线程的方法 - 面试题引发的思考

可以说是面试必答题了,一般性答案是三种:

暂时先不考虑线程池

  1. 继承Thread类,重写run方法
public class Test {
    public static void main(String[] args) {
        new ThreadTest().start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.print(Thread.currentThread().getName());
    }
}
  1. 实现Runable方法
public class Test {
    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        new Thread(runnableTest, "线程1").start();
        new Thread(runnableTest, "线程2").start();
    }
}

class RunnableTest implements Runnable{
    @Override
    public void run() {
       System.out.print(Thread.currentThread().getName());
    }
}
  1. 使用CallableFuture
public class Test {
    public static void main(String[] args){
        MyCallable callable = new MyCallable();
        FutrueTask<String> task = new FutureTask(callable);
        new Thread(task).start();
        try {
            System.out.println(task.get())
        } catch(Exception e){}
    }
}

class MyCallable implements Callable {
    @Override
    public String call() throw Exception {
        return Thread.currentThread().geName();
    }
}

在JAVA中线程的控制和业务的执行是分离的,可以看到三个创建线程的方法都包含new Thread().start(),实际上只有线程类的start()方法会调用native start0()去创建一个新的线程,不管是实现Runnable还是通过FutrueTask都是在启动线程后执行方法,这个方法一般来说可以执行任意操作take any action whatsoever

线程的实例化都做了什么

image.png Thread的实例化入参(从左到右)包括线程组、Runnable被调用run方法的对象(可用lambda表达式)、线程名、线程占用的堆栈大小(默认由虚拟机控制)、上下文

初始化init方法步骤

  1. 判断线程名是否为空,并赋值线程名; 如果为空,抛出空指针异常 name cannot be null
  2. 获取当前线程 Thread parent = currentThread()

public static native Thread currentThread(){}返回当前正在执行的线程对象的引用

  1. 获取当前系统的安全组管理器SecurityManager security = System.getSecurityManager()
  2. 判断是否指定线程组(树状数据结构),即入参的ThreadGroup, 如果指定了线程组,调用线程组的校验方法checkAccess()(校验当前线程是否有权限修改对应线程组);没有指定线程组,则用安全组内的线程组作为当前线程组或者使用当前线程的线程组
  3. 如果安全组存在校验安全组的使用权限security.checkPermission()
  4. 增加线程组内计数addUnstarted()
  5. 获取当前线程的上下文(通过当前线程的ClassLoader)
  6. 赋值Runnable对象, 判断是否守护线程,并提供优先级,当前线程ID累加nextThreadID()生成新线程tid

线程的start()都做了什么

public synchronized void start() {
    // 判断线程状态是否是未启动状态, 0状态对应New状态
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    // 当前线程添加到线程组的线程列表内,减少线程组计数(init时通过addUnstarted()增加的计数)
    group.add(this);
    boolean started = false;
    try {
        // 调用本地的start0方法创建一个线程
        start0();
        started = true;
     // 守护线程不一定执行finally(init时判断守护线程的结果)
    }finally {
        try {
            if(!started) {
                group.threadStartFaild(this);
            }
        } catch (Throwable ignore){}
    }
}

总结

通过继承Thread或者实现Runnable接口重写run()方法,写入在新建线程后要执行的业务逻辑,线程对象执行start()方法新建一个线程,在线程启动成功后会自动调用线程对象的run()方法,至于为什么可以通过实现Callable并使用FutureTask,是因为CallableRunnable类似,Executors类包含从其他常见形式转换为Callable类的实用方法,并且FutureTask实现的接口RunnableFuture接口继承于Runnable

附录: Callablecall方法与Runnablerun方法的区别

  1. call方法执行后可以有返回值,run方法没有返回值
  2. call方法可以抛出异常,run方法不可以
  3. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果 。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果