Central Topic
2024-07-10 20:14:50 1 举报
AI智能生成
登录查看完整内容
11
作者其他创作
大纲/内容
扩容规则
1. 基于数组,需要连续内存2. 随机访问快(指根据下标访问)3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低4. 数据局部性:由于ArrayList 的元素是存储在连续的内存位置上,访问这些元素时可以利用到 CPU 的数据缓存。这是因为现代 CPU 通常会将连续的内存块加载到高速缓存中,以提高数据访问速度
特性
ArrayList
1、基于双向链表,无连续内存2、随机访问慢(需要遍历链表)3、头尾插入删除性能高
LinkedList
迭代器通过构造方法初始化会记录数组大小modCount集合大小expectedModCount需要遍历多少次通过checkForComodification()检查,如果不相等则抛出异常
遍历本质是Iteratoor
Iterator
总结
1.7版本
1.8版本
链表意义
1、红黑树用来避免dos攻击,防止链表超长超时性能下降,树化是保底策略
红黑树意义
树化规则1、当链表长度超过树化阈值 8 时,先检查数组容量是否>=64情况1:触发扩容,重新hash取模,减少链表长度情况2:触发树化2、退化规则情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表,退化为链表可以节省内存空间和提高性能情况2:remove 树节点时,树结构过于分散或不平衡,或者删除节点后的数量小于等于 6,则可能会选择将树退化为链表,果在树中某个节点的左子节点、右子节点或左子节点的左子节点为 null,也会选择将树退化为链表
树化规则
1、hashMap是懒惰创建数组,首次使用才创建数组2、计算hash取值3、节点插入之前,检查当前元素数量是否已经超过了容量乘以负载因子的阈值(0.75)。超过则触发扩容操作,将数组的容量翻倍3、如果数组下标没人占用,创建Node占位返回4、如果数组下标被占用a、已是TreeNode走红黑树的添加和更新b、是普通Node,走链表的添加和更新,如果链表长度超过树化阈值,走树化逻辑
1.7和1.8区别1、链表插入节点时,1.7 是头插法,1.8 是尾插法2、1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容3、1.8 在扩容计算 Node 索引时,会优化(红黑树)
区别
流程
1. 在空间占用与查询时间之间取得较好的权衡2. 大于这个值,空间节省了,但链表就会比较长影响性能3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
扩容(加载)因子为何默认是 0.75f
HashMap
public class Singleton { private Singleton() { System.out.println(\"private Singleton()\"); } private static final Singleton INSTANCE = new Singleton(); public static Singleton getInstance() { return INSTANCE; }}
1、会被反射、序列化破坏
缺点
实现方式1
public class Singleton implements Serializable { private Singleton() { System.out.println(\"private Singleton()\"); } private static final Singleton INSTANCE = new Singleton(); public static Singleton getInstance() { return INSTANCE; } public Object readResolve() { return INSTANCE; }}
防止被序列化破坏
实现方式2
public class Singleton implements Serializable { private Singleton() { if (INSTANCE != null) { throw new RuntimeException(\"单例对象不能重复创建\"); } System.out.println(\"private Singleton()\"); } private static final Singleton INSTANCE = new Singleton(); public static Singleton getInstance() { return INSTANCE; } public Object readResolve() { return INSTANCE; }}
防止被构造破坏
实现方式3,究极结合体
public enum Singleton2 { INSTANCE; private Singleton2() { System.out.println(\"private Singleton2()\"); } @Override public String toString() { return getClass().getName() + \"@\" + Integer.toHexString(hashCode()); } public static Singleton2 getInstance() { return INSTANCE; } }
枚举饿汉式能天然防止反射、反序列化破坏单例
枚举饿汉式单例
public class Singleton3 implements Serializable { private Singleton3() { System.out.println(\"private Singleton3()\"); } private static Singleton3 INSTANCE = null; // Singleton3.class public static synchronized Singleton3 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; } public static void otherMethod() { System.out.println(\"otherMethod()\"); }}
同步锁结合static 锁的是当前Class
懒汉式
public class Singleton4 implements Serializable { private Singleton4() { System.out.println(\"private Singleton4()\"); } /* * * `INSTANCE = new Singleton4()` 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造 * 如果线程1 先执行了赋值,线程2 执行到第一个 `INSTANCE == null` 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象 * */ private static volatile Singleton4 INSTANCE = null; // 可见性,有序性 public static Singleton4 getInstance() { if (INSTANCE == null) { synchronized (Singleton4.class) { if (INSTANCE == null) { INSTANCE = new Singleton4(); } } } return INSTANCE; } public static void otherMethod() { System.out.println(\"otherMethod()\"); }}
双检锁懒汉式
public class Singleton5 implements Serializable { private Singleton5() { System.out.println(\"private Singleton5()\"); } private static class Holder { static Singleton5 INSTANCE = new Singleton5(); } public static Singleton5 getInstance() { return Holder.INSTANCE; } public static void otherMethod() { System.out.println(\"otherMethod()\"); }}
内部类懒汉式
* Runtime 体现了饿汉式单例* Console 体现了双检锁懒汉式单例* Collections 中的 EmptyNavigableSet 内部类懒汉式单例* ReverseComparator.REVERSE_ORDER 内部类懒汉式单例* Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例
JDK 中单例的体现
饿汉式
单例模式
新建(New):当线程对象被创建但还没有调用 start() 方法时,线程处于新建状态。运行(Running):线程获取到CPU时间片后,开始执行线程的 run() 方法,此时线程处于运行状态。阻塞(Blocked):线程由于某些原因被阻塞,无法继续执行,比如等待一个锁、等待输入输出等待等情况,线程会进入阻塞状态。等待(Waiting):线程进入等待状态是因为调用了 Object.wait()、Thread.join() 或 LockSupport.park() 等方法,线程会一直等待直到其他线程调用notify()或notifyAll()来唤醒它。超时等待(Timed Waiting):线程进入超时等待状态是因为调用了带有超时参数的方法,比如 Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis) 等,线程会在指定的时间之后自动唤醒。终止(Terminated):线程执行完毕或者由于异常退出了 run() 方法,线程将进入终止状态。
Java线程状态
创建态(New):当进程被创建时,处于创建态。在这个状态下,操作系统正在为进程分配资源和初始化。就绪态(Ready):表示进程已经准备好执行,正在等待分配CPU时间片。运行态(Running):表示进程目前正在执行指令。阻塞态(Blocked):表示进程由于某些原因(比如等待I/O完成)而暂时无法执行,处于等待状态。结束态(Terminated):表示进程已经完成任务或被终止,不再执行。
操作系统
线程池初始化的固定核心线程数不会死亡
核心线程数
当队列任务数上限且核心线程数上限才会创建的线程,最大线程数包括核心线程数
最大线程数
非核心线程的空闲时间
存活时间
通常单位秒,毫秒
存活时间单位
Runtime.getRuntime().availableProcessors()
获取设备cpu核心数
当核心线程上限后,则任务会存放队列,队列上限后,才创建非核心线程数执行
ArrayBlockingQueue:基于数组的有界阻塞队列,先进先出,当队列上限,阻塞新任务,直到有空间可用
LinkedBlockingQueue:基于链表的可选有界或无界阻塞队列,先进先出,当队列有界上限,阻塞新任务,直到空间可用,无界则不会阻塞插入
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。这种队列在任务传递过程中直接传递,而不保存它们。可以被用于传递实时消息
PriorityBlockingQueue:基于优先级的无界阻塞队列,按优先级处理,任务必须实现Comparable接口或者队列定制的Comparator决定顺序
DelayQueue:一个支持延迟获取元素的无界阻塞队列,其中的元素只有在其指定的延迟时间到了才能被取出。该队列是无界的,而且不允许插入null元素
LinkedTransferQueue:一个无界队列,可以用于生产者-消费者模式。与其他队列不同,它允许生产者线程将元素直接传递给消费者线程,而不需要将元素存储在队列中。
WorkStealingPool:Java 8 引入的一种特殊类型的线程池,其工作队列是由Deque(双端队列)实现的,每个线程都有自己的工作队列。空闲的线程会从其他线程的队列中窃取任务来执行,以提高整体的任务处理能力
7种队列类型
队列
用于创建新线程的工厂。通过指定线程工厂,可以自定义线程的创建过程,例如设置线程的名称、优先级等
实现ThreadFactory接口
实现方式
线程工厂
AbortPolicy:默认,当线程池无法接受新任务时,会抛出RejectedExecutionException异常
CallerRunsPolicy:当线程池无法接受新任务时,会使用提交任务的线程来执行该任务,可以降低新任务的流量,但可能会降低整体性能
DiscardPolicy:当线程池无法接受新任务时,会直接丢弃该任务,不会抛出异常。这可能会导致部分任务丢失,慎用
DiscardOldestPolicy:当线程池无法接受新任务时,会丢弃工作队列中最早加入(未被执行)的任务,然后尝试重新提交新任务
4种拒绝策略
实现RejectedExecutionHandler接口
自定义策略实现方式
拒绝策略
1、线程池创建,初始化核心线程数,准备接受任务2、新的任务进来,使用空闲的核心线程数执行 (1)、核心线程数上限后,将进来的任务放入阻塞队列,空闲的核心线程会自动取阻塞队列获取任务执行 (2)、阻塞队列上限后,就创建新线程执行,最大只能创建到最大线程数,也就是(最大线程数 - 核心线程数) (3)、最大线程数都执行任务完成后,会根据指定的空闲时间自动销毁,最终保持到核心线程数大小 (4)、如果线程数开到了最大线程数,还有新任务进来,就会使用reject指定的拒绝策略3、所有的线程创建都是由指定的Factory创建的
任务执行流程
7大核心参数
ThreadPoolExecutor
1、RUNNING:运行状态,线程池新建或调用execute()方法后,处于运行状态,能够接受新的任务 2、SHUTDOWN:关闭状态,调用shutdown()方法后,变为关闭状态,不在接受新的任务,但会执行已提交的等待队列中的任务 3、STOP:停止状态,调用shutdownNow()方法后,变为stop状态,不在接受新的任务,并且会中断正在处理的任务 4、TIDYING:当线程池处于Shutdown或Stop状态时,如果等待队列中还有未执行的任务,则线程池将执行整理操作,将等待队列中的未执行任务移除,并保存到一个列表中 5、TERMINATED:当线程池处于Shutdown状态,并且等待队列中的任务全部执行完毕,或者在Stop状态下,线程池内部的所有线程都已经终止时,线程池进入Terminated状态
线程池状态
线程池
线程
偏向锁:当一个线程访问同步块时,如果这个同步块未被其他线程访问过,并且没有锁竞争,会尝试获取偏向锁,并将对象头的进程ID设置为自己的ID,标记为偏向模式,在未来访问中,线程进入同步块,无需在进行任何同步操作,提高性能
轻量级锁:当偏向锁失败时,表示有其他线程尝试竞争同步块,这时JVM会尝试升级为轻量级锁,轻量级锁获取是通过CAS操作来实现,如果,某个线程获取了轻量级锁,则表示它独占同步资源,如果其他线程也想获取这个锁,也会发生自旋,而不是阻塞线程,自旋的目的是等待持有锁的线程释放锁,如果自旋失败,表示有多个线程竞争锁,则轻量级锁会升级为重量级锁
重量级锁:当自旋超过一定次数或持续时间后,轻量级锁会升为重量级锁,重量级锁会导致竞争失败的线程进入阻塞等待,直到持有锁的线程释放锁,重量级锁使用操作系统的互斥量来实现线程的阻塞和缓存,性能开销大,但确保了线程的安全
锁升级过程
核心:轻量级锁,会去使用CAS不断自旋获取锁,目的是为了避免频繁地将现场在用户态和内核态之间切换,从而减少操作系统介入的开销
1、可重入锁、重量级锁、同步锁、非公平锁,不可中断2、Java层面的关键字,Jvm的锁3、自动加锁,解锁,不可获取当前线程是否上锁4、锁的是对象,锁信息在对象的对象头中5、底层有锁的升级过程
Synchronized
1、Java层面的类,Api层的锁,公平锁与非公平锁,可重入锁2、手动加锁与释放锁3、可获取当前线程是否上锁4、可中断 1、可设置超时时间tryLock() 2、可调用lockInterruptibly()为了线程在获取锁的过程中能够响应中断请求interrupt() 方法是用来中断线程的执行5、通过state判断锁状态
ReentrantLock
1、Java层面关键字2、修饰变量,具有可见性、有序性、防止指令重排3、缺点:不能保证原子性当线程修改Volatile变量会导致其他线程的工作内存失效,从而去读取主内存,保证可见性由于应用层面在操作系统层面是很慢的,在多线程情况下,它会有个优化,导致指令重排,Volatile是借助于内存屏障实现有序性,防止指令重排
public class AddAndSubtract { static volatile int balance = 10; public static void subtract() { int b = balance; b -= 5; balance = b; } public static void add() { int b = balance; b += 5; balance = b; } public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(2); new Thread(() -> { subtract(); latch.countDown(); }).start(); new Thread(() -> { add(); latch.countDown(); }).start(); latch.await(); System.out.println(balance); }}
原子性:每一段的代码都应该是不可插入的部分,因为CPU是交互切换执行,所以并发情况是A代码刚到读取数据缓存了,正好切换到B代码修改数据了,此时的变量是无感知的,导致A代码使用的是旧数据
可见性:说法1,在多线程在内存中读取变量的数据,在自己的本地缓存中缓存了一遍,所以导致会存在脏数据问题
Volatile
悲观锁的代表是 synchronized 和 Lock 锁* 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】* 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能* 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
悲观锁
乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性* 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】* 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换* 它需要多核 cpu 支持,且线程数不应超过 cpu 核数
乐观锁
1、读取内存地址中的当前值。2、检查当前值是否等于期望值。3、如果相等,则将新值写入内存地址;否则,操作失败。4、返回操作是否成功的结果。
1、比较并交换,解决并发访问问题2、有三个值,内存地址(变量的引用),期望值,旧值3、非阻塞的,不需要加锁,因此在一些高并发的情况下可以提供更好的性能
如果一个值原来是A,后来被改成了B,再被改回A,那么CAS操作会误认为这个值没有被改变过。为了解决这个问题,可以使用版本号或者标记来区分不同的状态
ABA
循环开销:由于CAS操作是一个循环,如果某个线程长时间不成功,会导致CPU的消耗增加
CAS
* 数据结构:`Segment(大数组) + HashEntry(小数组) + 链表`,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突* 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
初始化时采用的设计模式饿汉式初始化,默认创建segment[0]容量,后续创建segment的时候 都是以原型模式去借用segment[0]的属性
1.8之前
* 数据结构:`Node 数组 + 链表或红黑树`,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能* 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容* 扩容条件:Node 数组满 3/4 时就会扩容
1.8之后
concurrentHashMap
1、线程同步器2、使用一个整型的状态(state)来表示同步资源的状态,同时使用一个等待队列来管理处于等待状态的线程3、AQS通过一个FIFO双向队列(即等待队列)来管理多个线程的竞争和等待状态,并提供了一些方法供子类来管理和控制这个队列4、根据信号量来控制可重入锁,加锁state +1,解锁 -15、acquire(int arg) 当一个线程尝试获取同步资源时,如果失败(即无法满足获取条件),则会将线程加入到等待队列中,并进入阻塞状态,直到资源可用。6、release(int arg) 当一个线程释放同步资源时,会尝试唤醒等待队列中的其他线程,让它们有机会获取到资源
AQS:AbstractQueuedSynchronizer
1、Java实现线程局部变量的类,允许每个线程都有自己独立的副本,在多线程环境下实现数据的隔离2、核心使用ThreadLocalMap,内部维护的散列表,储存每个线程的局部变量,ThreadLocalMap键是ThreadLocal实例。值是线程特定的变量副本当一个线程访问 ThreadLocal 的 get() 方法时,实际上是根据当前线程获取对应的 ThreadLocalMap,然后根据 ThreadLocal 实例获取其对应的值
ThreadLocal
JUC
由线程私有的,程序计数器,虚拟机栈,本地方法栈,线程共享的,方法区,堆组成
1、创建JVM,编译成class文件,JVM对其检查class是否是原生文件,不是则通过类加载子系统加载,将类的原始信息存入方法区2、创建主线程使用的是虚拟机栈执行,如果遇见了未见过的类,会继续出发类加载过程,同样存入方法区3、需要创建对象,会使用堆内存来存储对象执行 java 命令1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入**方法区**2. 创建 main 线程,使用的内存区域是 **JVM 虚拟机栈**,开始执行 main 方法代码3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入**方法区**4. 需要创建对象,会使用**堆**内存来存储对象5. 不再使用的对象,会由**垃圾回收器**在内存不足时回收其内存6. 调用方法时,方法内的局部变量、方法参数所使用的是 **JVM 虚拟机栈**中的栈帧内存7. 调用方法时,先要到**方法区**获得到该方法的字节码指令,由**解释器**将字节码指令解释为机器码执行8. 调用方法时,会将要执行的指令行号读到**程序计数器**,这样当发生了线程切换,恢复时就可以从中断的位置继续9. 对于非 java 实现的方法调用,使用内存称为**本地方法栈**(见说明)10. 对于热点方法调用,或者频繁的循环代码,由 **JIT 即时编译器**将这些代码编译成机器码缓存,提高执行性能
Java类编译过程
不会出现内存溢出的区域 – 程序计数器出现 OutOfMemoryError 的情况 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时 出现 StackOverflowError 的区域 JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用
会发生内存溢出的区域
方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间
方法区、永久代、元空间
第一次,从根节点(如虚拟机栈中的引用、静态变量等)开始,递归地遍历所有可达对象,并标记它们第二次,清除未标记的对象缺点,内存碎片化太严重
标记清除法
第一次,从根节点(如虚拟机栈中的引用、静态变量等)开始,递归地遍历所有可达对象,并标记它们第二次,清除未标记的对象,移动所有存活的对象,使它们在堆中连续分布缺点,效率比较低,标记整理算法需要移动存活对象,可能会导致一定的延迟,特别是在堆中存活对象较多时
标记整理法
第一次,从根节点(如虚拟机栈中的引用、静态变量等)开始,递归地遍历所有可达对象,并标记它们为活动对象第二次,将所有活动对象从当前使用的内存区域(比如 Eden 区)复制到另一个内存区域(比如 Survivor 区或者老年代)。同时,可能会进行对象的年龄提升(Promotion)操作,将存活时间较长的对象移动到老年代第三次,清除未被标记的对象,即垃圾对象。在复制阶段之后,原来的内存区域变成了不包含存活对象的内存块,可以直接清空或者重置为可用状态缺点,需要额外的空间作为复制目标,通常是原来空间的一半大小,移动存活对象可能会引起一定的延迟,特别是当存活对象较多时
标记复制法
JVM垃圾回收算法,可达性分析法
JVM
技术栈总结
Central Topic
![Central Topic](https://www.processon.com/chart_image/template/thumb/668e7b3a11f8f46a4896c383.png?tid=6661681aea15cf738b9782ac)
收藏
0 条评论
回复 删除
下一页