Java JUC 1 基础知识

下一节 Java JUC 2 -AQS

java线程状态转换关系

在这里插入图片描述

juc并发包

在这里插入图片描述
线程:是操作系统能够进行运算调度的最小单位

CPU多级缓存

在这里插入图片描述
由于cpu的计算速度与内存的i/o操作速度存在几个数量级的差距,为了弥补改差距,使用了多级高速缓存解决该问题,但同时又引出了新的问题
CPU多级缓存-缓存一致性问题,而缓存一致性的解决是通过各种协议完成如MSI MESI MOSI等
CPU多级缓存-乱序执行优化:在多线程中,如果两个线程有通信,该优化可能带来一些问题,比如线程a有一个标志位,在线程执行后改变;线程b会监听这个标志,然后做一些处理,由于乱序优化,可能导致标志提前被改变,这是线程b运行就会出问题,解决方式禁止指令重排

java内存模型

在这里插入图片描述

内存的八种操作

  1. lock(锁):
    作用于主内存的变量,把一个变量标识为一条线程独占状态;
  2. read(读取):
    作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  3. load(载入):
    作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
  4. use(使用):
    作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;
  5. assign(赋值):
    作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量;
  6. store(存储):
    作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  7. write(写入):
    作用于主内存的变量,它把 store操作从工作内存中ー个变量的值传送到主内存的变量中
  8. unlock(解锁):
    作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

原子性

  1. 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不支持
  1. AtomicLong与LongAdder
    LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
    缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。
  2. AtomicStampReference
    可用于解决CAS的ABA问题
  3. 原子性的对比
    • synchronized:不可中断锁,适合竞争不激烈,可读性好
    • Lock:可中断锁,多样化同步,竞争激烈时能维持常态
    • Atomic: 竞争激烈时能维持常态,比Lock性能好;只能同步ー个值

synchronized

  1. 对synchronized的了解?
  1. synchronized可以保证方法或者代码在运行时,同一时刻只有一个线程可以访问,同时还可以保证共享变量的内存可见性。
  2. 不能被继承
  3. 可以禁止指令重排序
  4. 修饰代码块和修饰方法,作用于调用的对象;
  5. 修饰静态方法和类,作用于所有对象
  6. 不用关心锁释放
  1. 当线程访问同步代码块时,它首先要得到锁才能执行代码,退出或者抛异常要释放锁,实现原理?

使用monitorentermonitorexit指令实现的,代码经过javac编译之后会在同步代码块的前后分别生成monitorenter和monitorexit指令。当执行monitorenter时,首先尝试获取对象的锁,如果对象没有锁定,或者当前线程已经获取到了锁,则把锁的计数器加一,而执行monitorexit时,计数器减一,当计数器为0时,表示释放了锁,其他线程可以访问了。如果获取锁失败,就阻塞等待,直到请求锁定的对象被持有者释放。

  1. 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);
    }
  1. Java内存模型的happens-before规则(不符合则线程不安全)
  1. 书写在前面的代码先与后面的代码执行
  2. volatile的写先于读
  3. a 先于b,b先于c,则a一定先于c
  4. 同一个对象的unlock操作先于后面一个lock操作
  5. 对象的初始化先于finalize()方法
  6. 线程的start先于其他操作
  7. 线程的中断先行发生于检测中断
  8. 线程的所有操作先于终止检测等

volatile

1. 作用

1、保证可见性,不保证原子性。
2、禁止指令重排序。

2. 实现原理

在JVM底层volatile是采用“内存屏障”来实现的,当写一个volatile变量时,JMM会把该线程对应的值立即刷新到主内存中。当读一个volatile变量时,JMM会把线程的本地内存置为无效,直接从主内存中读取共享变量。之所以可以防止指令重排序是因为,volatile会生成指令addl
$0x0,(%esp)
,即将修改的值写回内存,这样该指令前面的操作就必须先发生与后面的操作,从而阻止了指令重排序。

3. 可见性

可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的
导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存和主内存之间及时更新

CAS

  1. CAS(Compare And Swap),比较并交换,整个AQS同步组件,Atomic原子类操作等都是基于CAS实现的。

  2. 在CAS中有三个参数:内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时,才会将内存值V的值修改为B,否则什么也不干,是一种乐观锁。

  3. CAS可以保证一次读-改-写操作是原子操作,由底层硬件实现

  4. 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. 安全发布对象

  1. 在静态初始化函数中初始化一个对象引用
  2. 将对象的引用保存到 volatile类型成者 Atomicreference对象中
  3. 将对象的引用保存到某个正确构造对象的 Finale类型中
  4. 将对象的引用保存到一个由锁保护的域中

java不可变对象

  1. 用final 关键字修饰
  2. Collections.unmodifiableXXX() // LIst set map 等
  3. google Guava: Immutablexxx

线程池

使用线程池的优势

  • 可重用,性能好
  • 有效控制最大并发线程数,提高资源利用率,同时可避免过多资源竞争,避免阻塞
  • 提供定期执行,定时执行,单线程,并发数控制等

ThreadPoolExecutor

参数
  1. corepoolSize:核心线程数量
  2. maximumpoolsize:线程最大线程数
  3. workqueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响
  4. KeepAliveTime:线程存活时间,当没有执行任务超过多久后终止
  5. RejectHander:拒绝策略
    1. 当任务数量少于corePoolSize时,直接创建新线程处理任务,
    2. 当任务数大于CorePoolSize,但小于max时,先将任务放到workqueue中,当workqueue满时,再创建线程
    3. 当任务数大于max时,就会根据拒绝策略进行处理

拒绝策略

  1. 超过时抛出异常
  2. 丢弃当前线程
  3. 丢弃最早的线程
  4. 用调用者所在线程执行
    执行过程
    知道任务提交给线程池之后的处理策略,这里总结一下几点:
    • 如果当前线程池中的线程数目小于 corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
    • 如果当前线程池中的线程数目 >= corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
    • 如果当前线程池中的线程数目达到 maximumPoolSize,则会采取任务拒绝策略进行处理;
    • 如果线程池中的线程数量大于 corePoolSize 时,如果某线程空闲时间超过 keepAliveTime,线程将被终止,直至线程池中的线程数目不大于 corePoolSize ;
    • 如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过 keepAliveTime ,线程也会被终止。

线程池的状态

runState 表示当前线程池的状态,它是一个 volatile 变量用来保证线程之间的可见性;

  1. 当创建线程池后,初始时,线程池处于 RUNNING 状态;
  2. 如果调用了 shutdown() 方法,则线程池处于 SHUTDOWN 状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
  3. 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
  4. 当线程池处于 SHUTDOWN 或 STOP 状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
1 ScheduledExecutorService的schedule/scheduleAtFixedRate/scheduledWithFixedDelay的区别
  1. schedule会在给定时间,对任务执行一次
  2. scheduleAtFixedRate是周期性的,每隔多久执行一次,如果执行任务用时超过周期的时间,就会等上一次执行完才调用下一次执行任务,任务执行出错后,定时任务将不再继续执行
  3. 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 ;
});

FutureTask

Fork/Join框架

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值