实现线程是并发编程中基础的基础,因为只有先实现多线程,才可以继续后续一系列操作。
虽然实现线程看似简单,但实际上却暗藏玄机。
实现线程的的方式到底有几种?2种、3种或是4种,很少人说只有1种。接下来看看它们具体指什么?2种实现方式的描述是最基本的,也是最熟知的,就先以2种线程实现方式的源码为例。
实现Runnable接口
// 代码1
public class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println("用Runnable接口实现线程");
}
}
或
// 代码2
public class RunnableThread {
public void runThread {
Runnable r = () -> {
System.out.println("用Runnable接口实现线程");
}; // Runnable是一个函数式接口,可以用lambda表达式建立一个实例
Thread t = new Thread(); // 由Runnable创建一个Thread对象
t.start(); // 启动线程
// 不要调用Thread类或Runnable对象的run方法。
// 直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。
// 应该调用Thread.start方法。这个方法创建一个执行run方法的新线程。
}
}
第1种方式是通过实现Runnable接口实现多线程。如代码1所示,首先通过RunnableThread类实现Runnable接口,然后重写run()方法,之后只需要把这个实现run()方法的实例传到Thread类中就可以实现多线程。代码2中的RunnableThread类虽然没有实现Runnable接口,但也是通过Runnable实现多线程。由于Runnable是一个函数式接口,因此可以用lambda表达式建立一个实例,而后由Runnable创建一个Thread对象启动线程。
继承Thread类
public class ExtendsThread extends Thread {
@Override
public void run() {
System.out.println("用Thread类实现线程");
}
}
第2种方式是继承Thread类,如代码所示,与第1种方式不同的是它没有实现接口,而是继承Thread类,并重写了其中的run()方法。
以上两种方式是非常熟悉,并且经常在工作种使用。
线程池创建线程
为何还有第3种或第4种方式呢?先看看第3种方式:通过线程池创建线程。线程池确实实现了多线程,比如我们给线程池的线程数量设置成10,那么就会有10个子线程来完成工作。接下来,通过解析线程池中的源码,来看看线程池是怎么实现线程的?
static class DefaultThreadFactory implements ThreadFactory {
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
T.setPriority(Thread.NORM_PRIORITY)
}
return t;
}
}
对于线程池,本质上是通过线程工厂创建线程的,默认采用DefaultThreadFactory,它会给线程池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。但是无论怎么设置这些属性,最终它还是通过new Thread()创建线程的,只不过这里的构造函数传入的参数要多一些,由此可以看出通过线程池创建线程并没有脱离最开始的那两种基本的创建方式,因为本质上还是通过new Thread()实现的。
有返回值的Callable创建线程
class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
retrun new Random().nextInt();
}
}
// 创建线程池
ExecutorService service = Executors.newFriendsThreadPool(10);
// 提交任务,并用Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());
第4种线程创建方式是通过有返回值的Callable创建线程,Runnable创建线程池是无返回值的,而Callable和与之相关的Future、FutureTask,它们把线程执行的结果作为返回值返回,如代码所示,实现了Callable接口,并且给它的泛型设置成Integer,然后它会返回一个随机数。
但是,无论是Callable还是FutureTask,它们首先和Runnable一样,都是一个任务,需要被执行的,而不是说它们本身就是线程。它们可以放到线程池中执行,如代码所示,submit()方法把任务放到线程池中,并由线程池创建线程,不管用什么方法,最终都是靠线程来执行的,而子线程的创建方式仍然脱离不了最开始的两种基本方式,就是实现Runnable接口和继承Thread类。
其他创建方式
定时器Timer
class TimerThread extends Thread {
// 具体实现
}
定时器也可以实现线程,如果新建一个Timer,令其每隔10秒或设置两个小时后,执行一些任务,此时它确实也创建了线程并执行了任务,但深入分析定时器的源码会发现,本质上它还是会有一个继承自Thread类的TimerThread,所以定时器创建线程最后又回到最开始的两种方式。
其他方式
/**
* 描述:匿名内部类创建线程
*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
除此之外,还有一些其他的方式,比如匿名内部类或者lambda表达式方式,实际上,匿名内部类或lambda表达式创建线程,它们仅仅是在语法层面上实现了线程,并不能把它归结于实现多线程的方式,如匿名内部类实现线程的代码所示,它仅仅是用一个匿名内部类把需要传入的Runnable给实例出来。
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
再看一下lambda表达式方式。如上面的代码所示,最终它们依然符合最开始所说的那两种实现线程的方式。
实现线程只有一种方式
关于这个问题,先不聚焦为什么说创建线程只有一种方式,先认为有两种创建线程的方式,而其他的创建方式,比如线程池或是定时器,它们仅仅是在new Thread()外做了一层封装。如果把这些方式都叫做一种新的方式,那么创建线程的方式便会千变万化,比如JDK跟新了,它可能会多出几个类,会把new Thread()重新封装,表面上看又会是一种新的实现线程的方式,打开封装后,发现它们最终都是基于Runnable接口或继承Thread类实现的。
为什么说基于Runnable接口或继承Thread类实现多线程的方式本质上是一种呢?
@Override
public void run() {
if (target != null) {
target.run();
}
}
首先,启动线程需要调用start()方法,而start()方法最终还会调用run()方法。先来看看第一种方式中run()方法是怎么实现的。可以看出run()方法的代码短小精悍,第1行代码if(target != null),判断target是否等于null,如果不等于null,就执行第2行代码target.run(), 而target实际上就是一个Runnable,即使用Runnable接口实现线程时传给Thread类的对象。
然后时第二种方式,继承Thread方式。在继承Thread类之后,会把上述的run()方法重写,重写后run()方法里直接就是所需要执行的任务,但它最终还是需要调用thread.start()方法来启动线程,而start()方法最终也会调用这个已经被重写的run()方法来执行它的任务,这时就彻底明白了,事实上创建线程只有一种方式,就是构造一个Thread类,这是创建线程的唯一方式。
两种创建线程方式本质上是一样的,它们不同点仅仅在于实现线程运行内容的不同。运行内容来自于哪里?
运行内容主要来自于两个地方,要么来自于target,要么来自于重写的run()方法。因此,本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式。就是通过实现Runnable接口的方式,或是继承Thread类重写run()方法的方式,把想要执行的代码带入,让线程去执行,在此基础上,如果还想有更多实现线程的方式,比如线程池和Timer定时器,只需要在此基础上进行封装就行了。
实现Runnable接口比继承Thread类好
好在哪里呢,有三点。
首先, 从代码的架构考虑,实际上,Runnable里只有一个run()方法,它定义了需要执行的内容,在这种情况下,实现了Runable与Thread类的解耦,Thread类负责线程启动和属性设置等内容,权责分明。
第二,某些情况下可以提高性能,使用继承Thread类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了Thread类的类,如果此时执行的内容比较少,比如只是在run()方法里简单打印一行文字,那么它带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比run()方法打印文字本身带来的开销要大的多,相当于捡了芝麻丢了西瓜。如果使用实现Runnable接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
第三,Java语言不支持双继承,如果类一旦继承了Thread类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
综上,应该优先选择通过实现Runnable接口的方式来创建线程。