🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,
15年
工作经验,精通Java编程
,高并发设计
,Springboot和微服务
,熟悉Linux
,ESXI虚拟化
以及云原生Docker和K8s
,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea
深入浅出Java并发编程:线程基础
引言
在当今的软件开发领域,并发编程已经成为一项不可或缺的技能。随着多核处理器的普及,应用程序的性能优化越来越依赖于如何有效地利用多线程技术。Java作为一门成熟的编程语言,提供了丰富的并发编程工具和API,使得开发者能够轻松地构建高效、稳定的多线程应用。
然而,并发编程并非易事。它涉及到许多复杂的概念和技术,如线程安全、锁机制、线程通信等。对于初学者来说,理解这些概念并掌握其应用是一个不小的挑战。本文将从最基础的线程概念入手,逐步深入,帮助读者建立起对Java并发编程的全面理解。
本文将围绕以下几个核心主题展开:
- 进程与线程的区别与联系:理解操作系统层面的进程与线程,以及它们在Java中的具体表现。
- 线程的创建方式:详细介绍Java中创建线程的三种方式:继承
Thread
类、实现Runnable
接口、以及使用Callable
和Future
。 - 线程的生命周期与状态转换:深入探讨线程从创建到销毁的整个生命周期,以及各个状态之间的转换条件。
- 守护线程与用户线程:解释守护线程与用户线程的区别,以及它们在应用中的使用场景。
- 线程优先级与调度策略:探讨线程优先级的概念,以及Java虚拟机如何调度线程。
1. 进程与线程的区别与联系
1.1 进程与线程的基本概念
在操作系统中,进程和线程是两个核心概念。理解它们的区别与联系是学习并发编程的基础。
-
进程:进程是操作系统进行资源分配和调度的基本单位。每个进程都有独立的内存空间,包含了程序代码、数据、堆栈等。进程之间的通信需要通过特定的机制,如管道、消息队列、共享内存等。
-
线程:线程是进程中的一个执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。线程之间的通信相对简单,因为它们可以直接访问共享的内存。
1.2 进程与线程的区别
特性 | 进程 | 线程 |
---|---|---|
资源分配 | 独立的内存空间 | 共享进程的内存空间 |
通信方式 | 需要特定的机制(如管道、消息队列) | 可以直接访问共享内存 |
创建开销 | 较大 | 较小 |
切换开销 | 较大 | 较小 |
独立性 | 高度独立 | 依赖于进程 |
1.3 进程与线程的联系
- 资源共享:线程共享进程的资源,如内存、文件句柄等。这使得线程之间的通信更加高效。
- 并发执行:多个线程可以在同一个进程中并发执行,从而提高程序的执行效率。
- 依赖关系:线程依赖于进程,进程终止时,其所有线程也会终止。
2. 线程的创建方式
在Java中,创建线程主要有三种方式:继承Thread
类、实现Runnable
接口、以及使用Callable
和Future
。下面我们将详细介绍这三种方式。
2.1 继承Thread
类
继承Thread
类是最简单的创建线程的方式。通过重写run()
方法,可以定义线程执行的任务。
java">class MyThread extends Thread {@Overridepublic void run() {System.out.println("Thread is running");}
}public class Main {public static void main(String[] args) {MyThread thread = new MyThread();thread.start();}
}
优点:简单直观,适合简单的任务。
缺点:由于Java不支持多继承,继承Thread
类后无法再继承其他类。
2.2 实现Runnable
接口
实现Runnable
接口是更常用的创建线程的方式。通过实现run()
方法,可以将任务与线程分离。
java">class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("Runnable is running");}
}public class Main {public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());thread.start();}
}
优点:避免了单继承的限制,适合复杂的任务。
缺点:无法直接获取线程的执行结果。
2.3 使用Callable
和Future
Callable
接口与Runnable
接口类似,但它可以返回一个结果,并且可以抛出异常。通过Future
对象,可以获取线程的执行结果。
java">import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {return "Callable is running";}
}public class Main {public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<String> futureTask = new FutureTask<>(new MyCallable());Thread thread = new Thread(futureTask);thread.start();System.out.println(futureTask.get());}
}
优点:可以获取线程的执行结果,适合需要返回值的任务。
缺点:使用相对复杂,需要处理Future
对象。
3. 线程的生命周期与状态转换
线程的生命周期包括多个状态,理解这些状态及其转换条件对于掌握线程的行为至关重要。
3.1 线程的生命周期
Java线程的生命周期包括以下几个状态:
- 新建(New):线程对象被创建,但尚未启动。
- 就绪(Runnable):线程已经启动,等待CPU调度执行。
- 运行(Running):线程正在执行
run()
方法。 - 阻塞(Blocked):线程因为某些原因(如等待锁)暂时停止执行。
- 等待(Waiting):线程无限期等待其他线程的通知。
- 超时等待(Timed Waiting):线程在指定的时间内等待其他线程的通知。
- 终止(Terminated):线程执行完毕或被强制终止。
3.2 状态转换
线程的状态转换可以通过以下方法触发:
- start():将线程从新建状态转换为就绪状态。
- yield():将线程从运行状态转换为就绪状态。
- sleep():将线程从运行状态转换为超时等待状态。
- wait():将线程从运行状态转换为等待状态。
- notify()/notifyAll():将线程从等待状态转换为就绪状态。
- join():将线程从运行状态转换为等待状态,直到目标线程终止。
- interrupt():将线程从阻塞或等待状态转换为就绪状态。
3.3 状态跃迁图谱
4. 守护线程与用户线程
特性对比
特征项 | 用户线程 | 守护线程 |
---|---|---|
JVM退出条件 | 全部终止 | 不阻止退出 |
默认值 | 否 | 否 |
典型应用 | 业务逻辑 | GC线程 |
异常处理 | 向上传播 | 静默失败 |
4.1 守护线程
守护线程是一种特殊的线程,它在后台运行,为其他线程提供服务。当所有的用户线程结束时,守护线程会自动终止。
java">class DaemonThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("Daemon thread is running");}}
}public class Main {public static void main(String[] args) {DaemonThread daemonThread = new DaemonThread();daemonThread.setDaemon(true);daemonThread.start();System.out.println("Main thread is finished");}
}
特点:
- 守护线程不会阻止JVM退出。
- 守护线程通常用于执行一些后台任务,如垃圾回收、日志记录等。
4.2 用户线程
用户线程是普通的线程,它的生命周期与应用程序的生命周期一致。只有当所有的用户线程结束时,JVM才会退出。
特点:
- 用户线程的执行会影响应用程序的生命周期。
- 用户线程通常用于执行应用程序的核心任务。
5. 线程优先级与调度策略
5.1 优先级失效实验
java">IntStream.rangeClosed(1, 10).forEach(i -> {Thread thread = new Thread(() -> {long count = 0;while (!Thread.interrupted()) {count++;}System.out.println(Thread.currentThread().getName() + ": " + count);});thread.setPriority(i % 2 == 0 ? Thread.MAX_PRIORITY : Thread.MIN_PRIORITY);thread.start();
});
// 观察输出结果的无序性
5.2 线程优先级
Java中的线程优先级分为10个级别,范围从1(最低)到10(最高)。默认情况下,线程的优先级为5。
java">class PriorityThread extends Thread {@Overridepublic void run() {System.out.println("Thread priority: " + getPriority());}
}public class Main {public static void main(String[] args) {PriorityThread thread1 = new PriorityThread();PriorityThread thread2 = new PriorityThread();thread1.setPriority(Thread.MIN_PRIORITY);thread2.setPriority(Thread.MAX_PRIORITY);thread1.start();thread2.start();}
}
注意:线程优先级只是一个提示,具体的调度策略由JVM和操作系统决定。
5.3 线程调度策略
Java的线程调度策略主要依赖于操作系统的调度算法。常见的调度策略包括:
- 时间片轮转调度:每个线程分配一个时间片,时间片用完后切换到下一个线程。
- 优先级调度:高优先级的线程优先执行。
- 抢占式调度:高优先级的线程可以抢占低优先级线程的执行权。
注意:Java的线程调度是非确定性的,开发者不应依赖线程优先级来控制程序的执行顺序。
结语
通过本文的学习,我们详细探讨了Java并发编程中的线程基础,包括进程与线程的区别、线程的创建方式、线程的生命周期、守护线程与用户线程、以及线程优先级与调度策略。掌握这些基础知识是进一步学习并发编程的关键。
在实际开发中,理解并正确使用这些概念可以帮助我们构建高效、稳定的多线程应用。然而,并发编程的复杂性远不止于此,后续我们还将深入探讨线程安全、锁机制、线程通信等高级主题。
参考资料
- Java Concurrency in Practice - Brian Goetz
- Oracle Java Documentation
- Java Threads and the Concurrency Utilities - Jeff Friesen
- Java并发编程实战 - 方腾飞