1.方法区
方法区是java虚拟机中一个特殊的区域, 用来存储类的信息和静态数据, 主要包括以下内容
方法区是所有线程共享的,不同的线程共享方法区中的数据。当一个类被加载到方法区中时,它的信息、字段、方法、常量池和静态变量都会被创建,并存储在方法区中,供类的实例和方法使用
2.堆
状态信息
: 主要是对象实例的字段,可以是基本数据类型的值,也可以是引用类型的值。行为信息
:主要是对象实例的方法。字符串字面量
和字符串常量
。字符串常量池中的字符串对象在类的整个生命周期中是不变的,并且它们是线程安全的。数组的元素
和数组的长度
。数组的元素可以是基本数据类型的值,也可以是引用类型的值。3.虚拟机栈
虚拟机栈(Java Virtual Machine Stack,也称为栈帧栈)是Java虚拟机内存模型中的一个重要区域,用于存储栈帧。虚拟机栈是线程私有的,每个线程在启动时都会创建一个虚拟机栈,用于存储该线程执行的方法的栈帧。
4.本地方法栈
本地方法栈(Native Method Stack)是Java虚拟机内存模型中的一个重要区域,用于存储本地方法的栈帧。本地方法栈是线程私有的,每个线程在启动时都会创建一个本地方法栈,用于存储该线程执行的本地方法的栈帧。
本地方法是指由其他编程语言(通常是C或C++)编写的方法,而不是由Java语言编写的方法。这些方法通常是通过Java Native Interface(JNI)调用的,从而允许Java程序调用非Java代码中的方法。
5.程序计数器
是Java虚拟机中的一个内存区域,用于存储当前线程正在执行的字节码指令的地址。程序计数器是线程私有的,每个线程都有自己独立的程序计数器。
程序计数器的主要作用是指示当前线程正在执行哪个字节码指令,从而控制线程的执行流程。当线程开始执行一个方法时,程序计数器会指向该方法的字节码指令序列的第一个字节码指令。随着线程的执行,程序计数器会递增,指向下一个字节码指令。
问题引申: 常量是不是都存储在常量池?
有一些常量是存在在常量池, 但不是所有的常量都存储在常量池
String str = "hello"; // "hello" 存储在字符串常量池中
int x = 42; // 42 可能被存储在常量池中
final
修饰的基本数据类型或字符串字段,如果其值在编译时已知,那么它们也可能被放入常量池。public static final int CONSTANT_VALUE = 42; // 常量池中的整数常量
然而,并非所有的常量都会进入常量池。例如,通过运行时计算得到的常量值通常不会被放入常量池,而是在运行时动态生成。此外,对于对象实例、数组等,它们的常量信息一般不会直接放入常量池。
String dynamicString = "Hello" + " " + "World";
在这个例子中,字符串 “Hello World” 的拼接是在运行时发生的,而不是在编译时。因此,这个字符串的实例通常会被存储在堆中,而不是字符串常量池中。
java文件到最终运行, 需要经过编译
和类加载
这两个阶段
编译的过程:把.java文件编译成.class文件
类加载的过程:把.class文件加载到jvm的内存中
下图展示了类加载的过程
1.加载
加载过程指的是: 将类的字节码文件(.class)从磁盘加载到Java虚拟机内存中这一过程
在这个阶段,Java虚拟机会根据类的全限定名查找字节码文件,以流的方式将其内容读取到内存中,然后将其转换为方法区的运行时数据结构,这个过程也会在方法区中生成一个代表这个类的java.lang.Class对象。
这个数据结构存储了类的字段、方法、构造方法等信息,同时也包含了类的常量池,即字面量(如字符串、final常量)和符号引用等。
2.连接
3.初始化
在初始化阶段,Java虚拟机会执行类的静态初始化方法(静态块)和静态变量的赋值操作,以完成类的初始化工作。这个阶段是类加载过程的最后一个阶段,也是最重要的一个阶段。
什么是内存泄漏
程序运行过程中存在指针或引用,指向了不再被应用程序需要的堆内存,因为某些原因, 程序没有正确释放这些堆内存, 导致它们持续占用内存, 最终导致堆内存不足
或应用程序性能下降
可能会导致内存泄漏的原因
内存泄漏通常是由程序员的错误引起的,可能是由于静态字段
、未释放资源
、集合容器
、改变哈希值
、内部类持有外部类
、字符串常量
、ThreadLocal
等等
当发生内存泄漏时,垃圾回收器无法回收这些不再使用的对象,因为这些对象仍然被引用,尽管程序不再需要它们。
1.静态字段(static)导致内存泄漏
import java.util.ArrayList;
import java.util.List;
public class StaticMemoryLeak {
private static List<MyObject> myObjects = new ArrayList<>();
public static void main(String[] args) {
createObjects();
// 此时,myObjects中持有了MyObject的引用
// 假设后续的代码逻辑中不再需要这些对象,但由于myObjects中仍然持有引用,它们无法被垃圾回收。
}
private static void createObjects() {
MyObject obj1 = new MyObject();
MyObject obj2 = new MyObject();
myObjects.add(obj1);
myObjects.add(obj2);
}
private static class MyObject {
// 这里可以包含一些数据
}
}
由于myObjects是静态的, 它会在整个程序运行期间一直存在,而列表中的对象也不会被移除。 即使在应用程序的执行过程中,这些对象已经不再被需要,它们仍然会一直保留在列表中,无法被垃圾回收。
当不再需要某个对象时,确保将其从静态集合中移除
public static void main(String[] args) {
createObjects();
// 此时,myObjects中持有了MyObject的引用
// 假设后续的代码逻辑中不再需要这些对象,移除它们
myObjects.clear();
}
如果静态集合中持有的对象不需要强引用,可以考虑使用弱引用(WeakReference)或软引用(SoftReference)。这样,在垃圾回收器决定回收内存时,这些引用对象就会被自动释放。
private static List<WeakReference<MyObject>> myObjects = new ArrayList<>();
private static void createObjects() {
MyObject obj1 = new MyObject();
MyObject obj2 = new MyObject();
myObjects.add(new WeakReference<>(obj1));
myObjects.add(new WeakReference<>(obj2));
}
2.未关闭的资源导致内存泄漏
未关闭的资源可能导致内存泄漏,特别是对于一些与外部资源(如文件、网络连接、数据库连接等)有关的资源。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ResourceLeakExample {
public static void main(String[] args) {
readFromFile("example.txt");
// 此时,文件流未关闭,可能导致资源泄漏
}
private static void readFromFile(String filename) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(filename));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 注意:在这个例子中,finally块中关闭资源的代码漏写了,可能导致资源泄漏
// 应该添加以下代码确保关闭资源
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
如果在readFromFile方法调用后, 不再需要这个文件流,它就会一直保持打开状态,可能导致文件资源泄漏
private static void readFromFile(String filename) {
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
知识点引申: try-with-resources的原理和用法
原理:
在 try-with-resources 语句中,可以在 try 关键字后面的圆括号内声明一个或多个资源。这些资源必须实现 AutoCloseable 接口。在 try 代码块执行结束时,无论是否发生异常,会自动调用资源的 close() 方法。这样就无需显式在 finally 块中手动关闭资源。
用法:
try (ResourceType1 resource1 = new ResourceType1();
ResourceType2 resource2 = new ResourceType2();
// ... 可以有更多资源声明
) {
// 代码块,处理资源
} catch (ExceptionType e) {
// 异常处理
}
3.集合容器
区别于上面的static集合容器, 此处使用非static容器来掩饰
import java.util.ArrayList;
import java.util.List;
@Coment
public class SingleExample{
// 实例变量
private List<MyObject> myObjects = new ArrayList<>();
// 往集合容器中添加元素
public void addObject(MyObject obj) {
myObjects.add(obj);
}
}
// 元素对象
public class MyObject {
}
// 测试
public static void main(String[] args) {
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
SingleExample example= (User) beanFactory.getBean("singleExample");
MyObject obj1 = new MyObject();
MyObject obj2 = new MyObject();
example.addObject(obj1);
example.addObject(obj2);
}
SingleExample作为单例对象, 在容器生命周期内通常不会被销毁, Spring容器会一直保持这个单例对象的引用, 这种情况下, 实例变量myObjects也会随着 SingleExample对象一起成为不可达对象, 可能会导致内存泄漏
双亲委派模型是java类加载机制中的一种机制, 通过这种机制, 保证类的加载是由下至上的, 即从子类加载器开始,逐级向上委派,直至达到最顶层的启动类加载器。这是确保类加载的一致性和唯一性的关键机制。
下面是其工作原理:
扩展类加载器
或启动类加载器
,取决于当前类加载器的类型)通过这样的双亲委派机制,可以防止类的重复加载,确保了类的唯一性。同时,它也增加了类加载的安全性,防止恶意类的加载,因为类加载器在加载一个类之前会先委派给其父类加载器,如果父类加载器已经加载了这个类,那么就不会加载重复的类,从而避免了恶意类的替换。
Java内存回收, 主要是针对年轻代(Young)
、老年代(Tenured)
、元空间(MetaSpace)
进行操作的
Minor GC
, 清除不活跃的对象, 释放Eden区的内存空间, 随后对Eden区再次判断
FullGC
, 释放年轻代和老年代中保存的不活跃对象Java在每次创建对象时如果发现内存不足都会自动向其他区域延伸。为了提高性能,在实际应用中可能会开辟尽量大的内存空间,以实现更加合理的GC控制。
1.Minor GC
Minor GC是只针对新生代的垃圾回收, 清理Eden区
和Survivor区
不再被引用的对象, 此外它还会将满足年龄的阈值的对象从Eden区
晋升到Survivor区
, 晋升之后, 原来在 Eden 区的对象会被删除
年轻代的对象晋升到 Survivor 区的年龄阈值通常是15, 具体来说, 对象在 Eden 区创建后, 每次经历一次 Minor GC,对象的年龄就会递增1, 当对象的年龄达到一定的阈值时,它将晋升到 Survivor 区
Minor GC主要采用Parallel Scavenge
2.Full GC
Full GC 主要集中在老年代的垃圾回收,以及对整个堆内存(包括年轻代和老年代)的清理和整理
Full GC主要采用Parallel Old
Minor GC和Full GC是通过垃圾回收器进行垃圾回收的, 最常见的垃圾回收器有以下几种
JDK8中, 默认的垃圾回收器取决于运行的平台
垃圾回收器通过可达性分析
来标记不被引用的对象, 从GC Roots
出发, 沿着对象的引用关系, 标记所有直接或间接与GC Roots相连的存活对象。
这样,垃圾收集器就能够确定哪些对象是存活的,哪些对象是可以被回收的垃圾。
查看默认的垃圾回收器
java -XX:+PrintCommandLineFlags -version
Parallel Scavenge和Parallel Old的区别
复制算法
,将幸存的对象复制到另一个幼年代区域,并在这个过程中进行垃圾回收。标记-清除-整理算法
,包括标记阶段、清除阶段和整理阶段,以回收不再使用的对象并整理存活的对象。下面是标记-清除(Mark and Sweep)垃圾回收算法的基本流程:
知识点一: 可作为GC Roots的对象
在Java中,垃圾收集器通过GC Roots来确定对象的可达性。以下是一些常见的对象类型,它们可以作为GC Roots:
这些对象都是垃圾收集的起始点,通过追踪这些GC Roots对象及其引用关系,垃圾收集器可以确定哪些对象是可达的,哪些对象是不可达的垃圾。这种方式保证了不会误将仍然可达的对象回收,同时能够识别并清理掉不可达的对象。
Minor GC
多数情况下,对象在年轻代中的Eden区进行分配,若Eden区没有足够空间,就会触发YGC(Minor GC)
Full GC
JVM调优是为了优化Java程序的性能和资源利用率. 通过调整JVM的参数, 可以更好地适应不同的应用场景和硬件环境。以下是一些常见的JVM调优参数以及它们的作用:
1.作用
ThreadLocal是一种线程隔离机制
, 使得每个线程都可以拥有自己独立的变量副本,从而避免了多线程环境下的线程安全问题。
public class Demo {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static void print(String str){
System.out.println(str + ":" + threadLocal.get());
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("abc");
print("thread1 variable");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("def");
print("thread2 variable");
}
});
thread1.start();
thread2.start();
}
}
2.内部结构和原理
最初的设计
每个ThreadLocal是自己维护一个ThreadLocalMap, key是当前线程, value是要存储的局部变量, 这样就可以达到各个线程的局部变量隔离的效果
JDK8的设计
每个Thread维护一个ThreadLocalMap, 这个Map的key是ThreadLocal本身, value是要存储的变量. 具体的流程如下
这样设计的优点:
3.ThreadLocal与synchronized对比
虽然ThreadLocal与synchronized都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用时间换空间的方法, 只提供了一份变量, 让不同的线程排队访问 | ThreadLocal采用空间换时间的方式, 为每一个线程提供了一份变量的副本, 从而实现同时访问, 互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间数据相互隔离 |
4.源码分析
1.内存泄漏和内存溢出的区别
内存溢出 | 内存泄漏 | |
---|---|---|
定义 | 内存溢出指的是程序在运行过程中申请的内存超过了系统或者进程所能提供的内存大小(结果) | 内存泄漏指的是程序中已经不再需要的内存未被释放,造成系统内存的浪费(起因) |
原因 | 通常是由于程序中存在大量的内存申请,而且没有及时释放,导致系统的可用内存被耗尽 | 内存泄漏通常是由于程序中存在指针或引用,指向了不再使用的内存块,但程序却没有释放这些内存 |
表现 | 当内存溢出发生时,程序通常会崩溃,并且系统可能会报告无法分配内存的错误 | 内存泄漏不会导致程序立即崩溃,但随着时间的推移,系统可用内存会逐渐减少,最终可能导致系统变慢或者崩溃 |
总体来说,内存溢出是由于申请的内存过多,超出了系统限制,而内存泄漏是因为未能及时释放已经不再使用的内存。
解决内存溢出和内存泄漏的方法通常包括合理管理内存的申请和释放过程,使用合适的数据结构,以及利用内存管理工具进行检测和优化。
需要说明一点: 虽然内存泄漏可能会导致内存溢出,但内存溢出也可能是由于其他原因,例如程序中存在大量的内存申请,但这些内存并没有被泄漏,而是在程序执行期间一直保持被占用状态,最终导致系统内存耗尽。
2.强引用和弱引用的区别
Object obj = new Object(); // 强引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
总结:
3.哪些情况下, ThreadLocal会导致内存泄漏?
3.1 长时间存活的线程
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
// 如果线程一直存活,myThreadLocal 将一直持有对 obj 的引用,即使任务执行完毕。
}
}
在这个例子中,即使任务执行完毕,ThreadLocal 对象仍然持有对 MyObject 的引用,而线程的生命周期可能会很长,导致 MyObject 无法被垃圾回收,从而引发内存泄漏。
为了避免这种情况,需要在不再需要 ThreadLocal 存储的对象时,显式调用 remove() 方法来清理 ThreadLocal。这样可以确保 ThreadLocal 对象中的弱引用被正确清理,从而防止内存泄漏。例如:
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
try {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
} finally {
// 清理 ThreadLocal,防止内存泄漏
myThreadLocal.remove();
}
}
}
3.2 使用线程池
如果在使用线程池的情况下,ThreadLocal被设置在某个任务中,而这个任务在线程池中执行完成后线程被放回线程池而不是销毁,那么ThreadLocal可能在下一次任务执行时仍然持有对上次设置的对象的引用。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
// 线程被放回线程池,但 ThreadLocal 可能仍然持有对 obj 的引用。
});
为了避免这类问题,确保在ThreadLocal不再需要时,调用remove()方法清理它所持有的对象引用。这通常在任务执行结束时或者线程即将被销毁时执行。例如:
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
try {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
} finally {
// 清理 ThreadLocal,防止内存泄漏
myThreadLocal.remove();
}
}
}
死锁是指多个线程(两个或以上)在执行过程中互相等待对方释放资源, 无法继续执行下去.
在一个典型的死锁情况中, 每个进程都在等待某个资源, 但同时拥有另一个资源, 由于每个进程都不愿意先释放自己已经占有的资源, 所以形成了相互等待的状况
1.死锁的4个必要条件
i++
操作本身并不是线程安全的
i++
实际上是一个复合操作,包括读取 i 的当前值、将其加一、然后将结果写回 i。其中涉及到读取、修改、写入三个步骤。如果两个线程同时执行这段代码,可能会导致竞态条件,使得最终的结果不是期望的增加2,而可能是增加1或者其他值。
解决方式一: 使用同步(synchronization)
synchronized (lock) {
i++;
}
解决方式二: 使用原子类(Atomic Classes)
AtomicInteger i = new AtomicInteger(0);
i.incrementAndGet();
1.synchronized的三种用法
public synchronized void increase() {
}
public static synchronized void increase() {
}
public Object synMethod(Object a1) {
synchronized(a1) {
// 操作
}
}
2.synchronized用于静态方法与普通方法有区别吗?
public class MyClass {
public synchronized void instanceMethod() {
// 实例方法的同步代码块
}
}
实例方法中, 锁住的是当前实例对象(this)
, 对于MyClass类的不同实例, 它们的实力方法是独立的, 可以同时执行
public class MyClass {
public static synchronized void staticMethod() {
// 静态方法的同步代码块
}
}
静态方法中, 锁住的是整个类的Class对象
, 对于MyClass类的所有实例,同一时间只能有一个线程执行该静态方法。
SimpleDateFormat类在多线程环境下是线程不安全的。
SimpleDateFormat内部维护了一个Calendar实例,而Calendar是线程不安全的
Calendar 的实现并没有在设计上考虑到多线程并发访问的情况。因此,多个线程可能同时修改 Calendar 内部的状态,而不受到足够的同步或锁的保护,从而导致线程安全问题。
解决方式一: 使用局部变量
在每个线程中创建一个独立的 SimpleDateFormat 实例,而不是共享一个实例。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
解决方式二: 使用线程安全的替代方案
如果你需要在多线程环境中进行日期格式化和解析操作,可以考虑使用 java.time.format.DateTimeFormatter,它是 java.time 包中的类,设计为线程安全的。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
1.优点
线程池通过提供一种有效的线程管理和调度机制,帮助提高应用程序的性能和可维护性,尤其在处理大量并发任务时,线程池是一种强大而有效的工具。
2.缺点
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
当我们核心线程数已经到达最大值、阻塞队列也已经放满了所有的任务、而且我们工作线程个数已经达到最大线程数, 此时如果还有新任务, 就只能走拒绝策略了
1.作用
2.用法
在线程池中, BlockingQueue主要通过以下两个参数进行配置
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个LinkedBlockingQueue作为任务队列
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(10);
// 创建一个ThreadPoolExecutor,使用LinkedBlockingQueue作为任务队列
ExecutorService executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
1, // keepAliveTime
TimeUnit.SECONDS,
queue);
// 提交任务到线程池
for (int i = 0; i < 15; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000);
System.out.println("Task completed by: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
打印结果:
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-3
在这个例子中,LinkedBlockingQueue 作为任务队列,可以存储最多 10 个等待执行的任务。线程池的核心线程数为 5,最大线程数为 10,因此在任务队列未满时,新任务将放入队列等待。如果队列已满,新任务将创建新线程执行,但不会超过最大线程数。
因为开启了15个线程, 而核心线程数+阻塞队列容量正好为15个, 所以不会创建新的线程
1.ArrayBlockingQueue
基于数组实现的有界队列
。
固定容量,一旦创建就不能更改。
需要指定容量,适用于任务数量固定的情况。
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
使用 ArrayBlockingQueue 有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到 corePoolSize 时,则会将新的任务加入到等待队列中。若等待队列已满,即超过 ArrayBlockingQueue 初始化的容量,则继续创建线程,直到线程数量达到 maximumPoolSize 设置的最大线程数量,若大于 maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在 corePoolSize 以下,反之当任务队列已满时,则会以 maximumPoolSize 为最大线程数上限
2.LinkedBlockingQueue
基于链表实现的有界或无界队列
。
可以选择是否指定容量,如果不指定容量则默认是 Integer.MAX_VALUE
适用于任务数量不固定的情况
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你 corePoolSize 设置的数量,也就是说在这种情况下 maximumPoolSize 这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到 corePoolSize 后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
3.SynchronousQueue
一个不存储元素的队列
每个插入操作必须等待另一个线程的对应移除操作,反之亦然。
主要用于直接传递任务的场景,一个线程产生任务,另一个线程消费任务
4.PriorityBlockingQueue
基于优先级堆的无界队列。
元素需要实现Comparable接口或者在构造方法中提供Comparator
5.DelayedWorkQueue
一个支持延时获取元素的无界队列,用于实现定时任务。
元素需要实现 Delayed 接口
FixedThreadPool | SingleThreadExecutor | ScheduledThreadPool | CachedThreadPool | |
---|---|---|---|---|
名称 | 固定大小线程池 | 单线程线程池 | 定时任务线程池 | 缓存线程池 |
特点 | 固定线程数量的线程池,适用于负载较重的服务器 | 只有一个工作线程的线程池,确保所有任务按顺序执行 | 支持定时及周期性任务执行的线程池 | 线程数量根据需求动态调整,线程空闲一定时间后被回收 |
1.FixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
public class NewFixedThreadPoolTest {
public static void main(String[] args) {
System.out.println("主线程启动");
// 1.创建1个有2个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
}
};
// 2.线程池执行任务(添加4个任务,每次执行2个任务,得执行两次)
threadPool.submit(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
System.out.println("主线程结束");
}
}
上述代码:创建了一个有2个线程的线程池,但一次给它分配了4个任务,每次只能执行2个任务,所以,得执行两次。
该线程池重用固定数量的线程在共享的无界队列中运行。 在任何时候,最多 nThreads 线程将是活动的处理任务。如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待,直到有线程可用。 所以,它会一次执行 2 个任务(2 个活跃的线程),另外 2 个任务在工作队列中等待着。
submit() 方法和 execute() 方法都是执行任务的方法。它们的区别是:submit() 方法有返回值,而 execute() 方法没有返回值。
2.CachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
适用场景:快速处理大量耗时较短的任务,如 Netty 的 NIO 接受请求时,可使用 CachedThreadPool。
public class NewCachedThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
3.SingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序。
4.SingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池。
public class SingleThreadScheduledExecutorTest {
public static void main(String[] args) {
ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
System.out.println("添加任务,时间:" + new Date());
threadPool.schedule(() -> {
System.out.println("任务被执行,时间:" + new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
}, 2, TimeUnit.SECONDS);
}
}
ThreadFactory是一个接口, 用于创建新线程的工厂, 它允许你自定义线程的创建过程, 例如设置线程的名称、优先级、守护状态等…
import java.util.concurrent.*;
public class CustomThreadFactoryExample {
public static void main(String[] args) {
// 创建一个自定义的ThreadFactory
ThreadFactory customThreadFactory = new CustomThreadFactory("CustomThread");
// 使用自定义的ThreadFactory创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5, customThreadFactory);
// 提交一些任务
for (int i = 0; i < 10; i++) {
executorService.submit(() -> System.out.println(Thread.currentThread().getName()));
}
// 关闭线程池
executorService.shutdown();
}
}
// 自定义的ThreadFactory实现
class CustomThreadFactory implements ThreadFactory {
private final String threadNamePrefix;
public CustomThreadFactory(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
@Override
public Thread newThread(Runnable r) {
// 创建新线程并设置线程名称
Thread thread = new Thread(r, threadNamePrefix + "-" + System.nanoTime());
// 设置为后台线程(可选)
thread.setDaemon(false);
// 设置线程优先级(可选)
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}
一、AbortPolicy
这是默认的拒绝策略,当队列满时直接抛出
RejectedExecutionException异常,阻止系统继续运行。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
二、CallerRunsPolicy
新任务会被直接在提交任务的线程中运行。这样做可以避免任务被拒绝,但会影响任务提交的线程的性能。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
三、DiscardPolicy
新任务被直接丢弃,不做任何处理。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
四、DiscardOldestPolicy
尝试将最旧的未处理任务从队列中删除,然后重新尝试执行任务
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
验证主线程无法捕获线程池中异常
public static void main(String[] args) {
try {
System.out.println("主线程开始");
// 创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 执行线程方法
executor.execute(()->{
System.out.println("子线程运行开始");
int i = 1 / 0;
System.out.println("子线程运行结束");
});
executor.shutdown();
System.out.println("主线程结束");
} catch (Exception e) {
System.out.println("异常信息:" + e.getMessage());
}
}
在上面的代码中, 异常无法被捕获的原因是因为异常发生在子线程中,而主线程并不直接捕获这个异常。
execute方法提交任务后,异常会被线程池中的线程捕获并处理,但是这个异常处理是在线程内部进行的,不会传递到主线程中。
我们发现不论是使用execute方法或者submit方法提交任务, 都没有办法在主线程中捕获到异常, 有没有解决方式?
1.使用submit提交任务, 再通过阻塞方法等待任务完成, 如果这个过程中发生了异常, 会在主线程中被捕获
你可以通过Future对象的get方法来获取异常,但需要注意的是,如果任务执行过程中发生了异常,调用get方法会抛出ExecutionException,你需要在主线程中处理这个异常。
try {
System.out.println("主线程开始");
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
System.out.println("运行开始");
int i = 1 / 0;
System.out.println("运行结束");
});
executor.shutdown();
// 获取任务执行的结果,这里会阻塞直到任务完成
future.get();
System.out.println("主线程结束");
} catch (Exception e) {
System.out.println("捕获到异常:" + e.getMessage());
}
上述的方法使用submit提交任务, 那如果使用execute提交任务, 可以捕获线程池中的异常吗?
submit和execute方法的区别
submit和execute都可以提交任务, 两者有一些关键的区别
Callable的返回值类型是在实现接口时指定的, 例如下面这个例子
public class GetStrService implements Callable<String> {
private int i;
public GetStrService(int i) {
this.i = i;
}
@Override
public String call() throws Exception {
int t=(int) (Math.random()*(10-1)+1);
System.out.println("第"+i+"个任务开始啦:"+Thread.currentThread().getName()+"准备延时"+t+"秒");
Thread.sleep(t*1000);
return "第"+i+"个GetStrService任务使用的线程:"+Thread.currentThread().getName();
}
}
2.自定义ThreadPoolExecutor作为线程池
// 自定义线程池
class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("捕获到异常。异常信息为:" + t.getMessage());
System.out.println("异常栈信息为:");
t.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("主线程开始");
// 创建线程池
ExecutorService executor = new MyThreadPoolExecutor(5,50, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(20));
// 执行线程方法
executor.execute(()->{
System.out.println("子线程运行开始");
int i = 1 / 0;
System.out.println("子线程运行结束");
});
executor.shutdown();
System.out.println("主线程结束");
}