创建对象与创建线程的区别
在思考为什么要使用线程池,而不手动创建线程之前。我们先来了解 Java 创建普通对象与线程的区别。
普通Java对象创建
Java创建普通Java对象,了解JVM的同学,应该都知道JVM创建对象的过程,主要在内存上做了一些处理:
- JVM 会在 Java 堆中为该对象分配内存空间。这块内存空间用于存储对象的实例数据,包括成员变量的值等。
- JVM 还会在方法区中分配该对象的元数据信息,包括类的定义、方法签名、属性等。
- JVM 将创建的对象内存地址赋给相关引用变量。
Java 在创建普通对象时,主要在堆内存中进行数据处理。
Java线程创建
接下来,一起看看Java线程的创建过程。首先看一下线程创建的代码:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新线程运行入口");
}
}).start();
}
分析上述代码,线程创建分两个过程:第一步,创建一个Thread对象,其实这就是一个普通的Java对象创建,没有什么特殊的;第二步,调用 start() 方法,这个方法才是真正启动一个线程的过程。
在 start() 这个方法中,JVM 会与操作系统内核进行一系列的交互操作,以便能够在操作系统层面创建和管理该线程。这些交互过程主要包括以下几个方面:
- 内存分配:JVM 需要向操作系统申请分配线程所需的内存资源,包括线程栈、程序计数器等。这些内存空间是在 Java 堆之外分配的。
- 系统调用:JVM 会调用操作系统提供的线程创建 API,如 Windows 下的 CreateThread,Linux 下的 pthread_create 等,在操作系统内核层面创建一个新的本地线程。
- 关联映射:JVM 会在内部建立 Java 线程对象和操作系统线程之间的映射关系,将两者关联起来,这样 Java 线程就可以直接利用操作系统提供的线程功能。
- 数据结构更新:JVM 会将新创建的本地线程的一些信息,如文件描述符等,添加到自身的内部数据结构中,便于后续的线程管理和调度。
- 线程调度:创建完成后,JVM 会将新线程加入到自身的线程调度器中,等待被 CPU 调度执行。
在创建线程时,除了创建一个普通Java对象外,还需要上面描述的一些额外流程处理,包含了JVM 与操作系统内核交互。由此可以看出,创建线程比创建普通Java对象复杂得多,开销大。
总结 Java 中创建对象和创建线程的区别
1、内存分配方式:
- 创建对象时,JVM 会在 Java 堆中为新对象分配内存。
- 创建线程时,JVM 除了在堆中分配线程对象,还需要在 Java 堆外分配线程栈等资源。
2、执行过程:
- 创建对象只是在内存中分配空间并初始化对象,不涉及任何执行过程。
- 创建线程则需要 JVM 调用操作系统的线程创建 API,在操作系统层面创建新的执行线程。
3、关联映射:
- 创建对象只是 Java 层面的操作,不需要建立任何内部映射关系。
- 创建线程时,JVM 需要在内部建立 Java 线程和操作系统线程之间的对应关系。
4、资源管理:
- 创建对象只需要管理 Java 堆中的内存分配。
- 创建线程需要管理线程栈、程序计数器等线程特有的资源,还需要与操作系统交互。
5、性能影响:
- 创建对象相对简单,性能开销较小。
- 创建线程需要 JVM 执行更多的底层操作,性能开销会相对较大。
建议使用线程池,而不手动创建线程
分析:
基于前面线程创建分析可知,创建和销毁线程开销较大。既然创建线程开销大,那为何不将已创建的在使用完后不销毁,放在某个容器里面,等需要用的时候再取出来使用,从而提高复用性,这个容器就是线程池。
拿工人使用计算器记账的例子:工人是线程、计算器是系统资源。手动创建线程相当于老板要从市场上经过一系列面试流程聘请一个工人来记账,并给工人分配一个计算器,等工人记完账后就结清工资辞退工人,需要时再聘请,当任务量较多、需要频繁处理,对老板来说浪费大量的时间在聘请和辞退过程中。使用线程池就相当于建立了一个人事部,通过人事部聘请一些工人,并为他们分配计算器,工人记账完后不辞退,让工人休息下,随时待命,等有任务来时就可以马上安排合适的工人来处理。这极大的节省了聘请和辞退时间,提高了处理任务的效率,同时还可以统一管理工人,合理调度。
线程池采用一种池化思想,这是一种通用的资源管理策略,在系统中预先申请并维护一个资源池,然后根据需求动态地分配和复用这些资源,从而提高资源利用率和系统性能。使用线程池除了提高线程复用性这个优势外,还有其他很多优势。想一下,现在增加了一个线程池对象,是否可以基于线程池实现更多统一管理线程的功能(如调度、异常处理等)。虽然实现这些功能有些复杂,但一旦实现就可以一直使用。当然,这些功能也不需要自己去实现,Java 标准库中已经提供了多种线程池的实现,开发者可以直接使用。
手动创建线程与使用线程池对比:
在 Java 中,手动创建线程和使用线程池两种方式都可以实现多线程编程,但它们在实现原理、资源管理、性能表现等方面存在一些显著差异:
1、实现原理:
- 手动创建线程:直接调用 Thread 类的构造函数或 Runnable 接口创建新线程。
- 使用线程池:通过 ExecutorService 接口及其实现类 (如 ThreadPoolExecutor) 来管理和复用线程资源。
2、资源管理:
- 手动创建线程:每次创建线程都需要向操作系统申请分配新的线程栈等资源,资源开销较大。
- 使用线程池:线程池会预先创建并管理一定数量的线程,复用这些线程资源,减少了频繁创建和销毁线程的开销。
3、伸缩性:
- 手动创建线程:需要手动控制线程的创建和销毁,难以实现动态伸缩。
- 使用线程池:线程池可根据负载情况动态调整线程数量,具有更好的伸缩性。
4、任务调度:
- 手动创建线程:需要自行编写调度逻辑来管理多个线程。
- 使用线程池:线程池内部实现了任务队列和调度机制,可自动安排任务的执行。
5、异常处理:
- 手动创建线程:需要自行处理每个线程的异常。
- 使用线程池:线程池会统一处理线程执行时抛出的异常。
6、性能表现:
- 手动创建线程:线程创建和销毁开销较大,适用于任务少,执行时间较长的任务。
- 使用线程池:线程复用降低了开销,适用于任务多,执行时间较短的任务,能够更好地利用系统资源。
线程池的优势
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
主要特点为:线程复用、控制最大并发数、管理线程。
举例对比:
-
不采用线程池
public class ThreadPoolTest { public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); final Random random = new Random(); final ArrayList<Integer> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { list.add(random.nextInt()); } }); thread.start(); thread.join(); } System.out.println("处理时间:" + (System.currentTimeMillis() - start)); System.out.println("列表大小:" + list.size()); } } // 运行结果 处理时间:181 列表大小:1000
-
采用线程池运行结果
public class ThreadPoolTest { public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); final Random random = new Random(); final ArrayList<Integer> list = new ArrayList<>(); final ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 1000; i++) { executorService.execute(new Runnable() { @Override public void run() { list.add(random.nextInt()); } }); } executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.DAYS); System.out.println("处理时间:"+(System.currentTimeMillis()-start)); System.out.println("列表大小:"+list.size()); } } // 运行结果 处理时间:13 列表大小:1000