一、进程
1. 进程
进程是程序的一次动态执行过程,是操作系统资源分配的基本单位。
2. 进程和线程的区别
特性 | 进程 | 线程 |
---|---|---|
定义 | 独立运行的程序实例,资源分配的基本单位 | 进程中的一个执行单元,CPU调度的基本单位 |
资源 | 进程拥有独立的内存空间和资源 | 线程共享进程的堆和方法区(JDK1.8之后为元空间) |
通信 | 进程间通信(IPC)较为复杂 | 线程间通信(共享内存)较为简单 |
开销 | 创建和销毁进程开销较大 | 创建和销毁线程开销较小 |
独立性 | 进程之间相对独立 | 线程间相互影响 |
并发性 | 进程可以并发执行 | 线程可以并发执行 |
调度 | 由操作系统调度 | 由操作系统或线程库调度 |
崩溃影响 | 一个进程崩溃不会影响其他进程 | 一个线程崩溃可能会影响整个进程 |
3. 进程和线程的联系
联系 | 描述 |
---|---|
组成关系 | 一个进程可以包含一个或多个线程,线程是进程的一部分,多个线程共享进程的资源。 |
资源共享 | 线程共享进程的堆和方法区(JDK1.8之后为元空间)、文件句柄等资源,进程则有自己的独立资源。 |
并发执行 | 进程和线程都可以并发执行,利用多核 CPU 提高程序的并行度。 |
调度 | 进程和线程都由操作系统进行调度,多线程程序中,线程的调度可以由 JVM 和操作系统共同管理。 |
二、线程
1. Java线程
- JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程);JDK1.2之后Java线程是基于原生线程(Native Threads,操作系统内核线程)实现。
- 虚拟机栈和本地方法栈线程私有是为了保证局部变量不被其他线程访问
- 程序技术器线程私有是为了线程切换后能找到上次运行的位置继续执行
2. 线程的创建方式
- 继承Thread类并重写 run 方法来定义线程的执行逻辑。
bash
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
- 实现Runable接口并将其实例传递给 Thread 对象
bash
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
- 实现Callable接口并使用 FutureTask 包装 Callable 对象,然后将其传递给 Thread 对象(Callable 可以有返回值,且可以抛出异常)
bash
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable is running";
}
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start(); // 启动线程
try {
// 获取执行结果
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
- 其他方式:使用线程池、使用CompletableFuture类
3. 线程(Thread类)的常见方法
方法名 | 描述 |
---|---|
void start() |
启动线程,调用线程的 run 方法 |
void run() |
线程的执行方法,需要重写 |
void interrupt() |
中断线程 |
boolean isInterrupted() |
测试线程是否已经中断 |
static boolean interrupted() |
测试当前线程是否已经中断,并清除当前线程的中断状态 |
void join() |
等待线程终止 |
void join(long millis) |
等待线程终止最长时间为 millis 毫秒 |
void join(long millis, int nanos) |
等待线程终止最长时间为 millis 毫秒加 nanos 纳秒 |
static void sleep(long millis) |
使当前线程睡眠(暂停执行)指定的毫秒数 |
static void sleep(long millis, int nanos) |
使当前线程睡眠(暂停执行)指定的毫秒数加纳秒数 |
void setPriority(int newPriority) |
更改线程的优先级 |
int getPriority() |
返回线程的优先级 |
void setName(String name) |
更改线程名称 |
String getName() |
返回线程名称 |
long getId() |
返回线程的唯一标识符 |
Thread.State getState() |
返回线程的状态 |
boolean isAlive() |
测试线程是否还活着 |
static void yield() |
暂停当前正在执行的线程对象,并执行其他线程 |
static Thread currentThread() |
返回对当前正在执行的线程对象的引用 |
static int activeCount() |
返回当前线程的线程组中活动线程的数目 |
static void dumpStack() |
将当前线程的堆栈跟踪打印到标准错误流 |
StackTraceElement[] getStackTrace() |
返回一个数组,表示该线程的堆栈转储 |
static boolean holdsLock(Object obj) |
当且仅当当前线程在指定的对象上保持监视器锁时,返回 true |
void setDaemon(boolean on) |
将该线程标记为守护线程或用户线程 |
boolean isDaemon() |
测试该线程是否为守护线程 |
void checkAccess() |
判断当前运行的线程是否有权限修改此线程 |
ThreadGroup getThreadGroup() |
返回该线程所属的线程组 |
4. 线程的生命周期
- New (新建状态):线程对象被创建,但还未调用 start() 方法。
- Runnable (就绪状态):start() 方法被调用,线程进入就绪状态,等待 CPU 时间片的分配。
- Running (运行状态):线程获得 CPU 时间片,开始执行 run() 方法中的代码。
- Blocked (阻塞状态):线程因等待资源或锁而进入阻塞状态,无法继续执行。
- Waiting (等待状态):线程等待另一个线程显式地唤醒自己,通过 wait()、join() 或 sleep() 等方法进入等待状态。
- Timed Waiting (计时等待状态):线程等待一定时间后会被自动唤醒,通过 sleep(long millis)、wait(long timeout) 或 join(long millis) 等方法进入计时等待状态。
- Terminated (终止状态):线程运行结束或因异常退出 run() 方法,线程进入终止状态
以下是线程生命周期的图解:
(1)JDK1.5之前
(2)JDK1.5之后
三、多线程
1. 线程安全问题的解决方式
(1)产生原因
- 共享资源:多个线程同时访问和修改同一资源,例如变量、对象、文件等。
- 缺乏同步:线程在访问共享资源时,没有正确使用同步机制,导致多个线程同时执行对共享资源的操作。
- 原子性操作的缺乏:对共享资源的操作需要分多个步骤完成,如果这些步骤不能保证原子性,会导致线程安全问题。
- 可见性问题:一个线程对共享资源的修改,其他线程不能立即看到,导致数据不一致。
- 指令重排序:编译器和处理器为了优化性能,可能会对指令进行重排序,导致线程安全问题
(2)解决方式
- Synchronized关键字:同步块可以确保在同一时间只有一个线程执行同步代码,从而避免多个线程同时访问共享资源的问题。(详解请参考)
bash
public class SynchronizedExample {
public synchronized void synchronizedMethod() {
// 同步实例方法
// 其他线程不能同时执行此方法
}
public static synchronized void staticSynchronizedMethod() {
// 同步静态方法
// 其他线程不能同时执行此静态方法
}
public void synchronizedBlock() {
synchronized (this) {
// 同步代码块
// 锁定当前实例对象
}
}
}
- Lock锁:是 Java 提供的一种显式锁机制,有多种锁类型实现。
bash
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// 临界区代码
// 其他线程不能同时执行此代码
} finally {
lock.unlock();
}
}
}
- 使用线程本地变量 (ThreadLocal):每个线程都有自己的变量副本,互不干扰。
bash
public class ThreadLocalExample {
private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void increment() {
threadLocal.set(threadLocal.get() + 1);
}
public int get() {
return threadLocal.get();
}
}
- Atomic Variables(原子变量):java.util.concurrent.atomic 包提供了多种原子变量,如 AtomicInteger、AtomicLong、AtomicReference 等,它们提供了一种无锁的线程安全机制。
bash
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
public int getCounter() {
return counter.get();
}
}
(3) Synchronized 和 Lock 的区别
特性 | synchronized |
Lock |
---|---|---|
实现 | 内置语言特性 | 通过 java.util.concurrent.locks 包提供 |
锁的释放 | 自动释放:线程退出同步代码块或方法时自动释放 | 需要显式调用 unlock() 方法 |
灵活性 | 灵活性较低,只能锁定方法或代码块 | 灵活性较高,可以尝试获取锁、定时获取锁等 |
锁的获取 | 线程阻塞式等待 | 支持阻塞式、非阻塞式、定时尝试获取锁 |
性能 | 较低:适用于简单的同步 | 较高:适用于复杂的并发控制 |
条件变量 | 无 | 提供 Condition 类,支持多个条件变量 |
中断响应 | 不支持线程中断 | 支持线程中断,响应中断请求 |
读写锁 | 不支持 | 支持,通过 ReentrantReadWriteLock 实现 |
公平锁 | 不支持 | 支持公平锁,通过 ReentrantLock 实现 |
2. 死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
(1)死锁的产生:四个必要条件
- 非抢占式:线程已获得的资源在未使用完之前不能被其他线程强行剥夺
- 循环等待:若干线程之间形成一种头尾相接的循环等待资源关系
- 互斥条件:该资源任意一个时刻只由一个线程占用
- 请求与保持条件:个线程因请求资源而阻塞时,对已获得的资源保持不放
(2)死锁的预防:破坏必要条件
- 破坏请求与保持条件:一次性申请所有的资源(会造成内存开销极大,因为程序可能很长一段时间使用不到该资源)。
- 破坏非抢占式条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:按某一顺序申请资源,释放资源则反序释放。
(3)死锁的避免
- 在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
- 安全状态:系统能够按照某种线程推进顺序(P1、P2、P3......Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。
- 银行家算法代码实现
bash
import java.util.Arrays;
public class BankersAlgorithm {
private int numProcesses;
private int numResources;
private int[] available;
private int[][] maximum;
private int[][] allocation;
private int[][] need;
public BankersAlgorithm(int numProcesses, int numResources) {
this.numProcesses = numProcesses;
this.numResources = numResources;
this.available = new int[numResources];
this.maximum = new int[numProcesses][numResources];
this.allocation = new int[numProcesses][numResources];
this.need = new int[numProcesses][numResources];
}
public void setAvailable(int[] available) {
System.arraycopy(available, 0, this.available, 0, numResources);
}
public void setMaximum(int process, int[] max) {
System.arraycopy(max, 0, this.maximum[process], 0, numResources);
for (int j = 0; j < numResources; j++) {
this.need[process][j] = this.maximum[process][j] - this.allocation[process][j];
}
}
public void setAllocation(int process, int[] alloc) {
System.arraycopy(alloc, 0, this.allocation[process], 0, numResources);
for (int j = 0; j < numResources; j++) {
this.need[process][j] = this.maximum[process][j] - this.allocation[process][j];
}
}
public boolean requestResources(int process, int[] request) {
// Step 1: Check if request <= need
for (int j = 0; j < numResources; j++) {
if (request[j] > need[process][j]) {
return false; // Request exceeds need
}
}
// Step 2: Check if request <= available
for (int j = 0; j < numResources; j++) {
if (request[j] > available[j]) {
return false; // Request exceeds available resources
}
}
// Step 3: Pretend to allocate requested resources
for (int j = 0; j < numResources; j++) {
available[j] -= request[j];
allocation[process][j] += request[j];
need[process][j] -= request[j];
}
// Step 4: Check system safety
if (checkSafety()) {
return true; // Safe state, allocation is successful
} else {
// Revert allocation if not safe
for (int j = 0; j < numResources; j++) {
available[j] += request[j];
allocation[process][j] -= request[j];
need[process][j] += request[j];
}
return false; // Not a safe state
}
}
private boolean checkSafety() {
boolean[] finish = new boolean[numProcesses];
int[] work = Arrays.copyOf(available, numResources);
while (true) {
boolean foundProcess = false;
for (int i = 0; i < numProcesses; i++) {
if (!finish[i]) {
boolean canAllocate = true;
for (int j = 0; j < numResources; j++) {
if (need[i][j] > work[j]) {
canAllocate = false;
break;
}
}
if (canAllocate) {
for (int j = 0; j < numResources; j++) {
work[j] += allocation[i][j];
}
finish[i] = true;
foundProcess = true;
}
}
}
if (!foundProcess) {
break;
}
}
for (boolean f : finish) {
if (!f) {
return false; // System is not in a safe state
}
}
return true; // System is in a safe state
}
public static void main(String[] args) {
int numProcesses = 5;
int numResources = 3;
BankersAlgorithm ba = new BankersAlgorithm(numProcesses, numResources);
ba.setAvailable(new int[]{10, 5, 7});
ba.setMaximum(0, new int[]{7, 5, 3});
ba.setMaximum(1, new int[]{3, 2, 2});
ba.setMaximum(2, new int[]{9, 0, 2});
ba.setMaximum(3, new int[]{2, 2, 2});
ba.setMaximum(4, new int[]{4, 3, 3});
ba.setAllocation(0, new int[]{0, 1, 0});
ba.setAllocation(1, new int[]{2, 0, 0});
ba.setAllocation(2, new int[]{3, 0, 2});
ba.setAllocation(3, new int[]{2, 1, 1});
ba.setAllocation(4, new int[]{0, 0, 2});
int[] request = {1, 0, 2};
int process = 1;
boolean success = ba.requestResources(process, request);
System.out.println("Request " + (success ? "granted" : "denied"));
}
}
3. 线程池
(1)核心参数
- 核心线程数 (corePoolSize):线程池中保持活动的最小线程数量,即使这些线程处于空闲状态。
- 最大线程数 (maximumPoolSize):线程池中允许的最大线程数量。当任务队列已满且已达到核心线程数时,线程池会创建新的线程来处理任务,直到达到最大线程数。达到最大线程数后,新任务将被拒绝处理,并根据饱和策略进行处理。
- 空闲线程存活时间 (keepAliveTime):当线程池中线程数量超过核心线程数时,多余的空闲线程在等待新任务时的最长存活时间。超过这个时间的空闲线程将被终止和移除,直到线程池中的线程数量等于核心线程数。
- 时间单位 (unit):空闲线程存活时间的单位,如秒、毫秒等。与 keepAliveTime 参数一起使用。
- 任务队列 (workQueue):用于保存等待执行任务的阻塞队列。常用的队列实现有:
- 直接提交队列 (SynchronousQueue):不保存任务,每个插入操作必须等待相应的删除操作。
- 有界队列 (ArrayBlockingQueue):有固定容量的队列,当队列满时,插入操作将被阻塞。
- 无界队列 (LinkedBlockingQueue):队列大小没有上限,理论上可以无限制地增加队列长度。
- 优先队列 (PriorityBlockingQueue):按任务优先级排序的无界队列。
- 线程工厂 (threadFactory):用于创建新线程的工厂。通过自定义线程工厂,可以为每个新线程设置名称、优先级等属性。
- 拒绝策略 (handler):当任务队列已满且线程池中的线程数量已达到最大线程数时,如何处理新任务。Java 提供了四种预定义的拒绝策略:
- AbortPolicy(默认):抛出 -
- RejectedExecutionException,拒绝任务。
- CallerRunsPolicy:由调用线程处理该任务。
- DiscardPolicy:丢弃无法处理的任务,不予处理。
- DiscardOldestPolicy:丢弃最早添加到队列中的任务,然后尝试重新提交新任务。
(2)线程池创建
- 固定大小的线程池 (Fixed Thread Pool)
bash
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
- 单线程化的线程池 (Single Thread Executor)
bash
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- 缓存的线程池 (Cached Thread Pool)
bash
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- 定时任务线程池 (Scheduled Thread Pool)
bash
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);