JAVA的内存区域
- 程序计数器:线程私有的,保存当前线程的字节码文件。
- JAVA虚拟机栈:包含局部变量信息,用于方法的调用和执行。
- 本地方法栈:与JAVA虚拟机栈类似,但只服务于本地方法。
- 堆:所有线程共享,存放对象实例变量,GC的主要区域。
- 方法区:所有线程共享,用于存放已经被JVM加载的类信息。
- 运行时常量池:所有线程共享,方法区的一部分,存储编译期间生成的字面量和符号引用
JAVA栈和堆的区别
- 栈主要用于存储局部变量和方法调用的上下文信息,栈中数据生命周期短暂,当元素调用结束时,占空间会被释放;每个线程有自己独立的栈空间,栈上的数据不能被其他线程访问和修改;堆栈上的数据访问速度更快(堆中数据涉及复杂的算法保证数据分配和回收)。
- 堆主要用于存储对象实例和变量,堆的生命周期不稳定,当对象不再被引用时,堆会自动调用GC进行回收,堆中的数据所有线程共享,可以被其他线程访问和修改。
JAVA虚拟机栈溢出情况
- 递归调用过深:当一个方法调用自身时,如果没有正确的终止条件或者终止条件不满足。
- 方法调用层次过深:层层嵌套调用。
- 线程过多
- 栈帧过大:方法中局部变量过多,或者单个局部变量过大
- JVM参数配置不当:JVM为每个线程分配的栈过小。
解决方法
- 优化递归逻辑,确保递归调用有明确的终止条件,如果可以,将递归改为迭代。
- 合理设置JVM参数,增加每个线程的大小。
- 避免无限制创建线程。
垃圾回收(GC)算法
- Serial:单线程,在回收垃圾的时候会暂停所有的事情。
- CMS:多线程,采用标记-清理方法产生大量空间碎片。
- G1:多线程,强化分区,将JVM堆划分为多个区域,每次选择最多的区域进行回收。
类加载器
类加载器是JVM加载类文件的组件,它负责将字节码文件加载到内存中并转化为.class对象。
类加载过程
- 加载:根据类的全限定名查找它的字节码文件并创建.class对象
- 验证:验证类文件格式是否正确
- 准备:为静态变量创建内存并初始化默认值
- 解析:将符号引用改为直接引用
- 初始化:之形类的静态初始化块以及对静态变量赋值
分类
启动类加载器、扩展类加载器和应用程序类加载器
双亲委派模型
当一个类加载器需要加载一个类的时候,它先让它的父类去加载这个类,如果父类无法加载它就自己加载,这主要是为了避免核心类库被重复加载。
进程和线程
- 进程:资源分配的基本单位,拥有独立的内存空间和资源,创建和销毁开销大,具有良好的错误隔离和稳定性。
- 线程:程序执行的基本单位,共享进程的内存资源,适合高效的并发和并行程序。
- 并发:一个系统能够处理多个任务,这些任务不需要同时执行
- 并行:同一时刻执行多个任务
用户线程:执行具体的业务逻辑,完成程序的核心功能。只要有一个用户线程在执行,JVM就不会退出。
守护线程:提供辅助功能,主要用于后台任务,它的生命周期取决于用户线程,一旦用户线程执行完毕,JVM会强制退出,所有的守护线程也会退出。
Start和Run方法
Start会创建并执行一个新的线程,Run只是执行当前线程不会启动一个新线程。
线程状态
- new:线程刚创建时候
- 就绪:在可运行线程池中等待CPU时间片
- 运行:获得CPU时间片开始运行
- 阻塞:等待某种条件的发生(等待锁的释放)
- 终止:线程执行完毕
Sleep和Wait
- Sleep是线程类的方法,它不会释放锁而是在指定时间后恢复对线程的执行;用于定时任务或者防止循环过快消耗CPU资源。
- Wait是Object类的方法,它会释放锁然后等待另一个线程用notify来唤醒;用于线程间的同步和通信。
锁
公平锁:保证线程按照它们申请的顺序获得锁,每个线程都会获得锁不会导致线程饥饿。
非公平锁:不保证线程按照它们申请的顺序获得锁,锁的获取和释放更快,但可能导致线程饥饿。
Sychronized锁
JAVA的关键字,用于线程之间互斥和同步,作用于方法和代码块;由JVM管理,简单易用,可重入,自动释放锁;它是自动释放锁,可能会导致线程饥饿,不可中断。保证互斥、原子性和可见性。
Sychronized锁升级
- 偏向锁:优化技术,减少无竞争情况下的同步代价。如果一个线程获得了偏向锁,它在接下来的的锁请求中无需进行同步操作,直接进入临界区。只有在其他线程尝试竞争这把锁的时候,偏向锁才会撤销(锁的头部会记录第一个获得锁线程的ID,当这个锁再次被线程请求的时候,无需CAS直接进入临界区)。适合于无竞争情况,减少无竞争下的同步开销。
- 轻量级锁:自旋锁,线程获取锁失败的时候不立即阻塞而是忙循环反复尝试获取锁。适合于竞争时间比较小的锁,避免线程阻塞和唤醒的开销。
- 锁粗化:将多个小范围锁合并成一个大范围的锁,减少加锁解锁的开销。
- 锁去除:在编译期间判断锁对象是否只在当前线程内使用,如果是则去除锁。
首先,Sychronized通过尝试获取偏向锁来竞争资源,如果能竞争到代表加锁成功,如果不行则需要将锁升级为轻量级锁,在轻量级锁状态下线程会根据自旋次数来抢占锁资源,如果还是无法竞争到,会升级到重量级锁,在重量级锁状态下,没有竞争到锁的线程会被阻塞。
Volatile
轻量级的同步机制保证变量的可见性和有序性,它通过将修改后的值直接刷新到内存中来确保其他线程对修改后的变量值立即可见;通过禁止指令重排序来达到有序性。
Lock
Lock最常用的实现类是Reentrantlock(可重入锁,允许一个线程多次获取同一个锁而不会阻塞)。它的锁的获取和释放需要显示调用lock()和unlock()方法,它可以被配置为公平锁或者非公平锁;提供了trylock()方法来中断锁,提供了更加强大的功能,是用于处理复杂场景。
Sychronized与Volatile区别
Volatile比Sychronized有更小的性能开销,但Volatile只保证可见性和有序性,不保证复杂操作的原子性,它只适合状态标志、配置参数等不涉及复杂逻辑的变量,在复杂场景下仍旧使用Sychronized。
Sychronized与Lock区别
Sychronized是JAVA关键字,Lock是JUC的接口;Lock的灵活性更高,它可以使用lock()和unlock()方法随时加锁、解锁,Sychronized只能在同步代码块执行完自动释放锁;Lock提供了非公平锁和公平锁,Sychronized只提供非公平锁;如果同步代码块中抛出异常,Sychronized会自动释放,Lock则需要在finally中显示释放,否则会导致死锁。总之,Sychronized提供了更加简单直接的同步机制,而Lock提供了功能更加强大更灵活的同步机制。
Threadlock
Threadlock是线程变量,为每个使用该变量的线程提供了一个变量副本,每个线程可以独立修改自己的变量副本而不受其他线程的影响。它的实现核心在于每个线程维护了一个ThreadlockMap的对象,该对象保存了自己的本地变量。
使用场景
- 线程的上下文切换
- 数据库连接
- 事务管理,事务处理中保存每个事务信息
- 对象跨层传递时,打破层级间的约束避免多次传递
可能会导致内存泄漏,因为ThreadlockMap键是弱引用,但是它的值是强引用。当Threadlock没有被其他引用时,它会被GC收回,但是它的值无法回收。每次使用Threadlock后要利用remove清除数据。
JAVA内存模型
- 可见性:线程对变量的修改对其他线程可见
- 原子性:操作是不可以分割的,要么完全成功要么失败
- 有序性:禁止指令重排序
- 工作内存和主内存:主内存是线程共享区域,工作内存是每个线程私有区域。工作内存中保留了线程所需要的变量副本,线程修改变量的时候是在工作内存中,修改完成后刷新到主内存中。
JAVA内存模型中的关键机制
- Volatile:可见性和有序性
- Sychroized:互斥、可见性、有序性和原子性
- happens-before:保证前一个操作在后一个操作之前且对后一个操作可见。
JAVA中常见的并发容器
- ConcurrentHashMap:采用分段锁的技术,将整个数据分为多个段,每个段使用独立锁,在JAVA 8以后,采用更加高效CAS和同步链表代替分段锁,进一步提高性能。
- CopyonWriteArraylist:写操作时,不直接修改原来的数组而是创建一个新的数组,使用新数组来代替就数组,在读多写少的情况下性能更高。
线程池
线程启动前就创建若干个线程等待响应,减少了频繁创建和销毁线程的开销,通过控制并发线程数量和合理的任务调度来更好地进行资源调度;避免在高并发下因为创建过多线程导致系统崩溃,提供了更好的系统稳定性;在完成任务后将线程归还到池中,避免线程未正确销毁导致的泄露。
线程池的参数
- 核心线程数:线程池中始终保持的线程数量,线程池会优先使用核心线程来执行任务
- 最大线程数:线程池中可以创建线程的上限
- 空闲线程存活时间:非核心线程在执行完任务后最久存活时间
- 任务队列:存储等待执行的线程,当前线程数达到核心线程的数量时,新提交的任务会保留在任务队列中
- 拒绝策略:当任务队列已满且达到最大线程数时,线程池无法接受新的任务
实现原理
线程池在创建时根据默认的参数配置核心线程,无论是否当前有任务需要执行,这些线程都会一直保留在线程池中。当需要执行任务时,线程池会优先使用这些线程去执行。如果当前没有空闲的核心线程,就会把这个任务放在任务队列中等待执行。如果任务队列已满但还未达到线程池中所能接受的最大核心数时,线程池会创建一个新的线程去处理这个任务。当任务队列已满且线程数达到最大线程数时,线程池采用拒绝策略停止继续工作。任务完成后,线程会返回空闲状态。核心线程不会回收,非核心线程会根据空闲线程存活时间进行回收。当线程池不在需要的时候,调用shutdown()和shutdownnow()来执行(shutdown()是等待已提交的任务完成后回收,shutdownnow()会尝试停止当前正在执行的任务然后回收)
线程池的拒绝策略
- 默认策略:直接抛出异常
- 调用者运行策略:提交任务的线程自己处理任务,降低任务提交速度
- 直接丢弃策略:丢弃无法处理的任务,不抛出异常
- 丢弃最旧策略:丢弃队列中最旧的未处理的任务
execute和submit区别
execute只能提交实现了Runnable接口的任务,不返回任何结果,当出现异常时会直接抛出异常。submit可以提交实现了Runnable和Callable接口的任务,对于Callable任务会返回一个Future对象,这个对象可以检查任务是否完成,如果遇到异常允许调用者捕获并处理异常。