线程

线程
Cyrex区分程序,线程和进程
项目 | 定义 | 特点 | 示例 |
---|---|---|---|
程序Program | 是一组指令的集合,描述了完成特定任务的步骤和操作,通常以源代码的形式编写。是静态的,本身不具备执行能力。 | 不占用运行时系统资源(内存,CPU等)。没有生命周期的概念,只要存储介质存在就能长期保存。不能被操作系统直接调度执行。 | 一个用Java编写的Hello World程序,保存为.java后缀的文件,在未运行时就是程序。 |
进程Process | 是计算机中正在运行的程序的实例,是程序在其自身地址空间中的一次执行活动。当程序被加载到内存并由操作系统分配资源后开始执行,就形成了进程。 | 资源分配:是资源申请、调度和独立运行的单位,操作系统为其分配内存空间、CPU时间片、文件描述符等系统资源。例如每个进程都有自己独立的虚拟地址空间,用于存放代码、数据等。 动态性:有生命周期,从创建(程序加载执行时创建)开始,到终止(程序执行完毕或异常结束)为止,期间会经历不同状态,如就绪、运行、阻塞。 独立性:不同进程的工作相互独立,拥有各自独立的资源,进程间通常不能直接访问彼此资源。 开销较大:创建、撤销进程以及进程间上下文切换时,系统开销相对较大。 |
运行一个Java程序,操作系统会为其创建一个进程,该进程有自己独立的内存区域,执行程序中的代码逻辑。 |
线程Thread | 是进程中的执行单元,也可看作是进程中独立的控制流。一个进程可以包含多个线程,它们共享进程的内存空间和系统资源,是操作系统调度的基本单位。 | 共享资源:同一进程内的线程共享进程的资源,如代码段、数据段、堆内存等,但每个线程有自己独立的栈空间,用于存放局部变量、方法调用等信息。 轻量级:相较于进程,线程创建、撤销和切换的开销小很多,因为线程不需要像进程那样分配大量独立资源。 并发性:多个线程可并发执行,提高程序执行效率。例如一个浏览器进程中,可同时存在负责页面渲染、网络请求、用户交互等不同功能的多个线程。 受进程制约:线程不能独立存在,必须属于某个进程,一个进程中的线程崩溃可能导致整个进程受影响。 |
在Java多线程编程中,通过继承Thread类或实现Runnable接口创建多个线程,在一个进程内并发执行不同任务。 |
创建线程
在Java中,创建线程常见有以下几种方式:
继承Thread类
通过继承Thread
类并重写其run()
方法来定义线程执行的任务。步骤如下:
- 定义
Thread
类的子类,并重写run()
方法,run()
方法中的代码即为线程执行体。 - 创建
Thread
子类的实例,即线程对象。 - 调用线程对象的
start()
方法启动线程,使其进入就绪状态,等待CPU调度执行run()
方法。
示例代码:
1 | class MyThread extends Thread { |
优点是简单直观,适合初学者理解线程原理;缺点是Java单继承特性限制了该类再继承其他类,扩展性受限。
实现Runnable接口
实现Runnable
接口来定义线程任务,再借助Thread
类启动线程。步骤为:
- 定义实现
Runnable
接口的类,重写run()
方法编写任务逻辑。 - 创建
Runnable
实现类的实例。 - 以该实例为参数创建
Thread
对象,该Thread
对象才是实际执行的线程。 - 调用
Thread
对象的start()
方法启动线程。
1 | class MyRunnable implements Runnable { |
此方式优势在于任务逻辑和线程对象解耦,避免单继承限制,可继承其他类,还能让多个线程共享同一Runnable
实例,适合多线程处理同一份资源;不足是需额外创建Thread
对象。
实现Callable接口
Callable
接口在Java 5引入,类似Runnable
,但call()
方法可返回值和抛出异常。创建步骤:
- 创建实现
Callable
接口的类,实现call()
方法定义线程执行体及返回值。 - 创建
Callable
实现类的实例。 - 用
FutureTask
类包装Callable
对象,FutureTask
实现了Future
和Runnable
接口,能获取call()
方法返回值。 - 以
FutureTask
对象为参数创建Thread
对象并启动。 - 调用
FutureTask
对象的get()
方法获取线程执行结束后的返回值,调用时可能阻塞,直到线程执行完毕。
示例代码:
1 | import java.util.concurrent.Callable; |
适用于线程需返回结果场景,如数据查询、复杂计算;缺点是代码复杂度比Runnable
略高。
使用匿名内部类
可使用匿名内部类创建线程,代码简洁,常用于线程逻辑简单时。
继承Thread
类方式:
1 | public class AnonymousInnerClassThreadExample { |
实现Runnable
接口方式:
1 | public class Main { |
使用Lambda表达式(Java 8及以上版本)
Java 8及后续版本可用Lambda表达式简化线程创建,语法更简洁。
实现Runnable
接口:
1 | public class Main { |
使用线程池结合Lambda表达式:
1 | import java.util.concurrent.ExecutorService; |
使用线程池
线程池是管理线程的机制,可复用线程,减少创建和销毁开销,提高性能和资源利用率。Java通过Executor
框架及ThreadPoolExecutor
类实现线程池创建与管理。以Executors
工具类创建常见线程池示例:
创建固定大小线程池:
1 | import java.util.concurrent.ExecutorService; |
创建可缓存线程池:
1 | import java.util.concurrent.ExecutorService; |
创建单线程线程池:
1 | import java.util.concurrent.ExecutorService; |
创建支持定时及周期性任务执行的线程池:
1 | import java.util.concurrent.Executors; |
线程生命周期
- 新建状态(New):当使用
new
关键字创建一个Thread
对象时,线程就进入了新建状态。此时线程仅仅是在内存中被分配了空间并初始化了一些基本信息,但还没有开始执行。 - 就绪状态(Runnable):调用线程的
start()
方法后,线程进入就绪状态。在这个状态下,线程已经准备好执行,等待操作系统的线程调度器分配 CPU 时间片。 - 运行状态(Running):当线程调度器为处于就绪状态的线程分配了 CPU 时间片后,线程就进入运行状态,开始执行
run()
方法中的代码。 - 阻塞状态(Blocked):在运行过程中,线程可能会因为某些原因(如等待 I/O 操作完成、等待获取对象锁、调用
sleep()
或wait()
方法等)进入阻塞状态。在阻塞状态下,线程暂时停止执行,直到阻塞条件解除。 - 死亡状态(Terminated):线程执行完
run()
方法中的代码,或者因为异常退出run()
方法,线程就进入死亡状态。一旦线程进入死亡状态,就不能再重新启动。
多线程
多线程是指在一个程序中同时运行多个线程,每个线程都可以独立执行不同的任务,从而提高程序的执行效率和响应能力。
基本概念
- 线程:是程序执行的最小单位,是进程中的一个执行路径。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等。
- 多线程:允许在一个程序中并发执行多个线程,每个线程都有自己的执行上下文,包括程序计数器、栈和局部变量等。
优势
- 提高程序的执行效率:可以将一个复杂的任务分解为多个子任务,分别由不同的线程来执行,充分利用多核处理器的并行性,从而加快整个任务的完成速度。
- 增强程序的响应能力:在图形界面应用程序中,使用多线程可以避免界面在执行长时间任务时出现卡顿现象。例如,将文件下载、数据处理等耗时操作放在单独的线程中执行,而主线程则可以继续处理用户界面的交互,提高用户体验。
- 更好地利用系统资源:多线程可以使程序在等待某些操作完成(如I/O操作)时,利用等待时间执行其他任务,从而提高系统资源的利用率。
实现方式
- 继承Thread类:通过创建一个继承自
Thread
类的子类,并重写run()
方法来定义线程的执行逻辑。然后创建该子类的实例,并调用start()
方法启动线程。 - 实现Runnable接口:定义一个实现
Runnable
接口的类,实现run()
方法。将该实现类的实例作为参数传递给Thread
类的构造函数,创建线程对象并启动线程。这种方式比继承Thread
类更灵活,因为它避免了Java单继承的限制,并且可以让多个线程共享同一个Runnable
实例。 - 使用Callable和Future接口:
Callable
接口类似于Runnable
接口,但Callable
的call()
方法可以有返回值,并且可以抛出异常。通过ExecutorService
提交Callable
任务,会返回一个Future
对象,用于获取任务的执行结果。
线程同步与互斥
- 线程同步:当多个线程访问共享资源时,为了保证数据的一致性和完整性,需要对线程的访问进行同步控制。Java中提供了多种线程同步机制,如
synchronized
关键字、ReentrantLock
类等。 - 线程互斥:是指在同一时刻,只允许一个线程访问某个共享资源,其他线程必须等待。通过使用互斥锁来实现线程互斥,确保共享资源在任何时刻都只能被一个线程访问。
线程通信
- 多个线程之间可以通过共享变量、等待 - 通知机制等方式进行通信。例如,使用
wait()
、notify()
和notifyAll()
方法实现线程之间的等待和通知,使得一个线程可以在满足某些条件时通知其他线程继续执行。
线程同步和锁
在多线程编程里,线程同步和锁机制是保障线程安全、避免数据不一致问题的关键技术。
线程同步
概念
线程同步是指在多线程环境下,对多个线程访问共享资源的操作进行协调,保证在同一时刻只有一个线程能够访问共享资源,以此避免多个线程同时操作共享资源引发的数据竞争和不一致问题。
必要性
当多个线程并发访问和修改共享资源时,可能会出现数据不一致的情况。例如,多个线程同时对一个计数器进行自增操作,若没有同步机制,就可能会出现计数错误。
实现方式
synchronized 关键字:这是 Java 内置的线程同步机制,可用于修饰方法或代码块,在方法声明中使用synchronized关键字,同一时刻仅允许一个线程访问该方法。
1 | public class SynchronizedExample { |
1 | //使用synchronized关键字修饰代码块时需要指定一个对象作为锁 |
锁 Lock
概念
Lock
是 Java 5 引入的一个接口,它提供了比 synchronized
关键字更灵活、更强大的线程同步机制。Lock
接口定义了获取锁和释放锁的方法,允许开发者手动控制锁的获取和释放。
常用实现类
ReentrantLock:可重入锁,这是 Lock
接口的一个常用实现类。它支持与 synchronized
相同的语义,并且提供了更多的功能,如可中断的锁获取、超时锁获取等。
1 | import java.util.concurrent.locks.Lock; |
ReadWriteLock:读写锁,它维护了一对锁,一个读锁和一个写锁。多个线程可以同时获取读锁,但在写锁被获取时,其他线程不能获取读锁或写锁。读写锁适用于读多写少的场景。
1 | import java.util.concurrent.locks.ReadWriteLock; |
Lock
与 synchronized
的比较
- 灵活性:
Lock
接口提供了更灵活的锁获取和释放方式,例如可中断的锁获取、超时锁获取等,而synchronized
关键字的锁获取和释放是隐式的,缺乏这种灵活性。 - 性能:在高并发场景下,
Lock
接口的性能通常优于synchronized
关键字,因为Lock
接口可以通过一些优化策略来减少锁的竞争。 - 可重入性:
synchronized
和ReentrantLock
都支持可重入性,但Lock
接口的可重入性更加灵活,可以设置重入次数等。 - 锁的公平性:
ReentrantLock
可以设置为公平锁,即按照线程请求锁的顺序来分配锁,而synchronized
关键字是非公平锁。