- 进程
进程是程序在操作系统中的一次执行过程(一个程序),是系统进行资源分配和调度的基本单位。可以将进程看作是一个正在运行的程序实例,它拥有自己独立的内存空间、文件资源、网络资源等,是一个相对独立的执行环境。例如,当你打开一个浏览器程序,操作系统就会为这个浏览器创建一个进程,该进程负责管理浏览器的所有操作和资源。
- 线程
线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的大部分资源,但每个线程都有自己独立的栈和程序计数器。例如,在浏览器进程中,可能会有一个线程负责渲染网页,另一个线程负责处理用户的输入事件,还有一个线程负责下载文件等。
一个进程包含多个线程,这些线程共享进程的内存空间和系统资源,比如打开一个浏览器程序,它就是一个进程,而在浏览器中同时进行网页加载、视频播放、下载文件等操作,这些操作通常是由不同的线程来完成的。
- 资源分配
进程:拥有自己独立的资源空间,包括代码段、数据段、堆、栈、文件描述符表、网络套接字等。不同进程之间的资源是相互隔离的,一个进程不能直接访问另一个进程的资源。如果需要进行进程间通信(IPC),则需要使用特定的机制,如管道、消息队列、共享内存等。
线程:共享其所属进程的大部分资源,如代码段、数据段和堆。这意味着多个线程可以访问和修改相同的全局变量和堆上的对象。但每个线程都有自己独立的栈空间,用于存储线程执行过程中的局部变量、函数调用信息等,以及独立的程序计数器,用于记录线程当前执行的指令地址。
- 并发(Concurrency)
并发指在同一时间段内,有多个任务在逻辑上是同时执行的,但实际上在单处理器系统中,这些任务是通过快速切换轮流使用 CPU 资源来实现 “同时” 执行的,并不是真正意义上的同时进行。在多处理器系统中,也可以是不同处理器上同时执行不同的任务。并发强调的是多个任务之间的互相重叠执行的概念,这些任务之间可能存在着各种交互和依赖关系。
示例:在操作系统中,用户一边使用浏览器浏览网页,一边使用音乐播放器播放音乐,还可能同时打开了文档编辑器在编辑文档。从用户的角度看,这些应用程序似乎都在同时运行,但实际上在单核心 CPU 的情况下,CPU 会在这些应用程序的任务之间快速切换,让用户感觉它们是同时在运行的。
- 并行(Parallelism)
并行指在同一时刻,有多个任务在不同的处理器或处理器核心上同时进行执行,是真正意义上的同时执行。并行通常需要硬件支持,例如多核心 CPU、多处理器系统或者分布式系统等,使得多个任务能够在不同的计算资源上同时开展工作,相互之间没有时间上的交错。
示例:在一个具有多个 CPU 核心的服务器上,有多个大数据处理任务,每个任务可以被分配到不同的 CPU 核心上同时进行处理。比如,一个任务在对大量用户数据进行统计分析,另一个任务在对视频文件进行转码处理,它们可以在不同的核心上同时进行,互不干扰,这就是并行执行。
- 多个线程有作用呢?
提高程序的并发性和响应性:在一个程序中,如果有多个任务可以同时进行,使用线程可以让这些任务并发执行,提高程序的整体运行效率。比如在一个图形界面程序中,使用一个线程来处理用户界面的交互,另一个线程来进行数据的加载和处理,这样用户在操作界面时不会因为数据加载等耗时操作而出现界面卡顿的情况,提高了用户体验。
充分利用多核处理器资源:现代计算机通常具有多个 CPU 核心,使用多线程可以将不同的任务分配到不同的核心上并行执行,充分发挥多核处理器的性能,提高程序的运行速度。
实现异步操作:在网络编程、文件读写等操作中,使用线程可以实现异步操作,即主线程可以在发起一个操作后继续执行其他任务,而不需要等待该操作完成,当操作完成后,通过回调等方式通知主线程。这样可以提高程序的执行效率和资源利用率。
-
线程调度模型
线程的调度模型是指按照特定机制为多个线程分配CPU使用权的方式。主要有两种调度模型:
分时调度模型
定义:让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片。
特点:在这种模型下,每个线程都会获得相等的时间片来执行,时间片用完之后,线程会被挂起,等待下一次分配时间片。这种方式保证了每个线程都有机会执行,避免了某个线程长时间占用CPU的情况。
抢占式调度模型
定义:优先让可运行池中优先级高的线程占用CPU。如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
特点:
优先级高的线程会获得更多的执行时间,而优先级低的线程则可能得不到执行机会或执行时间较短。
在这种模型下,线程可能因为时间片用完、主动放弃CPU(如调用yield方法)、被更高优先级的线程抢占、进入阻塞或等待状态等原因而放弃CPU。
Java虚拟机采用的就是抢占式调度模型。 -
实现线程
在Java中,实现线程主要有三种方式:继承Thread类、实现Runnable接口以及使用Callable接口和Future对象(虽然Callable不是直接实现线程的方式,但它与线程池结合使用时可以实现带返回值的线程任务)。
继承Thread类
// 定义一个线程类,继承Thread类
class MyThread extends Thread {
// 重写run方法,定义线程执行的任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
// 休眠100毫秒,模拟任务执行时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
// 创建线程对象
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
}
}
实现Runnable接口
// 定义一个任务类,实现Runnable接口
class MyRunnable implements Runnable {
// 实现run方法,定义线程执行的任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
// 休眠100毫秒,模拟任务执行时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
// 创建任务对象
MyRunnable myRunnable = new MyRunnable();
// 创建线程对象,并传入任务对象
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
使用Callable接口和Future对象(结合线程池)
import java.util.concurrent.*;
// 定义一个任务类,实现Callable接口
class MyCallable implements Callable<Integer> {
// 实现call方法,定义线程执行的任务,并返回结果
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
// 模拟任务执行时间
Thread.sleep(100);
}
return sum;
}
}
public class CallableExample {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 提交Callable任务给线程池,并获取Future对象
Future<Integer> future = executorService.submit(new MyCallable());
try {
// 从Future对象中获取任务执行的结果
Integer result = future.get();
System.out.println("Task result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭线程池
executorService.shutdown();
}
}
}
以上三种方式各有优缺点,选择哪种方式取决于具体的应用场景和需求。例如,如果任务需要返回值,则应该使用Callable接口;如果任务不需要返回值且任务类已经继承了其他类,则可以使用Runnable接口;如果任务不需要返回值且任务类没有继承其他类的需求,则可以直接继承Thread类。
- 线程的常用方法
start():启动线程,执行线程对象中的run()方法。
run():线程需要执行的任务代码通常放在这个方法中。
sleep(long millis):使当前线程休眠指定的毫秒数。
//哪个线程在执行,就休眠哪个线程
Thread.sleep(1000); // 休眠1秒
join():等待其他线程终止。
join(long millis):等待该线程执行完毕,最多等待 millis 毫秒。如果超过这个时间,即使被调用线程还未执行完,当前线程也会恢复执行。
join(long millis, int nanos):等待该线程执行完毕,最多等待 millis 毫秒加 nanos 纳秒。
public class JoinMethodExample {
public static void main(String[] args) {
// 创建一个新线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
// 当前线程休眠 1 秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 t1 执行: " + i);
}
});
// 创建另一个新线程
Thread t2 = new Thread(() -> {
try {
// 等待 t1 线程执行完毕
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
try {
// 线程休眠 1 秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 t2 执行: " + i);
}
});
// 启动 t1 线程
t1.start();
// 启动 t2 线程
t2.start();
try {
// 主线程等待 t2 线程执行完毕
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程执行完毕");
}
}
isAlive():测试线程是否处于活动状态。
currentThread():返回对当前执行线程的引用。
yield():提示调度器当前线程愿意放弃对处理器的使用。
interrupt():中断线程。
setPriority(int newPriority) :设置线程的优先级。
线程优先级的取值范围是 1 到 10,其中 1 是最低优先级(Thread.MIN_PRIORITY),10 是最高优先级(Thread.MAX_PRIORITY),默认优先级是 5(Thread.NORM_PRIORITY)。不过需要注意的是,线程优先级只是给操作系统调度器一个建议,并不保证高优先级的线程一定会先执行或者执行更多时间,因为具体的调度还受到操作系统和硬件的影响。
public class ThreadPriorityExample {
public static void main(String[] args) {
// 创建一个低优先级的线程
Thread lowPriorityThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("低优先级线程执行: " + i);
try {
// 线程休眠 100 毫秒,模拟执行任务
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 创建一个高优先级的线程
Thread highPriorityThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("高优先级线程执行: " + i);
try {
// 线程休眠 100 毫秒,模拟执行任务
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 设置低优先级线程的优先级为最低优先级
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
// 设置高优先级线程的优先级为最高优先级
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
// 启动低优先级线程
lowPriorityThread.start();
// 启动高优先级线程
highPriorityThread.start();
}
}
-
线程的生命周期
线程的生命周期指的是一个线程从创建到销毁的整个过程,这些状态定义在 Thread.State 枚举类中:
新建(New)
当使用 new 关键字创建一个 Thread 对象时,线程就处于新建状态。此时线程仅仅是一个对象实例,还没有开始执行任何代码,它还未与操作系统的线程关联起来。
例如 Thread thread = new Thread();。
就绪(Runnable)
当调用线程的 start() 方法后,线程进入就绪状态。处于就绪状态的线程已经具备了运行的条件,正在等待操作系统的调度器分配 CPU 时间片。一旦获得 CPU 时间片,线程就会进入运行状态。
运行(Running)
当线程获得 CPU 时间片后,就进入运行状态,开始执行 run() 方法中的代码。在多线程环境中,线程可能会因为调度器的调度而在运行状态和就绪状态之间切换。
阻塞(Blocked)
线程在运行过程中,可能会因为某些原因进入阻塞状态,暂时停止执行。常见的导致线程进入阻塞状态的原因有:
等待获取锁:当线程尝试获取一个被其他线程占用的锁时,会进入阻塞状态,直到锁被释放。
等待 I/O 操作完成:例如进行文件读写、网络通信等操作时,线程会等待 I/O 操作完成。
调用 Thread.sleep()、Object.wait()、Thread.join() 等方法:这些方法会使线程主动进入阻塞状态。
等待(Waiting)
线程调用 Object.wait()、Thread.join() 或 LockSupport.park() 等方法后,会进入等待状态。处于等待状态的线程需要其他线程进行显式的唤醒操作,如调用 Object.notify()、Object.notifyAll() 或 LockSupport.unpark() 方法,否则线程会一直等待下去。
超时等待(Timed Waiting)
与等待状态类似,但超时等待状态的线程会在指定的时间后自动恢复到就绪状态。例如调用 Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis) 等方法时,线程会进入超时等待状态。
终止(Terminated)
线程的 run() 方法执行完毕或者因为异常退出时,线程进入终止状态。一旦线程进入终止状态,就不能再重新启动。
-
线程安全问题
线程出现不安全的情况,主要是由于多线程环境下对共享资源的并发访问没有得到妥善管理。
多个线程同时修改共享变量:
当多个线程同时访问并修改同一个变量时,如果没有适当的同步机制,就可能导致数据竞争和不一致性。
非原子操作:
(如i++)在Java中不是原子的,它们可能由多个CPU指令组成。在多线程环境下,这些操作可能会被中断,从而导致数据不一致。
public class AtomicProblem implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++; // 非原子操作,包含读取、计算、写入三个步骤
}
}
public static void main(String[] args) throws InterruptedException {
AtomicProblem ap = new AtomicProblem();
Thread t1 = new Thread(ap);
Thread t2 = new Thread(ap);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + ap.count); // 结果通常小于20000
}
}
内存可见性问题:
一个线程对共享变量的修改可能对其他线程不可见,这通常是由于编译器的优化或内存模型的特性导致的。
public class MemoryVisibility {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
flag = true;
System.out.println("Flag set to true");
});
Thread reader = new Thread(() -> {
while (!flag) {
// 等待flag变为true
}
System.out.println("Flag is true, exiting");
});
writer.start();
reader.start();
reader.join();
}
}
//writer线程将flag设置为true,但reader线程可能由于内存可见性问题而看不到这个变化,导致它陷入死循环。
指令重排序:
为了提高性能,编译器和处理器可能会对指令进行重排序。在多线程环境中,这种重排序可能导致不可预期的结果。
异步(Asynchronous)
异步线程是指在执行某个任务时,不需要等待该任务完成,就可以继续执行后续的代码。也就是说,主线程或调用线程不会因为启动了一个新线程而被阻塞,可以并发地去做其他事情。
线程同步(Synchronization)
线程同步是指多个线程在访问共享资源或执行相关任务时,通过某种机制来协调它们的执行顺序,以确保数据的一致性和完整性,避免出现竞态条件等线程安全问题。简单来说,同步意味着线程之间需要按照一定的顺序依次执行,一个线程在执行某个操作时,其他相关线程需要等待该操作完成后才能继续执行。
解决方案
为了解决线程不安全的问题,可以采取以下措施:
使用同步机制(如synchronized关键字或Lock对象)来确保对共享资源的访问是互斥的。
使用原子变量(如AtomicInteger)来执行原子操作。
使用volatile关键字来确保变量的内存可见性。
避免在多线程环境中进行复杂的、不可预测的操作,尽量将操作简化为原子操作或受保护的同步块。
异步导致的数据问题
public class ThreadUnsafeExample {
// 共享的计数器
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
int threadCount = 10;
Thread[] threads = new Thread[threadCount];
// 创建并启动多个线程
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 对共享计数器进行递增操作
counter++;
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 预期计数器的值应该是 10 * 1000 = 10000
System.out.println("预期计数器的值: 10000");
System.out.println("实际计数器的值: " + counter);
}
}
使用 synchronized 关键字
synchronized 关键字可以修饰方法或代码块,保证同一时间只有一个线程可以访问被修饰的方法或代码块,从而实现线程同步。
public class ThreadSafeWithSynchronized {
// 共享的计数器
private static int counter = 0;
// 同步方法,保证同一时间只有一个线程可以执行该方法
public static synchronized void increment() {
counter++;
}
public static void main(String[] args) throws InterruptedException {
int threadCount = 10;
Thread[] threads = new Thread[threadCount];
// 创建并启动多个线程
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 调用同步方法进行递增操作
increment();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 预期计数器的值应该是 10 * 1000 = 10000
System.out.println("预期计数器的值: 10000");
System.out.println("实际计数器的值: " + counter);
}
}
使用 ReentrantLock
ReentrantLock 是 Java 并发包中提供的一个显式锁,通过 lock() 和 unlock() 方法来控制线程对共享资源的访问。
import java.util.concurrent.locks.ReentrantLock;
public class ThreadSafeWithReentrantLock {
// 共享的计数器
private static int counter = 0;
// 创建一个可重入锁
private static final ReentrantLock lock = new ReentrantLock();
public static void increment() {
// 获取锁
lock.lock();
try {
// 对共享计数器进行递增操作
counter++;
} finally {
// 释放锁,确保在任何情况下锁都会被释放
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
int threadCount = 10;
Thread[] threads = new Thread[threadCount];
// 创建并启动多个线程
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 调用同步方法进行递增操作
increment();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 预期计数器的值应该是 10 * 1000 = 10000
System.out.println("预期计数器的值: 10000");
System.out.println("实际计数器的值: " + counter);
}
}
使用原子类(AtomicInteger)
AtomicInteger 是 Java 并发包中提供的一个原子类,它使用 CAS(Compare-And-Swap)操作来保证对整数的操作是原子性的,从而避免了多线程环境下的数据不一致问题。
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeWithAtomicInteger {
// 使用 AtomicInteger 作为共享的计数器
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
int threadCount = 10;
Thread[] threads = new Thread[threadCount];
// 创建并启动多个线程
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 调用 AtomicInteger 的 incrementAndGet 方法进行递增操作
counter.incrementAndGet();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 预期计数器的值应该是 10 * 1000 = 10000
System.out.println("预期计数器的值: 10000");
System.out.println("实际计数器的值: " + counter.get());
}
}
- 线程通信
线程通信指的是多个线程在执行过程中,通过某种机制进行信息交互和协调,以实现特定的业务逻辑。例如,一个线程完成了数据的准备工作后,通知其他线程可以开始使用这些数据;或者一个线程在等待某个条件满足时,被其他线程唤醒继续执行。
使用 Object 类的 wait()、notify() 和 notifyAll() 方法实现线程通信
wait() 方法会使当前线程进入等待状态,直到其他线程调用该对象的 notify() 或 notifyAll() 方法。notify() 方法会唤醒在此对象监视器上等待的单个线程,notifyAll() 方法会唤醒在此对象监视器上等待的所有线程。
// 共享资源类
class SharedResource {
private boolean isDataReady = false;
// 生产者线程调用此方法生产数据
public synchronized void produce() {
try {
// 模拟生产数据的过程
Thread.sleep(1000);
System.out.println("生产者生产了数据");
isDataReady = true;
// 唤醒等待的消费者线程
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费者线程调用此方法消费数据
public synchronized void consume() {
try {
// 如果数据还未准备好,线程进入等待状态
while (!isDataReady) {
wait();
}
System.out.println("消费者消费了数据");
isDataReady = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadCommunicationExample {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
// 生产者线程
Thread producer = new Thread(() -> {
sharedResource.produce();
});
// 消费者线程
Thread consumer = new Thread(() -> {
sharedResource.consume();
});
// 启动线程
consumer.start();
producer.start();
}
}
- 线程池
线程池是一种设计模式,用于限制在程序中同时运行的线程数量,以减少系统资源的开销和提高性能。它通过一个共享的线程集合来执行任务,而不是为每个任务创建新的线程。线程池可以复用线程,减少线程的创建和销毁开销,同时也可以通过限制线程数量来控制并发级别,避免过多的线程竞争系统资源。
java.util.concurrent包提供了对线程池的支持。最常见的实现类是ThreadPoolExecutor,但更常用的是Executors工厂类。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 定义一个任务类,实现 Runnable 接口
class MyTask implements Runnable {
private final int taskId;
public MyTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
}
}
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为 3 的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交 5 个任务给线程池
for (int i = 1; i <= 5; i++) {
executor.submit(new MyTask(i));
}
// 关闭线程池,不再接受新任务,但会执行完已提交的任务
executor.shutdown();
}
}
使用 ThreadPoolExecutor 自定义线程池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
// 任务类
class MyTask4 implements Runnable {
private final int taskId;
public MyTask4(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
}
}
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 创建一个自定义的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 线程空闲时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10) // 任务队列
);
// 提交 15 个任务给线程池
for (int i = 1; i <= 15; i++) {
executor.submit(new MyTask4(i));
}
// 关闭线程池
executor.shutdown();
}
}