Java多线程学习笔记——创建新线程的4种方法

1,347 阅读4分钟

Java语言内置了多线程支持。当我们启动一个Java程序时,实际上是启动了一个JVM进程,然后JVM启动主线程来执行main()方法。在main()方法中,我们又可以创建、启动其他线程。

本质上来讲,在Java中创建新线程十分简单,只需要实例化一个Thread实例即可:

Thread t = new Thread();

但是当我们调用该实例的start()方法来启动线程时,该线程其实什么事情也没干就结束了,原因是Thread类中覆写Runnable接口的run()方法的方法体如下:

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

这里的target字段是Runnable接口的一个实例,而当Thread构造器没传入任何参数时,target为null,线程自然也就不会执行任何操作。

在Java中,要创建能执行指定代码的新线程,有以下几种方法:

  1. Thread类派生一个自定义类,然后覆写run()方法。
  2. 创建Thread实例时,传入一个Runnadble接口的实例,同样要实现接口定义的run()抽象方法。
  3. 实现Callable接口,重写call()方法,并包装成FutureTask类对象,再作为参数传入Thread的构造器。
  4. 使用线程池创建新线程。

注意:

  1. 方法1和方法2因为都是通过覆写run()方法来创建新线程,而run()方法并无返回值,所以这两种方法也无法获取返回值。
  2. 在实际项目工程中,最好不要在应用中自行显式创建线程(方法1、2、3),线程资源要通过线程池提供。使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

1. 继承Thread类,覆写run()方法

public class ExecuteThread {
  public static void main(String[] args) {
    Thread t = new MyThread();
    t.start();	// 启动新线程
  }
}

// 继承自Thread的自定义类
class MyThread extends Thread {
  @Override
  public void run() {
    System.out.println("start new thread!");
  }
}

如果该类型的线程只需要使用一次,也可以用匿名内部类的方式让代码更加简单:

public class ExecuteThread {
  public static void main(String[] args) {
    Thread t = new Thread() {
      // 匿名内部类对run()方法的覆写
    	@Override
  		public void run() {
    		System.out.println("start new thread!");
  		}
    };
    t.start();	// 启动新线程
  }
}
  • 优点:编码简单,容易理解。
  • 缺点:不能继承其他类,功能单一。

2. 实现Runnadble接口,实现run()抽象方法

public class ExecuteThread {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 启动新线程
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

或者用Java8引入的Lambda语法进一步简写为:

public class ExecuteThread {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("start new thread!");
        });
        t.start(); // 启动新线程
    }
}
  • 优点:
    1. 传入的实例实现的是Runnadble接口,所以可以继承其他的类,避免了单继承的局限性。
    2. 适合多个相同程序代码的线程共享一个资源(同一个线程任务对象根据传入构造器的不同Runnadble接口实例,可以被包装成多个线程对象),实现解耦操作,代码和线程独立。
  • 缺点:实现相对复杂一些。

3. 实现Callable接口,重写call()方法,并包装成FutureTask对象传入Thread构造器

public class ExecuteThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callableInstance = new MyCallable<>("return value");
        // 使用FutureTask类包装Callable接口的实例,该对象封装了Callable接口实现实例的call()方法的返回值。
        FutureTask<String> task = new FutureTask<>(callableInstance);
        Thread t = new Thread(task);
        t.start();  // 启动新线程
        // 调用FutureTask实例的get()方法获取新线程执行结束返回值。
        System.out.println(task.get());
    }
}

class MyCallable<V> implements Callable<V> {
    private V toReturn;

    public MyCallable (V val) {
        toReturn = val;
    }

    @Override
    public V call() throws Exception{
        System.out.println("start new thread!");
        return toReturn;
    }
}
  • 优点:相对于方法2(传入Runnadble实例),可以获取返回值。
  • 缺点:实现比较复杂。

4. 使用线程池创建新线程

线程池的使用避免了因为频繁创建、销毁线程带来的大量系统开销,实现了资源的复用。具体的使用方法看这篇笔记:Java多线程学习笔记——如何使用线程池

参考链接