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