1.1 进程与线程的概念
// 进程示例:启动多个记事本进程(这里只是模拟,实际可能需要系统调用)
public class ProcessExample {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
try {
// 这里的命令在不同操作系统下可能不同
ProcessBuilder pb = new ProcessBuilder("notepad.exe");
Process p = pb.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 线程示例
public class ThreadExample {
public static void main(String[] args) {
// 创建一个线程对象
Thread thread = new Thread(() -> {
System.out.println("线程执行中");
});
// 启动线程
thread.start();
}
}
-
进程
- 定义:程序由指令和数据组成,进程是用来加载指令、管理内存、管理 I/O 的实体,是程序的一个实例。当一个程序被运行,从磁盘加载程序代码至内存时,就开启了一个进程。大部分程序可同时运行多个实例进程,也有部分程序只能启动一个实例进程。
- 示例:记事本、画图、浏览器等程序可同时运行多个实例进程;网易云音乐、360 安全卫士等程序通常只能启动一个实例进程。
-
线程
- 定义:一个进程之内可分为一到多个线程,一个线程是一个指令流,将指令流中的指令按顺序交给 CPU 执行。在 Java 中,线程是最小调度单位,进程是资源分配的最小单位,在 Windows 中进程是线程的容器。
- 对比:进程相互独立,拥有共享资源(如内存空间)供内部线程共享,进程间通信复杂(同一计算机的进程通信称为 IPC,不同计算机之间的进程通信需通过网络并遵守协议);线程存在于进程内,通信相对简单(因为共享进程内的内存),且线程更轻量,线程上下文切换成本一般低于进程上下文切换成本。
1.2 并行与并发
// 并发示例:单核CPU下模拟多个线程轮流执行
public class ConcurrencyExample {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; i < 3; j++) {
System.out.println(Thread.currentThread().getName() + " 执行中,计数:" + j);
try {
// 模拟线程执行一段时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程" + i).start();
}
}
}
// 并行示例:多核CPU下利用多线程并行执行任务
public class ParallelismExample {
public static void main(String[] args) {
// 获取CPU核心数
int numCores = Runtime.getRuntime().availableProcessors();
Thread[] threads = new Thread[numCores];
for (int i = 0; i < numCores; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 3; j++) {
System.out.println(Thread.currentThread().getName() + " 执行中,计数:" + j);
}
}, "核心线程" + i);
}
for (Thread thread : threads) {
thread.start();
}
}
}
-
并发
- 定义:在单核 CPU 下,由于操作系统的任务调度器将 CPU 的时间片(如 Windows 下时间片最小约为 15 毫秒)分给不同的程序使用,虽然线程实际是串行执行,但因 CPU 在线程间的切换非常快,人类感觉是同时运行的,这种线程轮流使用 CPU 的做法称为并发。
- 示例:可以类比家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这些事,就是并发的体现。
-
并行
- 定义:在多核 CPU 下,每个核都可以调度运行线程,此时多个线程可以真正同时运行,这种情况称为并行。
- 示例:家庭主妇雇了保姆一起做事,或者雇了多个保姆分别负责不同任务(如一个专做饭、一个专打扫卫生、一个专喂奶),此时既有并发也有并行。如果涉及到共享资源(如锅只有一口),则可能会产生竞争。
1.3 应用
// 异步调用示例
public class AsyncCallExample {
public static void main(String[] args) {
// 创建一个新线程执行耗时操作
new Thread(() -> {
try {
System.out.println("开始执行耗时操作");
// 模拟耗时操作,这里睡眠3秒
Thread.sleep(3000);
System.out.println("耗时操作完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("主线程继续执行其他任务");
}
}
// 提高效率示例
public class EfficiencyImprovementExample {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
// 获取CPU核心数
int numCores = Runtime.getRuntime().availableProcessors();
Thread[] threads = new Thread[numCores];
for (int i = 0; i < numCores; i++) {
final int index = i;
threads[i] = new Thread(() -> {
int sum = 0;
for (int j = 0; j < 100000000 / numCores; j++) {
sum += index + j;
}
System.out.println(Thread.currentThread().getName() + " 计算结果:" + sum);
}, "核心线程" + index);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("总耗时:" + (endTime - startTime) + " 毫秒");
}
}
-
异步调用
- 设计思路:在单核 CPU 下,如果没有线程调度机制,某些耗时操作(如读取磁盘文件)会导致 CPU 在操作期间无法执行其他代码。多线程可以使方法执行变为异步,避免阻塞主线程或其他关键线程。
- 示例场景:在项目中,视频文件转换格式、tomcat 的异步 servlet 处理耗时操作、ui 程序中开线程进行其他操作等,都是通过多线程实现异步调用,提高程序的响应性。
-
提高效率
- 设计思路:充分利用多核 CPU 的优势,将计算任务拆分到多个线程并行执行。对于多个独立的计算任务,通过合理设计,让不同核心分别执行不同线程的计算任务,从而缩短总运行时间。
- 示例场景:假设有 3 个计算任务,分别花费 10ms、11ms 和 9ms,汇总需要 1ms。在单核 CPU 下串行执行需要 31ms,而在四核 CPU 下,各个核心分别使用线程执行不同计算任务,由于并行执行,总时间只取决于最长的那个线程运行时间(11ms)加上汇总时间,即 12ms。需要注意的是,单核 CPU 下多线程不能实际提高程序运行效率,只是实现任务切换;并且并非所有计算任务都适合拆分并行执行,同时 IO 操作通常不占用 CPU,但可能因阻塞等待而未充分利用线程,后续有相关优化方法(如非阻塞 IO 和异步 IO)。
2. Java 线程
-
创建和运行线程
// 方法一:直接使用Thread类 public class ThreadCreationExample1 { public static void main(String[] args) { Thread thread = new Thread() { @Override public void run() { System.out.println("直接使用Thread类创建的线程执行"); } }; thread.start(); } } // 方法二:使用Runnable配合Thread public class ThreadCreationExample2 { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { System.out.println("使用Runnable配合Thread创建的线程执行"); } }; Thread thread = new Thread(runnable); thread.start(); } } // 方法三:FutureTask配合Thread import java.util.concurrent.FutureTask; public class ThreadCreationExample3 { public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask<>(() -> { System.out.println("使用FutureTask配合Thread创建的线程执行任务"); return 42; }); Thread thread = new Thread(futureTask); thread.start(); try { Integer result = futureTask.get(); System.out.println("结果:" + result); } catch (Exception e) { e.printStackTrace(); } } }
- 方法一:直接使用 Thread
- 示例代码:创建一个匿名内部类继承自 Thread 类,重写 run 方法定义线程要执行的任务,然后通过 start 方法启动线程。例如:
- 方法一:直接使用 Thread
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
t.start();
- 方法二:使用 Runnable 配合 Thread
- 示例代码:将线程要执行的任务定义在 Runnable 接口的实现类中,然后将 Runnable 对象作为参数传递给 Thread 类的构造函数创建线程对象,再启动线程。这种方式将线程和任务分开,更灵活,也更容易与线程池等高级 API 配合。例如:
Runnable runnable = new Runnable() {
public void run() {
// 要执行的任务
}
};
Thread t = new Thread(runnable);
t.start();
- 方法三:FutureTask 配合 Thread
- 示例代码:FutureTask 能够接收 Callable 类型的参数,用于处理有返回结果的情况。首先创建一个 FutureTask 对象,将实现了 Callable 接口的匿名内部类作为参数传递给 FutureTask 的构造函数,在 Callable 的 call 方法中定义任务并返回结果。然后将 FutureTask 对象作为参数传递给 Thread 类的构造函数创建线程并启动。主线程可以通过 FutureTask 的 get 方法同步等待任务执行完毕并获取结果。例如:
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
new Thread(task3, "t3").启动线程对象(new Thread(task3, "t3")).start();
Integer result = task3.get();
log.debug("结果是:{}", result);
-
观察多个线程同时运行
public class MultipleThreadsExample { public static void main(String[] args) { Thread thread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程1执行,计数:" + i); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程2执行,计数:" + i); } }); thread1.start(); thread2.start(); } }
- 关键理解:多个线程同时运行时,它们是交替执行的,线程执行的先后顺序不由我们控制。
-
查看进程线程的方法
- Windows 系统:任务管理器可查看进程和线程数,也可用于杀死进程;tasklist 查看进程,taskkill 杀死进程。
- Linux 系统:ps -fe 查看所有进程,ps -fT -p <PID>查看某个进程的所有线程,kill 杀死进程,top 按大写 H 切换是否显示线程,top -H -p <PID>查看某个进程的所有线程。
- Java 相关命令:jps 命令查看所有 Java 进程,jstack <PID>查看某个 Java 进程的所有线程状态,jconsole 图形界面查看某个 Java 进程中线程的运行情况(需要进行相关配置,如指定远程监控参数等)。
-
原理之线程运行
- 栈与栈帧:Java Virtual Machine Stacks 为每个线程分配栈内存,栈由多个栈帧组成,每个栈帧对应一次方法调用所占用的内存,每个线程只有一个活动栈帧,对应当前正在执行的方法。
- 线程上下文切换:因多种原因(如 CPU 时间片用完、垃圾回收、高优先级线程需要运行、线程自身调用某些方法等)导致 CPU 切换执行线程时,需要保存当前线程状态并恢复另一个线程状态,Java 中通过程序计数器实现。频繁的上下文切换会影响性能。
-
常见方法
- 介绍了 Thread 类的多个常见方法,如 start 用于启动线程(每个线程对象的启动方法 start 只能调用一次),run 是线程启动后执行的方法(可通过继承 Thread 或实现 Runnable 来定义),join 用于等待线程运行结束(可设置等待时间),getid 获取线程 ID,getName 和 setName 用于获取和设置线程名,getPriority 和 setPriority 用于获取和设置线程优先级,getState 获取线程状态,islnterrupted 判断是否被打断(不清除打断标记),isAlive 判断线程是否存活,interrupt 用于打断线程(根据线程状态有不同行为),interrupted 判断当前线程是否被打断(会清除打断标记),currentThread 获取当前正在执行的线程,sleep 让当前线程休眠,yield 提示线程调度器让出 CPU 使用权等。同时说明了各个方法的注意事项和功能细节。
- start 与 run
// start方法启动线程示例 public class StartExample { public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("线程执行中,当前线程:" + Thread.currentThread().getName()); }, "新线程"); thread.start(); System.out.println("主线程继续执行"); } } // 直接调用run方法示例 public class RunExample { public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("线程执行中,当前线程:" + Thread.currentThread().getName()); }, "新线程"); thread.run(); System.out.println("主线程继续执行"); } }
- 区别:直接调用 run 方法是在主线程中执行了 run 方法的代码,没有启动新线程;而使用 start 方法是启动新线程,新线程会间接执行 run 方法中的代码。通过示例代码展示了两种方式的不同执行结果和行为。
- sleep 与 yield
// sleep方法示例 public class SleepExample { public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("线程开始执行"); try { // 线程睡眠3秒 Thread.sleep(3000); System.out.println("线程唤醒后继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); } } // yield方法示例 public class YieldExample { public static void main(String[] args) { Thread thread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程1执行,计数:" + i); if (i == 2) { // 线程1让出CPU使用权 Thread.yield(); } } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程2执行,计数:" + i); } }); thread1.start(); thread2.start(); } }
- sleep:调用 sleep 会使当前线程从 Running 进入 Timed Waiting 状态(阻塞),其他线程可通过 interrupt 方法打断正在睡眠的线程,睡眠结束后的线程未必会立刻得到执行,建议使用 TimeUnit 的 sleep 代替 Thread 的 sleep 以提高可读性。
- yield:调用 yield 会使当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其他线程,其具体实现依赖于日前操作系统的任务调度器。线程优先级会提示调度器优先调度该线程,但不是绝对的,在 CPU 忙时优先级高的线程会获得更多时间表片,闲时作用不大。
- join 方法详解
// 单个线程join示例 public class JoinSingleExample { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { try { System.out.println("子线程开始执行"); // 子线程睡眠2秒 Thread.sleep(2000); System.out.println("子线程执行结束"); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); System.out.println("主线程等待子线程执行结束"); thread.join(); System.out.println("主线程继续执行"); } } // 多个线程join示例 public class JoinMultipleExample { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { try { System.out.println("线程1开始执行"); // 线程1睡眠2秒 Thread.sleep(2000); System.out.println("线程1执行结束"); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread thread2 = new Thread(() -> { try { System.out.println("线程2开始执行"); // 线程2睡眠3秒 Thread.sleep(3000); System.out.println("线程2执行结束"); } catch (InterruptedException e) { e.printStackTrace(); } }); thread1.start(); thread2.start(); System.out.println("主线程等待线程1和线程2执行结束"); thread1.join(); thread2.join(); System.out.println("主线程继续执行"); } } // 有时效的join示例 public class JoinWithTimeoutExample { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { try { System.out.println("子线程开始执行"); // 子线程睡眠4秒 Thread.sleep(4000); System.out.println("子线程执行结束"); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); System.out.println("主线程等待子线程执行结束,最多等待3秒"); thread.join(3000); System.out.println("主线程继续执行"); } }
- 作用和示例:用于等待一个或多个线程运行结束。通过示例代码展示了在主线程中等待单个线程或多个线程执行完毕的用法,以及不同顺序使用 join 方法的结果。还介绍了有时效的 join 方法的使用和结果差异(如等待足够时间和未等够时间的情况)。
- interrupt 方法详解
// 打断sleep线程示例 public class InterruptSleepExample { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { try { System.out.println("线程开始睡眠"); // 线程睡眠5秒 Thread.sleep(5000); System.out.println("线程正常唤醒"); } catch (InterruptedException e) { System.out.println("线程被打断,打断状态:" + Thread.currentThread().isInterrupted()); } }); thread.start(); // 主线程等待1秒后打断线程 Thread.sleep(1000); thread.interrupt(); } } // 打断正常运行线程示例 public class InterruptRunningExample { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("线程被打断,打断状态:" + Thread.currentThread().isInterrupted()); break; } } }); thread.start(); // 主线程等待1秒后打断线程 Thread.sleep(1000); thread.interrupt(); } } // 打断park线程示例 import java.util.concurrent.locks.LockSupport; public class InterruptParkExample { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { System.out.println("线程开始park"); LockSupport.park(); if (Thread.currentThread().isInterrupted()) { System.out.println("线程被打断,打断状态:" + Thread.currentThread().isInterrupted()); } }); thread.start(); // 主线程等待1秒后打断线程 Thread.sleep(1000); thread.interrupt(); } }
- 对不同状态线程的作用:打断 sleep、wait、join 的线程时,会清空打断状态(以 sleep 为例);打断正常运行的线程不会清空打断状态;打断 park 线程也不会清空打断状态,但如果打断标记已经是 true,则 park 会失效。
- 不推荐的方法:介绍了一些不推荐使用的线程方法,如 stop、suspend 和 resume,这些方法已过时,容易破坏同步代码块,造成线程死锁。
- 主线程与守护线程
// 守护线程示例 public class DaemonThreadExample { public static void main(String[] args) { Thread daemonThread = new Thread(() -> { while (true) { System.out.println("守护线程执行中"); try { // 守护线程睡眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); // 设置为守护线程 daemonThread.setDaemon(true); daemonThread.start(); try { // 主线程睡眠3秒 Thread.sleep(3000); System.out.println("主线程执行结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }
- 定义和示例:默认情况下,Java 进程需等待所有线程结束才会结束。守护线程则只要其他非守护线程运行结束,即使自身代码未执行完也会强制结束。例如,垃圾回收器线程就是守护线程,通过示例代码展示了设置守护线程的方法和执行结果。
- 五种状态(从操作系统层面描述)
- 包括初始状态(仅是在语言层面创建了线程对象,还未与操作系统线程关联)、可运行状态(线程已创建且与操作系统线程关联,可由 CPU 调度执行)、CPU 阻塞状态(因调用阻塞 API 导致线程实际不使用 CPU,如 BIO 读写文件)、运行状态(获取了 CPU 时间片正在运行)、终止状态(线程执行完毕,生命周期结束)。
- 六种状态(从 Java API 层面的描述)
- 包括 NEW(线程刚被创建,但还没有调用 start 方法)、BLOCKED、CPU RUNNABLE(涵盖了操作系统层面的可运行、运行和因 BIO 导致的阻塞状态)、WAITING、TIMED_WAITING、TERMINATED(当线程代码运行结束)。
3. 共享模型之管程
-
共享带来的问题
- 小故事示例
- 通过老王出租算盘给小南和小女使用的故事,形象地描述了共享资源在多线程环境下可能出现的问题。如小南在使用算盘过程中可能会因为各种原因(如休息、阻塞 IO 操作、等待条件满足等)暂停使用,导致算盘闲置浪费,同时小女也可能需要使用算盘,如果没有合理的共享机制,就会出现不公平和资源浪费的情况。而且在计算过程中需要存储中间结果时,如果出现线程切换,可能会导致数据不一致的问题。
- Java 代码示例
// 共享带来的问题 - 临界区和竞态条件示例 public class SharedProblemExample { private static int counter = 0; public static void main(String[] args) { Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { // 临界区,对共享资源counter进行自增操作 counter++; } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i日前的操作系统,以下是更新操作系统为Linux后的yield方法示例 ```java // yield方法示例(Linux操作系统下行为可能不同) public class YieldExampleLinux { public static void main(String[] args) { Thread thread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程1执行,计数:" + i); if (i == 2) { // 线程1让出CPU使用权 Thread.yield(); } } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程2执行,计数:" + i); } }); thread1.start(); thread2.start(); } }
- 通过两个线程对初始值为 0 的原始静态变量一个做自增,一个做自减,各做 5000 次的示例,展示了由于 Java 中对静态变量的自增、自减操作不是原子操作,在多线程环境下可能导致结果不确定(可能是正数、负数或零)。从字节码层面分析了自增和自减操作的指令序列,以及在多线程下指令交错执行可能出现的情况。同时引入了临界区(一段代码块内如果存在对共享资源的多线程读写操作)和竞态条件(多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测)的概念。
- 小故事示例