多线程
并发和并行的区别:
并发:同一时间,多个线程抢占同一个资源
并行:我一边做A的事情,也做B的事情,例:一边听歌一边打游戏
谈一下volatile *
volatile 是JVM提供的轻量级的同步机制,具有三大特性: 可见性、不是原子性,有序性。
该变量被volatile修饰,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
(单例模式双重校验锁变量为什么使用 volatile 修饰,目的就是禁止指令重排)
什么是CAS*
CAS就是比较并交换,可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。
缺点:
1.只能支持一个变量的原子操作
2.CAS频繁自旋导致CPU开销大
3.ABA问题(线程1和线程2同时去修改一个变量,线程1把值改1,又改为0,线程2获取值是0,它不知道线程1改过,一般加上版本号就可以解决)
CAS底层原理:
利用自旋锁和Unsafe类,可以不加Synchrpnized也可以保证原子性;Unsafe类是根据内存偏移地址获取数据,该类里面很多方法都是native修饰的,来调用本地方法栈,本地方法栈都是C语言,
如何保证ArrayList的线程安全?
1.collentions.synchronizedList()
2.Vector (每个方法都由synchronized修饰,效率低)
3.juc包下的CopyOnWriterArrayList
CopyOnWriterArrayList是如何解决线程安全
它的思想就是读写分离,读的时候不加锁,写的时候,进行加Lock锁,然后复制一个数据,再新数组上进行扩容改值,然后替换原先的数据,再解锁,来保证线程安全;
List list = Collections.synchronizedList(new ArrayList());
HashSet底层是什么
HashMap,只是HashSet只记录key,不记value
如何保证HashSet的线程安全?
1.collentions.synchronizedSet()
2.juc包下的CopyOnWriteArraySet
如何保证HashMap的线程安全?*
1 使用ConcurrentHashMap
2 使用HashTable
3 Collections.synchronizedHashMap()方法
ConcurrentHashMap如何保证的线程安全 *
JDK1.7: 使用分段锁,底层是一个大的hashmap下面有多个小个hashmap,默认是16个,每个段都是一个小的hashmap,每次操作只对其中一个段加锁。
JDK1.8: 也是分段锁,只是采用CAS+Synchronized,jdk8也保留到大数组segment,添加元素,首先判断容器是否为null,如果为null会使用volatile和CAS初始化,如果不为null会根据key计算出的该位置是否为null,也就是链表一个节点,如果为null,利用CAS设置该节点。如果不为null,就使用synchronized加锁,遍历或者添加数据。
(每次插入数据时判断在当前数组下标是否是第一次插入,是就通过CAS方式插入,然后判断f.hash是否=-1,是的话就说明其他线程正在进行扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保证并发下移除元素安全)
什么是非公平锁和公平锁
非公平锁:先到先得,谁先抢到谁就获取到锁(默认)
公平锁:按照申请锁的顺序来获取锁
什么是可重入锁、自旋锁、独占锁和共享锁
可重入锁:也是递归锁,外层获取锁,里面的锁也获取到了,避免死锁,可重入锁,前提是同一个对象
自旋锁:尝试是获取锁,采用循环的方式获取锁资源,不会发生阻塞
独占锁:指该线程只能被一个线程持有
共享锁:指该线程可被多个线程持有
LockSupport理解 *
它是实现线程同步和线程之间的通信,它所有方法是静态的,它有2个方法,是park(),unpark(),分别是阻塞和唤醒。
LockSupport.park()不会释放锁资源,只是阻塞当前线程,它与wait()、await()更加灵活,不需要再同步代码块执行,唤醒和阻塞顺序变化也不会报错;
Thread a = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + "开始执行 ---- ");
LockSupport.park();
System.out.println(name + "被唤醒-成功");
}, "A线程");
a.start();
TimeUnit.MILLISECONDS.sleep(3000);
new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + "开始执行 ---- ");
LockSupport.unpark(a);
System.out.println(name + "唤醒线程A 1---- ");
}, "B线程").start();
CountDownLatch、CyclicBarrier、Semaphore的使用
CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。
【应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行】
CountDownLatch latch = new CountDownLatch(6);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "任务");
latch.countDown(); // 减-
}, "线程" + i).start();
}
latch.await(); // 当减到6 就不再阻塞
System.out.println("完成");
CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值
CyclicBarrier cyc = new CyclicBarrier(7, () -> {
System.out.println(Thread.currentThread().getName() + "我来了,每满足7个线程,就执行该逻辑");
});
for (int i = 0; i < 15; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "任务");
try {
cyc.await(); // 会阻塞
} catch (Exception e) {
e.printStackTrace();
}
}, "线程" + i).start();
}
Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。
【应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票】
Semaphore sem = new Semaphore(2);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
sem.acquire();
System.out.println(Thread.currentThread().getName() + "抢到资源");
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + "释放资源");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
sem.release(); // 释放资源
}
}, "线程" + i).start();
}
阻塞队列是什么,分为有什么
阻塞队列:队列是空时,一个线程获取元素会被阻塞;队列是满了,一个线程插入元素会被阻塞;
ArrayBlockingQueue 基于数组的阻塞队列(先进先出)
LinkedBlockingQueue 基于链表的阻塞队列(先进先出)
SynchronousQueue 线程安全带阻塞队列,进去一个元素,必须等待取出来之后,才能再往里面放入一个元素
多线程并发需要注意什么
如有判断,尽量使用while代替if语句,避免虚假唤醒
Synchrpnized和Lock的区别 *
synchronized是JVM层面的,是关键字,lock是一个类,有很多api
synchronized在发生异常时会自动释放锁,lock需要手动释放锁
synchronized是非公平锁,lock可以设置公平锁和非公平锁
lock比较灵活,他的唤醒可以指定某个线程唤醒,可中断锁
创建线程有哪些方式
继承Thread类
实现runnable接口
实现Callable接口
通过线程池创建
为什么要用线程池,优势是什么
作用:线程复用,控制最大的并发,进行管理线程
优势1:降低资源消耗,减少了创建和销毁线程的次数
优势2:提高响应速度
优势3:方便同一管理
池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
如何使用线程池
使用Executors工具类,快速创建线程池
使用ThreadPoolExecutor创建线程池,也是推荐的
ThreadPoolExecutor有哪些参数 *
有7大参数:
核心线程池大小
最大的线程池大小
最大线程的存活时间
最大线程的存活时间单位
阻塞队列 :1 基于数组的有界阻塞队列 2基于链表的无界阻塞队列 3一个不缓存任务的阻塞队列 4优先级的无界阻塞队列
线程工厂
拒绝策略 :有四种策略:1 抛异常 2 丢弃任务不抛异常 3 打回任务 4 尝试与最老的线程竞争
如何配置线程池的参数
# 查看CPU核数 :Runtime.getRuntime().availableProcessors()
CPU密集型 :CPU核数 + 1
IO密集型分为2种
1. CPU核数*2 (一般IO不会让CPU一直执行,可以设置大些)
2. CPU核数 / 阻塞系数 例子:(1-0.9 or 1-0.8)
产生死锁的原因
死锁指的是多个线程在执行过程中,因抢夺资源造成的一种相互等待的僵局
造成的原因有:系统资源不足、进程运行的顺序不合适、资源分配不当
如何排查死锁
# 查看java的进程信息,可得到进程id
jps -l
# 打印出给定的java进程ID的堆栈信息
jstack 进程id
# 如出现 deadlock 就是死锁,注意:需要在java的当前目录执行
预防死锁
使用定时锁、尽量让线程用相同的加锁顺序
进程和线程的区别,进程间如何通信 *
进程:一个系统运行着很多进程,进程在运行过程中都是相互独立。
线程:一个进程包含多个线程,它们共享同一进程内的系统资源。
进程间通过管道、 共享内存、信号量机制、消息队列通信。
什么是线程上下文切换
当一个线程被剥夺cpu使用权时,切换到另外一个线程执行
什么是AQS锁 *
AQS是一个抽象类,它有三个核心:
1. state 状态,代表加锁状态,初始值是0 (state是被volatile修饰)
2 是获取到锁的线程
3 还有一个阻塞队列(双向队列)
它的原理:
当有线程想获取锁时,其中一个线程以CAS的将state变为1,将加锁线程设为自己。
当其他线程来竞争锁时会,判断state是不是0,不是自己就把自己放入阻塞队列种。(这个阻塞队列是用双向链表实现)
当这个线程使用完,会把state变为0
(可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1,释放锁的时候就将state-1。当state减到0的时候就去唤醒阻塞队列的第一个线程)
(Node节点绑定当前线程,后续的节点是否被唤醒)
为什么AQS使用的双向链表
因为有一些线程可能发生中断 ,发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点
有哪些常见的AQS锁
分为:独占锁和共享锁
1 ReentrantLock(独占锁):可重入,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队
2 Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。
【应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票】
3 CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。【应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行】
4 CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值
【应用场景:多线程计算数据,最后合并计算结果的应用场景】
sleep()和wait()的区别 *
1 wait()是Object的方法,sleep()是Thread类的方法
2 wait()必须获得锁,才可以调用,让线程进入等待状态,会释放锁,sleep()是休眠,不会释放锁
3 wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒
4 wait()要在同步方法或者同步代码块中执行,sleep()没有限制
yield()和join()区别
yield() :让出CPU调度,调用后线程进入就绪状态,让CPU让步,有可能再次命中选中。
join() :A线程中调用B线程的join() ,则B执行完前A进入阻塞状态
线程的5个状态
1 新建状态(New) 、2 就绪状态(Runnable)、3 运行状态(Running)、4 阻塞状态(Blocked)、5 死亡状态(Dead)
Java内存模型
JMM(Java内存模型 ):定义了JVM如何将程序中的变量在主存中读取
所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的。
由于主存与工作内存之间有读写延迟,所以会有线程安全问题。
保证并发安全的三大特性
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
有序性:禁止指令重排
AtomicInteger的使用场景
AtomicInteger是一个提供原子操作的Integer类,使用CAS+volatile实来现线程安全的数值操作。适合在高并发的时候使用。
ThreadLocal原理 *
每个线程内部都维护了一个Map,key为threadLocal实例,value为要保存的副本,不同线程之间不可见,来保证线程安全。
使用ThreadLocal会存在内存泄露问题,因为key为弱引用,而value为强引用,每次gc时key都会回收,而value不会被回收。
避免这种情况:可以在每次使用完后删除value或者使用static修饰。
JVM
JVM运行时数据区(内存结构)*
线程私有区:
虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧
本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一
程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
线程共享区:
堆内存:Jvm进行垃圾回收的主要区域,存放对象信息,分为新生代和老年代,内存比例为1:2,新生代的Eden区内存不够时时发生轻GC,老年代内存不够时发生重GC
方法区:存放类信息、静态变量、常量、运行时常量池等信息。JDK1.8之前用持久代实现,JDK1.8后用元空间实现,元空间使用的是本地内存,而非在JVM内存结构中
什么情况下会内存溢出?
堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时
栈溢出:方法调用次数过多,一般是递归不当造成
JVM有哪些垃圾回收算法?
1 标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。
2 复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。(新生代)
3 标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。(老年代)
(不同的堆内存,使用不同的回收算法)
GC如何判断对象可以被回收
1 引用计数法:每个对象有一个引用计数器,新增一个引用时计数器加1,引用释放时计数器减1,当计数为0时,可以回收(一般不推荐)
2 可达性分析法:从GCRoot开始往下搜索,搜索过的路径称为引用链,若一个对象GCRoot没有任何的引用链,则判定可以回收
(虚拟机栈中引用的对象,方法区中静态变量引用的对象,本地方法栈中引用的对象)
什么是GCRoot
用来判断对象是否可以回收,GCRoot作为对象的起始点,一个对象创建后被引用一定在GCRoot下,没有在GCRoot下的对象,就会被回收掉
那些对象可以作为GCRoot对象
1.虚拟栈里的局部变量引用的对象
2.方法区里的静态属性引用的对象
3.方法区里的常量引用的对象
4.本地方法栈中native引用的对象
JVM内存参数有哪些 *
-Xmx: 堆空间最大内存 ,默认是物理内容 1/4
-Xms: 堆空间最小内存,一般设置成跟堆空间最大内存一样的 , 默认是物理内容 1/64
-Xmn: 新生代的最大内存,一般新生代是堆的 1/3 老年代是堆的2/3
-Xss: 设置单个栈大小 ,默认是 512k - 1024k之间
# 一般设堆空间为最大可用物理地址的百分之80
-XX:+PrintGCDetails
# 打印GC细节信息,结果会有:PSYoungGen 新生代、ParOldGen 老年代、Metaspace 元空间
# 如果OOM,会打印出GC细节信息,打印出以下信息
# 新生代:GC前空间大小->GC后空间大小(新生代总共大小) GC前JVM堆空间大小->GC后JVM堆空间大小(JVM堆总共大小)GC耗时 [用户耗时 系统耗时 实际耗时]
[PSYoungGen: 2048K->512K(2560K)] 2048K->977K(9728K), 0.0014224 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-XX:SurvivorRatio=4
# 设置新生代里的伊甸园区和幸存区的比例, 默认是8 比例:8:1:1
-XX:NewRatio=2
# 设置老年代的占比,默认是2,新生代占总内存比例是1,老年代占总内存比例2,如果值为3,就是新生代是1,老年代是3
-XX:MaxTenuringThreshold=15
# 设置对象经过多少次GC,会进入到老年代,默认是15,java8,最大只能设置15,范围在0-15之间
# springboot jar包启动及JVM参数设置 ,进行打包
java -server -Xms512m -Xmx1024m -jar jar包文件.jar
java -server -Xmx4G -Xms4G -XX:+UseG1GC # 使用G1
java -server -Xmx4G -Xms4G -XX:+UseConcMarkSweepGC # 使用CMS
JVM如何调优,如何查看JVM系统默认值
# 查看进程
jps -l
# 方式1:查看JVM参数信息,(Command line:该命令下是自己设置的配置参数)(默认JVM堆内存是总内存的四分之一)
jinfo -flags 进程id :
jinfo -flag -XX:NewSize 进程id # NewSize 指定名称的参数
# 方式2:查看JVM参数启动的初始值
java -XX:+PrintFlagsInitial
# 查看是否手动修改JVM配置(没有改过初始值就是 = ,如果改过就是 := 可能是JVM自己改的)
java -XX:+PrintFlagsFinal
# 更改某个JVM配置
java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512 T
# 方式3:查看JVM默认的垃圾回收器
java -XX:+PrintCommandLineFlags -version
# 结果
-XX:InitialHeapSize=259054080 -XX:MaxHeapSize=4144865280 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_281"
{
InitialHeapSize :起始堆内存大小
MaxHeapSize :最大堆内存大小
UseCompressedClassPointers :默认打开Class指针压缩
UseParallelGC :并行垃圾收集器 、 UseSerialGC :串行垃圾收集器
}
JAVA打印Java虚拟机信息参数
// CPU核数
System.out.println(Runtime.getRuntime().availableProcessors());
// Java虚拟机中的内存总量
System.out.println(Runtime.getRuntime().totalMemory());
// Java虚拟机中的最大内存量
System.out.println(Runtime.getRuntime().maxMemory());
// Java虚拟机中的可用内存量
System.out.println(Runtime.getRuntime().freeMemory());
JVM内存模型,为什么要分新生代和老年代,对于新生代,为什么要分eden区和survial区
因为有些垃圾回收器是分代进行垃圾回收,不同的代使用不同的垃圾回收器,
对于新生代,为什么要分eden区和survial区,因为新创建的对象在不使用的时候要及时回收,更细化新生代内部,有利于快速腾出连续空间,避免空间的浪费,把继续已使用的对象移动到survial区,然后eden区有更多的空间存新创建的对象。
JVM中有哪些引用
强引用:new的对象。哪怕内存溢出也不会回收
软引用:只有内存不足时才会回收
弱引用:每次垃圾回收都会回收
虚引用:只在被垃圾收集器回收时才会被使用
强引用、软引用、弱引用、虚引用的区别
强引用:只要该对象被引用,就不会被 GC 回收;
例: Object obj = new Object();
软引用:当堆内存不足时,会被 GC回收;
例:SoftReference<String> softRef = new SoftReference<>("hello");
弱引用:对象没被引用,就会被 GC 回收;
例:WeakReference<String> weakRef = new WeakReference<>("hello");
虚引用: 只在被垃圾收集器回收时才会被使用,在被 GC前,会放在引用队列中,看有没有遗言,然后被GC回收;
# 在缓存中间件底层使用大量的弱引用,ThreadLocal也是;
WeakHashMap 也是弱引用,适合做缓存,如果key没有其他引用,会被GC 回收;
谈谈你对OOM理解 *
1 栈内存溢出:java.lang.StackOverflowError
2 堆内存溢出:java.lang.OutOfMemoryError: Java heap space
3 垃圾回收上头:java.lang.OutOfMemoryError: GC overhead limit exceeded
(一般CPU使用率很高,每次GC回收都回收的内容很少,效果不好,会出现这种异常)
4 内存溢出:java.lang.OutOfMemoryError: Direct buffer memory
(一般这个出现NIO,利用ByteBuffer,写文件,先写入缓存中,再写入文件中,缓存中的大小默认是64M,缓存中存不下就会这种异常)
5 线程创建过多:java.lang.OutOfMemoryError: Unable to create new native thread
(线程数超过操作系统最大线程数限制,linux最大是1024个,默认是几核就是几个线程,可以改linux的线程数)
6 元空间溢出:java.lang.OutOfMemoryError: Metaspace
(jdk8之后元空间,存在本地内存,这些会存元空间里:静态变量、常量池、编译后的代码、虚拟机加载的信息)
GC回收算法和垃圾回收器的关系
GC算法有:引用计数、复制、标清、标整
针对每个区域,有不同的垃圾收集器,也有适合的GC算法;
分别有4种垃圾收集器:
串行垃圾回收器、并行垃圾回收器、并发垃圾回收器、G1垃圾收集器、JAVA11之后有了ZGC垃圾收集器
典型垃圾回收器有哪些
串行垃圾回收器:只有一个线程进行垃圾回收,会暂停所有的用户线程,先清理,再执行程序代码
并行垃圾回收器:跟串行垃圾回收器类似,只是并行,指多个线程来进行垃圾回收,也会暂停用户线程,只是比串行垃圾回收器执行快,它采用分代算法,再新生代使用复制算法、老年代使用标记整理算法回收内存
并发垃圾回收器:用户线程和垃圾收集线程同时执行,底层是交替执行,一边执行,一边垃圾回收清理,再新生代使用复制算法,老年代使用标记清除算法回收内存
G1垃圾收集器:JDK1.9以后的默认垃圾回收器,支持并发,局部使用复制算法、整体使用标记整理算法回收内存,使用可达性分析法来判断对象是否可以被回收
JDK11之后有了ZGC垃圾收集器:
ZGC它不采用分代算法,是采用分区域,不同区域大小,才有不同的回收算法;它有自己的内存布局,它使用了读屏障、 颜色指针等技术来实现的;
查看服务器默认的垃圾回收器
java -XX:+PrintCommandLineFlags -version
jdk8默认是并行垃圾回收器
# 通过这个设置s限制线程数量,默认是几个线程几个GC回收线程
-XX:ParallelGCThreads8
吞吐量是什么
吞吐量:用户线程执行时间 / 用户线程执行时间 + GC回收所用时间
也就是用户线程执行100m,垃圾回收1m,它的吞吐量就越高
GC之间如何选择垃圾回收器
单CPU的话,设置串行垃圾回收器
多CPU的话,设置并行垃圾回收器(默认)
多CPU的话,追求高吞吐量,设置并发垃圾回收器,命令:-XX:+UseConcMarkSweepGC
更好的选择,G1垃圾回收器,命令:-XX:+UseG1GC
常时配置: -XX:+UseG1GC -Xms32g -XX:MaxGCPauseMillis=100 (最大GC停顿时间,如果太小,会导致频繁GC)
G1与CMS的区别
G1不再是分代处理,G1没有内存碎片,G1比CMS更适合大堆内存的应用
类加载器和双亲委派机制
当一个类加载器收到类加载请求时,会先把这个请求交给【父类加载器】处理,若父类加载器找不到该类,再由自己去寻找。
该机制 1 避免类被重复加载,2 避免系统级别的类被篡改
(BootStrapClassLoader 加载路径为:JAVA_HOME/jre/lib)
(ApplicationClassLoader 加载路径为:classpath)
类加载过程
1 加载 :把字节码通过二进制的方式转化到方法区中的运行数据区
2 连接: 先验证:验证字节码文件的正确性;准备:正式为类变量在方法区中分配内存,(final类型的变量在编译时已经赋值了)
解析:将常量池中的符号引用(如类的全限定名)解析为直接引用(类在实际内存中的地址)
3 初始化 :执行类构造器(不是常规的构造方法),为静态变量赋初值并初始化静态代码块
JVM类初始化顺序
父类静态代码块 -> 子类静态代码块 -> 父类代码块和普通成员变量 -> 父类构造方法 -> 子类代码块和普成员变量 -> 子类构造方法
AtomicInteger和LongAdder区别
LongAdder是jdk8提出的,AtomicInteger是原子类,它是多个线程对一个热点数据进行原子操作,当线程过多的时,自旋会大大增加导致CPU上升;而LongAdder将一个热点数据value打散成多个Cell数组,每个Cell维护一个value,当存在多线程并发操作的时候,将线程映射到一个Cell上进行操作,每个Cell内的value值还是使用CAS更新,就这样将竞争分散到多个Cell中,效率比AtomicInteger高。
CAS与Synchronized的区别
CAS是乐观锁,理念是通过比较并交换,和自旋实现,适合读多写少的场景
Synchronized是悲观锁,共享资源每次只给一个线程使用,底层是通过JDK实现的,适合写多读少的场景
自旋锁一直获取不到锁,会一直自旋吗
它通过循环检测锁的状态来实现线程的等待,自旋锁并不会一直自旋下去。为了避免浪费CPU资源,通常会设置一个最大自旋次数或者超时时间,有对应的自旋API的方法