拥有多线程和拥有一百枚核弹没有区别,因为都是毁灭性的存在。——麦克阿瑟
在Java中,实现多线程主要有三种方式:继承Thread类、实现Runnable接口和实现Callable接口。
多线程的形式上实现方式主要有两种,一种是继承Thread类,一种是实现Runnable接口。本质上实现方式都是来实现线程任务,然后启动线程执行线程任务(这里的线程任务实际上就是run方法)。
第一种方式:继承Thread类
万物皆可视为对象,线程也不例外。线程作为对象,具备可抽取的公共特性,这些特性可封装为类。通过使用类,我们可以实例化多个相同特性的对象。Thread类是JDK提供的一种简单方式来实现线程,通过继承Thread类并重写其run方法,我们可以在线程启动时执行自定义的run方法体内容。
在Spring Boot项目中,新创建一个TestThread测试线程类,并继承Thread,然后添加一个当前线程的名称,代码如下:
public TestThread() {
this.setName("测试线程");
}
接着重写run方法,来实现相关的业务逻辑,代码如下:
@Override
public void run() {
while (true) {
System.out.println("线程名:" + Thread.currentThread().getName());
try {
//线程休眠2秒
Thread.sleep(2000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
然后在项目启动类中main方法中,启动该线程,在启动线程的时候,并不是调用线程类的run方法,而是调用了线程类的start方法。
public static void main(String[] args) {
SpringApplication.run(DemoThreadApplication.class, args);
TestThread thread = new TestThread();
//使用start,启动线程
thread.start();
while (true) {
System.out.println("线程名:" + Thread.currentThread().getName());
//线程休眠2秒
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
启动项目,然后在控制台会输出运行的结果:
线程名:main
线程名:测试线程
线程名:测试线程
线程名:main
线程名:测试线程
线程名:main
线程名:测试线程
线程名:main
主线程main和测试线程run方法交替执行,在控制台是随机输出的,原因就是cpu将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替的执行输出,导致输出呈现乱序的效果。由此可见,线程开启后不一定会立即执行,则是由cpu调度执行的。
第二种方式:实现Runnable接口
为了避免Java单继承的限制,我们可以选择实现Runnable接口。通过实现Runnable接口的run()方法,我们可以将一个实现了该接口的对象传递给Thread类的构造方法来创建和启动线程。
Runnable接口的源代码如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
使用Runnable创建线程的步骤如下:
- 定义一个类实现Runnable的接口,作为线程任务类。
- 重写run方法,并实现相应的业务代码,也是线程所要执行的代码。
- 在启动类的main方法中创建线程任务类。
- 创建Thread类,并将线程任务类作为Thread类的构造方法传入,并启动线程。
创建线程任务,每隔2秒执行一次,代码如下:
public class TestRunnable implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("TestRunnable线程名:" + Thread.currentThread().getName());
try {
//线程休眠2秒
Thread.sleep(2000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
然后在启动类中创建线程,并将任务交付给线程进行处理,然后启动该线程,代码如下:
public static void main(String[] args) {
TestRunnable testRunnable = new TestRunnable();
//创建线程对象,通过线程对象来开启的线程
new Thread(testRunnable).start();
while (true) {
System.out.println("线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
运行结果如下所示:
线程名:main
TestRunnable线程名:Thread-4
TestRunnable线程名:Thread-4
线程名:main
TestRunnable线程名:Thread-4
线程名:main
实现Runnable接口的输出结果和继承Thread类的结果是一样的,都是交替运行输出, 但是ava是单继承的,就比如我们一个类已经继承了其他的类,就不能使用继承Thread类来创建线程了。但是我们可以使用实现Runnable接口来创建线程。
第三种方式:实现Callable接口
Callable是Java中的一个接口,类似于Runnable,它允许定义一个可以有返回值的任务,并且这个任务可以并发执行。Callable接口和Runnable接口类似,但是它允许返回结果,并能抛出异常。Callable需要依赖FutureTask,用于接收运算结果。一个产生结果,一个拿到结果。FutureTask是Future接口的实现类,也可以用作闭锁。
在使用Callable接口时,需要注意以下几点:
- Callable接口的任务可以抛出异常,因此需要在任务中处理异常或者在调用Future.get()方法时处理异常。
- Future.get()方法是阻塞的,它会等待任务执行完成并返回结果。如果任务执行时间较长,会影响程序的性能。因此,在使用Future.get()方法时需要谨慎考虑程序的性能和效率。
- Callable接口的任务可以并发执行,因此可以在多线程环境下使用,可以使用ExecutorService类来管理线程池并提交任务。
使用Callable创建线程的步骤:
- 创建一个类实现Callable接口,并实现call方法。
- 创建一个FutureTask,指定Callable对象,做为线程任务。
- 创建一个线程,并指定线程任务。
- 启动线程。
创建一个TestCallable类,然后实现Callable接口,并实现call方法,代码如下:
public class TestCallable implements Callable {
@Override
public Integer call() throws Exception {
System.out.println("开始执行Callable线程。。。");
// 睡1s
Thread.sleep(1000);
return 111;
}
}
然后在启动类中执行该线程。
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable testCallable = new TestCallable();
//执行Callable方式,需要FutureTask实现类的支持,
FutureTask<Integer> futureTask = new FutureTask(testCallable);
//启动线程
new Thread(futureTask).start();
System.out.println("接收线程运算的结果。。。");
Integer result = futureTask.get();
System.out.println("线程返回值:" + result);
}
运行结果如下所示:
接收线程运算的结果。。。
开始执行Callable线程。。。
线程返回值:111
三种创建线程的方式比较
- Thread
优点:简单易用,易于理解。可以重写Thread类的run()方法来实现线程的功能,符合面向对象的思想。
缺点:
- Java不支持多重继承,因此如果已经继承了其他类,则无法再继承Thread类。
- 每个线程都需要创建新的对象,会消耗更多的内存。
- 不适合在多线程共享资源的情况下使用,因为不同线程的run()方法内部使用的不是同一个对象,需要额外处理共享资源的问题。
- 实现Runnable接口
优点:
- 可以避免Java单继承的限制,可以与其他类继承。
- 适合在多线程共享资源的情况下使用,因为多个线程可以共享同一个Runnable对象。
- 可以将任务代码和资源分离,代码更加清晰。
缺点: - 不支持传递参数,如果需要传递参数,需要在Runnable对象中添加属性或者使用外部变量。
- 不适合在需要返回值的情况下使用,因为Runnable接口没有定义返回值的方法。
- 实现Callable接口
优点:
- 可以定义有返回值的任务,比Runnable更加灵活。
- 可以抛出异常,适合在需要处理异常的情况下使用。
- 可以使用Future对象获取任务的结果,比使用Thread.join()方法更加方便。
- 支持并发执行,可以提高程序的效率和性能。
缺点: - 使用比较复杂,需要使用ExecutorService类和Future对象来管理线程和获取任务结果。
- 不适合在需要大量使用线程并且需要返回结果的场景下使用,因为每个任务都需要创建一个新的Callable对象和Future对象,会消耗更多的内存。
综上所述,应该根据具体的需求和场景选择最合适的方式。如果只需要简单的启动线程并且不需要返回结果,可以选择继承Thread类的方式;如果需要传递参数并且不需要返回结果,可以选择实现Runnable接口的方式;如果需要返回结果并且能够处理异常,可以选择实现Callable接口的方式。
代码地址:https://github.com/dawandou/msy-code中的demo-thread项目。