一、基础知识
在 Java 中,线程是程序执行的基本单元。多线程编程允许程序同时执行多个任务,提高了程序的效率和性能。在学习多线程编程之前,需要了解以下基本概念:
-
线程:线程是执行代码的独立路径。一个程序可以同时执行多个线程,每个线程都有自己的执行栈和执行路径。
-
并发:指的是多个线程在同一时间段内执行,实现任务的并行执行。
-
同步:指的是协调多个线程的执行顺序,防止出现数据竞争和不一致的情况。
线程生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
- 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
进程和线程的区别
常见面试题
它们分别代表了任务和执行单元。进程是程序的执行实例,拥有独立的地址空间和资源;线程是进程内的执行单元,共享进程的资源,可以并发执行任务。进程切换开销较大,但进程之间隔离性好;线程切换开销较小,但线程之间通信和同步相对复杂。选择进程还是线程取决于具体的应用场景和需求。
1. 定义
-
进程:进程是程序的执行实例,是系统进行资源分配和调度的基本单位。每个进程都有自己的独立地址空间,包括代码、数据、堆栈等,可以运行在独立的内存空间中。
-
线程:线程是进程中的一个执行路径,是程序执行的最小单位。一个进程可以包含多个线程,它们共享相同的内存空间和资源,可以并发执行任务。
2. 资源分配
-
进程:每个进程都拥有独立的地址空间和资源,包括内存、文件句柄、设备等。进程之间的通信需要使用进程间通信(IPC)机制,如管道、信号、共享内存等。
-
线程:线程是进程内的一个执行单元,共享进程的地址空间和资源。线程之间可以直接访问共享的内存,因此线程间通信更加简单高效,但也更容易出现竞争和同步问题。
3. 调度和切换
-
进程:进程切换涉及到内存上下文的切换,需要保存和恢复进程的状态信息,因此进程切换开销较大。
-
线程:线程切换只涉及到线程的上下文切换,通常只需要保存和恢复寄存器的状态信息,因此线程切换开销较小。
4. 创建和销毁
-
进程:创建和销毁进程的开销较大,需要分配和释放大量的系统资源,包括内存、文件描述符等。
-
线程:创建和销毁线程的开销较小,因为线程共享进程的资源,只需要分配和释放一些线程私有的资源。
5. 并发性和可扩展性
-
进程:进程之间的通信和同步相对复杂,进程间切换开销较大,因此进程级别的并发性和可扩展性较差。
-
线程:线程间通信和同步相对简单高效,线程切换开销较小,因此线程级别的并发性和可扩展性较好。
二、线程创建和管理
在 Java 中,线程的创建和管理有几种常见的方式:
1. 继承 Thread 类
通过继承 Thread
类并重写 run()
方法来创建线程。以下是创建线程的基本步骤:
步骤:
- 创建一个类,继承自
Thread
类。 - 在该类中重写
run()
方法,定义线程要执行的任务。 - 创建该类的实例,并调用
start()
方法启动线程。
示例:
public class MyThread extends Thread {
public void run() {
// 定义线程执行的任务
System.out.println("Thread is running...");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
2. 实现 Runnable 接口
通过实现 Runnable
接口并将其传递给 Thread
类的构造函数来创建线程。以下是创建线程的基本步骤:
步骤:
- 创建一个类,实现
Runnable
接口。 - 在该类中实现
run()
方法,定义线程要执行的任务。 - 创建
Thread
类的实例,将实现了Runnable
接口的类作为参数传递给Thread
类的构造函数。 - 调用
start()
方法启动线程。
示例
public class MyRunnable implements Runnable {
public void run() {
// 定义线程执行的任务
System.out.println("Thread is running...");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
这两种方法都可以创建线程,并在 Java 中非常常用。通常来说,推荐使用实现 Runnable
接口的方式,因为它避免了单继承的限制,并且使得类的设计更加灵活。
3. 实现 Callable
接口
实现 Callable
接口创建线程与实现 Runnable
接口类似,但是 Callable
接口的 call()
方法可以返回一个结果或抛出一个异常。需要通过 ExecutorService
的 submit()
方法提交 Callable
对象来执行,并可以获得 Future
对象来获取线程执行的结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MyCallable implements Callable<String> {
public String call() throws Exception {
return "Callable thread is running...";
}
public static void main(String[] args) {
// 创建一个 ExecutorService 实例
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交 Callable 对象给 ExecutorService 执行
Future<String> future = executor.submit(new MyCallable());
try {
// 获取线程执行的结果
String result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭 ExecutorService
executor.shutdown();
}
}
日常工作的实际使用,会在项目里创建线程池,统一管理。可以提高系统稳定性。
三、线程同步和通信
在多线程编程中,需要特别注意线程之间的同步和通信问题,以避免数据竞争和死锁等并发问题。
-
同步块和锁:使用
synchronized
关键字或ReentrantLock
类来创建同步块,确保多个线程对共享资源的访问是安全的。 -
线程通信:使用
wait()
、notify()
和notifyAll()
方法来实现线程之间的通信,以便在多个线程之间进行协调和同步。
线程同步(Thread Synchronization)
线程同步是指多个线程之间对共享资源的访问进行协调和控制,以避免数据竞争和并发访问导致的不确定性结果。常见的线程同步方式包括:
- Synchronized 关键字:使用
synchronized
关键字对关键代码块或方法进行同步,确保同一时刻只有一个线程可以访问共享资源。
public synchronized void synchronizedMethod() {
// 同步的代码块
}
- Lock 接口:使用
Lock
接口及其实现类如ReentrantLock
进行显式锁定和解锁,提供了更灵活和精细的线程同步控制。
Lock lock = new ReentrantLock();
lock.lock();
try {
// 同步的代码块
} finally {
lock.unlock();
}
线程通信(Thread Communication)
线程通信是指多个线程之间通过共享对象来进行信息交换和协作,实现线程之间的互斥执行和协同工作。常见的线程通信方式包括:
- wait()、notify() 和 notifyAll() 方法:通过
wait()
方法使当前线程进入等待状态,通过notify()
或notifyAll()
方法唤醒等待的线程。
synchronized (sharedObject) {
while (condition) {
sharedObject.wait();
}
// 执行任务
sharedObject.notify();
}
- Condition 接口:使用
Condition
接口及其实现类如ReentrantLock
的newCondition()
方法来实现更灵活的线程通信。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (condition) {
condition.await();
}
// 执行任务
condition.signal();
} finally {
lock.unlock();
}
注意事项:
-
避免死锁:合理设计同步和通信机制,避免出现死锁现象。
-
避免饥饿和活锁:合理设计线程调度策略,避免线程饥饿和活锁问题。
-
精细控制同步范围:尽可能减小同步代码块的范围,以减少同步的开销和提高程序的并发性能。
-
合理使用线程间通信:根据实际需求选择合适的线程通信方式,避免不必要的线程唤醒和等待。
四. 线程池(Thread Pools)
使用线程池可以有效地管理和重用线程,提高系统的稳定性和性能。线程池可以减少线程的创建和销毁开销,避免频繁地创建和销毁线程所带来的性能损耗,并且可以限制系统中并发线程的数量,防止因过多线程而导致系统资源耗尽或系统负载过高的问题。
线程池实现方式
1. ThreadPoolExecutor
ExecutorService executor = new ThreadPoolExecutor(
5, // 核心线程池大小
10, // 最大线程池大小
60, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>() // 任务队列
);
executor.submit(() -> {
// 执行任务的操作
});
2. Executors 工具类
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
// 执行任务的操作
});
3. ScheduledExecutorService
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
// 定时执行任务的操作
}, 0, 1, TimeUnit.MINUTES);
4.自定义线程池
ExceptionCatchThreadPoolExecutor
//定义一个名为 "asyncExecutor" 的线程池 bean
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
//15:线程池的核心线程数,表示线程池中同时能够运行的线程数量。
//30:线程池的最大线程数,表示线程池中最多能够拥有的线程数量。
//200:空闲线程存活时间,表示当线程池中的线程数量超过核心线程数时,空闲线程的存活时间
//TimeUnit.SECONDS:空闲线程存活时间的时间单位。
//new LinkedBlockingQueue<>(200):任务队列,用于存放等待执行的任务。这里使用了一个容量为 200 的链表阻塞队列作为任务队列。
//(runnable) -> new Thread(runnable, "activity_async_task_executor"):线程工厂,用于创建新的线程。这里使用一个 lambda 表达式来创建线程,线程的名称为 "activity_async_task_executor"。
return new ExceptionCatchThreadPoolExecutor(15, 30, 200, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200),
(runnable) -> new Thread(runnable, "activity_async_task_executor"),
new ThreadPoolExecutor.DiscardOldestPolicy());
}
注意事项:
-
资源管理:使用完线程池后及时关闭,释放资源,避免资源泄漏和浪费。
-
性能调优:合理配置线程池的大小、任务队列等参数,以达到最佳的性能和资源利用率。
-
异常处理:在任务执行过程中可能会抛出异常,需要及时捕获并处理。
-
线程安全性:线程池通常会涉及到共享的资源和状态,需要保证线程安全。