Java并发
一、进程和线程
进程:
程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在Java中,当我们启动main函数时其实就是启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。
线程:
线程是比进程更小的执行单位。一个进程执行过程中可以产生多个线程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈,因此系统在产生/切换线程时代价比进程小得多-----线程也成为轻量级进程。
线程与进程的关系、区别和优缺点:
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
程序计数器、虚拟机栈和本地方法栈私有原因:
程序计数器:
程序计数器功能:①字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制--------顺序执行、选择、循环等 ②多线程情况下,程序计数器用于记录当前线程执行的位置,当线程切换时能够继续该线程原操作。
Java程序执行流程:
编写源代码文件(.java后缀)---> Java编译器编译源代码转为字节码(.class后缀)---> Java虚拟机(JVM)加载.class文件中的字节码 ---> 字节码解释器解释字节码并执行 + JIT编辑器在运行时将热点代码编译为机器代码提高性能 ---> 执行程序 (JVM执行字节码或机器代码,管理内存、线程等)---> 运行时管理(JVM负责内存分配、垃圾回收)
程序计数器参与部分:
- 当 JVM 加载字节码时,程序计数器会为每个线程分配一个独立的计数器
- 在执行字节码的过程中,程序计数器指向当前正在执行的字节码指令的地址。
- 每当一条指令执行完成后,程序计数器会更新,指向下一条要执行的指令。
- 当方法被调用时,程序计数器保存当前指令的位置,以便在方法返回时能够继续执行。
- 这对于支持多线程和方法调用非常重要,确保线程之间的独立执行顺序。
(当一个方法被调用时,程序计数器会保存调用该方法时的下一条指令的位置。这意味着在方法执行完成后,可以返回到正确的位置继续执行)
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈:
虚拟机栈功能:
每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈功能:
与虚拟机栈相似------虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的本地(Native)方法服务。
Native方法:
是Java 程序中声明的,可以在底层使用其他语言(如 C 或 C++)实现的功能,通常用于访问系统资源或提高性能。
虚拟机栈和本地方法栈私有是为了保证线程中的局部变量不被别的线程访问到
二、线程创建
线程创建方式:
①继承Thread类
每个线程都是一个 Thread 实例,调用 start() 方法后会执行 run() 方法。系统为每个线程分配资源和调度。
class MyThread extends Thread {@Overridepublic void run() {System.out.println("Thread is running");}
}// 使用
MyThread thread = new MyThread();
thread.start();
②实现Runnable接口
创建一个 Runnable 对象,并将其传递给 Thread。调用 start() 时,线程会执行 Runnable 的 run() 方法,线程与任务分离,便于复用。
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("Runnable is running");}
}// 使用
Thread thread = new Thread(new MyRunnable());
thread.start();
③Callable接口
与 Runnable 类似,但 Callable 可以返回结果并抛出异常。使用 FutureTask 来管理线程的结果和状态。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<String> {@Overridepublic String call() {return "Callable is running";}
}// 使用
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
④使用线程池
线程池重用线程,减少创建和销毁线程的开销。线程由 ExecutorService 管理,可以通过提交任务来控制线程的执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> System.out.println("Task is running in thread pool"));
executorService.shutdown();
⑤CompletableFuture类
基于 Future 和 Executor,支持异步编程和链式调用。它实现了非阻塞的方式来处理并发任务,允许更灵活的组合和错误处理。
import java.util.concurrent.CompletableFuture;CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {System.out.println("CompletableFuture is running");
});
future.join(); // 等待任务完成
非阻塞:在当前线程下创建线程池(默认ForkJoinPool)来执行异步任务
线程生命周期:
线程在生命周期中会伴随代码执行而在不同状态间切换(区别于Bean的周期):
- NEW: 初始状态,线程被创建出来但没有被调用
start()
。 - RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。 - BLOCKED:阻塞状态,锁被其它线程占有,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,相当于在等待状态的基础上增加了超时限制,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,执行完
run()
方法之后进入该状态,表示该线程已经运行完毕。
RUNNABLE中细分了两个状态:
①RUNNING :可运行状态的线程获得了 CPU 时间片后就处于 RUNNING(运行) 状态
②READY : 线程在调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态
二合一原因:两种状态切换太快,没有必要区分
多线程:
并发与并行的区别:
并发:两个及两个以上的作业在同一时间段内执行。
并行:两个及两个以上的作业在同一时刻执行。
同步和异步的区别:
同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步:调用在发出之后,不用等待返回结果,该调用直接返回。
线程调度方式:
①抢占式调度(JVM默认):
操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。
这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。
存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。
②协同式调度:
线程执行完毕后,主动通知系统切换到另一个线程。
可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。
单核CPU支持多线程问题:
单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。
单核CPU运行多线程效率问题:
情况一:
线程为IO密集型----IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。
效率会提高----如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。
情况二:
线程为CPU密集型----CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。
效率降低----如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。
线程死锁:
死锁:
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
预防死锁:
死锁的产生的必要条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
预防-----破坏上述必要条件
乐观锁和悲观锁:
悲观锁:
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
悲观锁思想的实现:synchronized和ReentrantLock等独占锁
乐观锁:
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
版本号机制:
每个数据项都有一个版本号,表示数据的状态。
在进行更新时,线程会检查当前版本号与预期版本号(即操作开始时的版本号)是否一致。
如果一致,则更新数据并增加版本号;如果不一致,则表示数据已被其他线程修改,更新操作失败。
Compare-And-Swap(CAS)算法:
CAS 是一个原子操作,包括三个参数:要更新的变量值(Var)、预期值(E)和新值(N)。
CAS 检查内存地址 V 的当前值是否等于预期值 E,如果是,则将其更新为新值 N。
CAS 操作要么成功(更新),要么失败(不更新),并返回当前值。