Java 并发编程—实现线程的方式只有一种
无论是使用 Thread 还是 Runnable 都无法返回值,这一点是它们的共同缺点。JDK1.5 之后提出了 Callable。
Callable 接口更像是 Runnable 接口的增强版,相比较 Runable 接口,Call()方法新增捕获和抛出异常的功能;Call()方法可以返回值
Future 接口提供了一个实现类 FutureTask 实现类,FutureTaks 类用来保存 Call()方法的返回值,并作为 Thread 类的 target。
调用 FutureTask 的 get()方法来获取返回值
class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
}
但是在 Thread 类中并没有接受 Callable 实例对象的方式,在实现 Callable 之后需要借助 FutureTask 类,在 JDK1.5 之后 Java 提供了 java.util.concurrent.FutureTask。
来看看 FutureTask 的两种构造方法:
public FutureTask(Callable<V> callable)
public FutureTask(Runnable runnable,V result)
其基本的类继承结构如图所示,可以发现 FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 又实现了 Future 和 Runnable 接口
所以无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行的,而不是说它们本身就是线程。它们可以放到线程池中执行,不管用什么方法,最终都是靠线程来执行的,而子线程的创建方式仍脱离不了最开始讲的两种基本方式,也就是实现 Runnable 接口和继承 Thread 类。
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future 提交返回结果
Future<Integer> future = service.submit(new CallableTask());
[](()线程池创建线程
线程池确实实现了多线程,比如我们给线程池的线程数量设置成 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 也是可以创建线程的,但是它们本质上也是通过前两种基本方式实现的线程创建。”这样的回答会成为面试中的加分项。
需要更多大厂面试资料的话也可以[点击直接进入,免费获取!](()暗号:CSDN
[](()总结 1:实现线程只有一种方式
关于这个问题,我们先不聚焦为什么说创建线程只有一种方式,先认为有两种创建线程的方式,而其他的创建方式,比如线程池或是定时器,它们仅仅是在 new Thread() 外做了一层封装,如果我们把这些都叫作一种新的方式,那么创建线程的方式便会千变万化、层出不穷,比如 JDK 更新了,它可能会多出几个类,会把 new Thread() 重新封装,表面上看又会是一种新的实现线程的方式,透过现象看本质,打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的。
[](()总结 2:实现 Runnable 接口比继承 Thread 类实现线程要好
下面我们来对刚才说的两种实现线程内容的方式进行对比,也就是为什么说实现 Runnable 接口比继承 Thread 类实现线程要好?好在哪里呢?
实现 Runnable 与 Thread 类的解耦。Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,Thread 类负责线程启动和属性设置等内容,权责分明。
提高性能。使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run( 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 ) 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。
[](()总结 3:为什么多线程启动不是调用 run()而是 start()
public synchronized void start() {
/**
This method is not invoked for the main method thread or "system"
group threads created/set up by the VM. Any new functionality added
to this method in the future may have to also be added to the VM.
A zero status value corresponds to state "NEW".
*/
// 没有初始化,抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
so that it can be added to the group's list of threads
and the group's unstarted count can be decremented. */
group.add(this);
// 是否启动的标识符
boolean started = false;
try {
// start0() 是启动多线程的关键
// 这里会创建一个新的线程,是一个 native 方法
// 执行完成之后,新的线程已经在运行了
start0();
// 主线程执行
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
start 方法的源码也没几行代码,注释也比较详细,最主要的是 start0() 方法,这个后面在解释。再来看看 run() 方法的源码:
@Override
public void run() {
// 简单的运行,不会新起线程,target 是 Runnable
if (target != null) {
target.run();
}
}
run() 方法的源码就比较简单的,就是一个普通方法的调用,这也印证了我们上面的结论。
评论