java线程状态转换关系
juc并发包
线程:是操作系统能够进行运算调度的最小单位
CPU多级缓存
由于cpu的计算速度与内存的i/o操作速度存在几个数量级的差距,为了弥补改差距,使用了多级高速缓存解决该问题,但同时又引出了新的问题
CPU多级缓存-缓存一致性问题,而缓存一致性的解决是通过各种协议完成如MSI MESI MOSI等
CPU多级缓存-乱序执行优化:在多线程中,如果两个线程有通信,该优化可能带来一些问题,比如线程a有一个标志位,在线程执行后改变;线程b会监听这个标志,然后做一些处理,由于乱序优化,可能导致标志提前被改变,这是线程b运行就会出问题,解决方式禁止指令重排
java内存模型
内存的八种操作
- lock(锁):
作用于主内存的变量,把一个变量标识为一条线程独占状态; - read(读取):
作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用; - load(载入):
作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中; - use(使用):
作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎; - assign(赋值):
作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量; - store(存储):
作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作 - write(写入):
作用于主内存的变量,它把 store操作从工作内存中ー个变量的值传送到主内存的变量中 - unlock(解锁):
作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
原子性
- atomicxxx
AtomicInteger #getAndIncrement该方法–>调用unsafe.getAndAddInt(this, valueOffset, 1);
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
只有当现在的值与从主内存获取的值相同时才加,否则一直去轮询获取底层的值,这里就出现了缺点,当多次不一致会持续循环
2. AtomicIntergerFieldUpdater
用于更新字段,使用注意事项
- 不能是private的,因为使用的反射获取
- 必须是volatile修饰的
- 不能是static的,因为Unsafe.objectFieldOffset不支持
- AtomicLong与LongAdder
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。 - AtomicStampReference
可用于解决CAS的ABA问题 - 原子性的对比
- synchronized:不可中断锁,适合竞争不激烈,可读性好
- Lock:可中断锁,多样化同步,竞争激烈时能维持常态
- Atomic: 竞争激烈时能维持常态,比Lock性能好;只能同步ー个值
synchronized
- 对synchronized的了解?
- synchronized可以保证方法或者代码在运行时,同一时刻只有一个线程可以访问,同时还可以保证共享变量的内存可见性。
- 不能被继承
- 可以禁止指令重排序
- 修饰代码块和修饰方法,作用于调用的对象;
- 修饰静态方法和类,作用于所有对象
- 不用关心锁释放
- 当线程访问同步代码块时,它首先要得到锁才能执行代码,退出或者抛异常要释放锁,实现原理?
使用monitorenter和monitorexit指令实现的,代码经过javac编译之后会在同步代码块的前后分别生成monitorenter和monitorexit指令。当执行monitorenter时,首先尝试获取对象的锁,如果对象没有锁定,或者当前线程已经获取到了锁,则把锁的计数器加一,而执行monitorexit时,计数器减一,当计数器为0时,表示释放了锁,其他线程可以访问了。如果获取锁失败,就阻塞等待,直到请求锁定的对象被持有者释放。
- jdk1.6之后对synchronized的优化
锁主要存在四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
1. 自旋锁
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一个负担很重的工作,对于一些持有锁时间很短暂的线程来说,与其做一次状态的切换,不如空循环几次,即可得到锁,从而减少不必要的开销。一般会设置自旋次数,超过后没有得到锁就被挂起。
适应性自旋锁
虚拟机根据上一次获取锁的自旋时间和持有者的状态来推断。如果上一次线程自旋成功了,那么下次自旋的次数会增加,因为虚拟机认为既然上次成功了,那么此次也可能成功。反之,如果对于某个锁,很少有自旋能成功的,那么以后等待这个锁的时候自旋的次数会减少甚至不自旋。
2. 偏向锁
偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,那持有偏向锁的线程将永远不需要同步。
3. 轻量级锁
对于绝大部分的锁,在整个同步周期内是不存在竞争的,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作(自旋锁)避免了使用互斥量的开销,当如果存在多个锁竞争时,会升级为重量级锁。具体实现方式是使用对象头的一些标志位实现的
4. 锁消除
对检测到不可能存在共享数据竞争的锁消除
5. 锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
public static void test() {
List<String> list = new ArrayList<>();
for (int i=0; i<10; i++) {
synchronized (Demo.class) {
list.add(i + "");
}
}
System.out.println(list);
}
- Java内存模型的happens-before规则(不符合则线程不安全)
- 书写在前面的代码先与后面的代码执行
- volatile的写先于读
- a 先于b,b先于c,则a一定先于c
- 同一个对象的unlock操作先于后面一个lock操作
- 对象的初始化先于finalize()方法
- 线程的start先于其他操作
- 线程的中断先行发生于检测中断
- 线程的所有操作先于终止检测等
volatile
1. 作用
1、保证可见性,不保证原子性。
2、禁止指令重排序。
2. 实现原理
在JVM底层volatile是采用“内存屏障”来实现的,当写一个volatile变量时,JMM会把该线程对应的值立即刷新到主内存中。当读一个volatile变量时,JMM会把线程的本地内存置为无效,直接从主内存中读取共享变量。之所以可以防止指令重排序是因为,volatile会生成指令addl
$0x0,(%esp),即将修改的值写回内存,这样该指令前面的操作就必须先发生与后面的操作,从而阻止了指令重排序。
3. 可见性
可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的
导致共享变量在线程间不可见的原因
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存和主内存之间及时更新
CAS
CAS(Compare And Swap),比较并交换,整个AQS同步组件,Atomic原子类操作等都是基于CAS实现的。
在CAS中有三个参数:内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时,才会将内存值V的值修改为B,否则什么也不干,是一种乐观锁。
CAS可以保证一次读-改-写操作是原子操作,由底层硬件实现
CAS的缺点:
- 循环时间太长:如果自旋CAS长时间不成功,则会给CPU带来非常大的开销,需要限制自旋次数
- 只能保证一个共享变量原子操作
- ABA问题
对象发布与对象逃逸
1. 对象发布
使一个对象能够被当前范围之外的代码所使用
@Getter
private String[] arr = {"a","b","c"};
public static void main(String[] args) {
UnsafePublish publish = new UnsafePublish();
System.out.println(Arrays.toString(publish.getArr()) );
publish.getArr()[0] = "d";
System.out.println(Arrays.toString(publish.getArr()));
}
2. 对象逃逸
一种错误的发布。当一个对象还没有枃造完成时,就使它被其他线程所见
public class Escape {
private Integer count = 0;
public Escape() {
new InnerClass();
}
private class InnerClass{
public InnerClass() {
System.out.println(Escape.this.count);
}
}
public static void main(String[] args) {
new Escape();
}
}
3. 安全发布对象
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到 volatile类型成者 Atomicreference对象中
- 将对象的引用保存到某个正确构造对象的 Finale类型中
- 将对象的引用保存到一个由锁保护的域中
java不可变对象
- 用final 关键字修饰
- Collections.unmodifiableXXX() // LIst set map 等
- google Guava: Immutablexxx
线程池
使用线程池的优势
- 可重用,性能好
- 有效控制最大并发线程数,提高资源利用率,同时可避免过多资源竞争,避免阻塞
- 提供定期执行,定时执行,单线程,并发数控制等
ThreadPoolExecutor
参数
- corepoolSize:核心线程数量
- maximumpoolsize:线程最大线程数
- workqueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响
- KeepAliveTime:线程存活时间,当没有执行任务超过多久后终止
- RejectHander:拒绝策略
- 当任务数量少于corePoolSize时,直接创建新线程处理任务,
- 当任务数大于CorePoolSize,但小于max时,先将任务放到workqueue中,当workqueue满时,再创建线程
- 当任务数大于max时,就会根据拒绝策略进行处理
拒绝策略
- 超过时抛出异常
- 丢弃当前线程
- 丢弃最早的线程
- 用调用者所在线程执行
执行过程
知道任务提交给线程池之后的处理策略,这里总结一下几点:
- 如果当前线程池中的线程数目小于 corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
- 如果当前线程池中的线程数目 >= corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
- 如果当前线程池中的线程数目达到 maximumPoolSize,则会采取任务拒绝策略进行处理;
- 如果线程池中的线程数量大于 corePoolSize 时,如果某线程空闲时间超过 keepAliveTime,线程将被终止,直至线程池中的线程数目不大于 corePoolSize ;
- 如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过 keepAliveTime ,线程也会被终止。
线程池的状态
runState 表示当前线程池的状态,它是一个 volatile 变量用来保证线程之间的可见性;
- 当创建线程池后,初始时,线程池处于 RUNNING 状态;
- 如果调用了 shutdown() 方法,则线程池处于 SHUTDOWN 状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
- 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
- 当线程池处于 SHUTDOWN 或 STOP 状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
1 ScheduledExecutorService的schedule/scheduleAtFixedRate/scheduledWithFixedDelay的区别
- schedule会在给定时间,对任务执行一次
- scheduleAtFixedRate是周期性的,每隔多久执行一次,如果执行任务用时超过周期的时间,就会等上一次执行完才调用下一次执行任务,任务执行出错后,定时任务将不再继续执行
- scheduledWithFixedDelay是上一次执行完成后,隔多久再执行
2 线程池的数量计算方式
CPU个数 * CPU使用率 *(1 + 等待时间/计算时间)
3 Submit与execute的区别
Submit可能会吃掉代码运行时的错误,就像什么也没有发生.
ExecutorService executorService = Executors.newCachedThreadPool();
// submit不会抛任何错,也不会打印错误日志,就像什么也没发生
executorService.submit(()->{
int i = 10 / 0 ;
});
// 会打印错误
executorService.execute(()->{
int i = 10 / 0 ;
});