大厂学院 雷丰阳 JUC 学习笔记

基础篇

synchronized和lock的区别

1.从本质上:synchronized是Java内的一个关键字,lock是一个接口。
2.从代码的形式上:
    synchronized在发生异常时会主动释放锁,lock需要我们在finally语句中释放,不然会死锁;
    通过lock可以知道锁有没有获取成功,synchronied不行
3.从性能上:在1.6前没提出锁升级过程时,重量级锁 在被系统检测到后会 阻塞 尝试获取锁的线程  线程的阻塞和唤醒 都要操作系统来帮忙,这需要从用户态转到内核态,转换费时。

虚假唤醒

if(condition){
    this.wait; //t线程在此等待,再次被唤醒,就不会再判断,直接向下执行了。改为while即可
}

针对集合类线程不安全的 “写时复制”技术

就是 对写加锁 读不加锁,允许随便读。进程创建子进程 时 常用。

Callable对比Runnable,FutureTask,CompletableFuture

Callable它有返回值  能抛异常
FutureTask 同时实现了两者,是常用的创建的多线程的方式,核心方法 get(),isDone

CompleteableFuture是对FutureTask的优化:当我们想获取futureTask的结果时,get()会阻塞线程,轮询isDone也会等待,为此CompleteableFuture可以自动回调 我们传入的回调对象的回调方法。


最后我们注意CompletableFuture的命名规则:

xxx():表示该方法将继续在已有的线程中执行;
xxxAsync():表示将异步在线程池中执行。

为什么需要阻塞队列,核心方法

我们不用关心  什么时候 去阻塞  唤醒线程,实现了自动化

抛异常:add  remove
ture false,null: offer  poll
阻塞:put take
超时时间:offer(time,unit).poll(time,unit)

为什么出现了线程池,工作流程、拒绝策略、参数设置

1.为什么:
    充分利用资源  有任务就跑  任务少就关几个线程 自动管理
2.工作流程:
    执行一个任务 正在工作的线程数
    小于corePoolSize,直接创建线程执行;
    大于等于corePoolSize,放入队列中;
    队列都满了,创建非核心线程数;
    队列满了,也达到最大线程数,执行拒绝策略

3.拒绝策略:
    AbortPolicy:丢弃并 抛异常
    CallerRunsPolicy:不丢弃 退回给调用者
    DiscardOldestPolicy:抛弃排队最久的
    DiscardPolicy:直接丢弃 也不抛异常

4.参数设置:
    在流量平均的前提下,套公式即可coreSize = tps*time,每秒任务数*单线程需要处理一个任务需要的时间
    当流量经常是有峰值波动的,就可以考虑 动态化线程池,做监控 预警 消息通知 修改配置中心。


注:为什么要自定义?
    不自定义的话 用工具类的话  队列长度和最大线程数上限都是 Integer.MaxValue 导致请求积压或过多线程,导致OOM;
 

Java锁-保证线程安全篇

1.synchronized

1.1实现原理

在类加载时 获取monitor锁对象 通过monitorEnter 和 monitorExit 两个命令实现的,流程的话:锁对象里有 锁计数器和指向它的线程的指针。
       当执行moniterenter时,计数器为零,说明没有其他线程持有 该线程则就可以持有该锁对象 并将计数器加一 ;计数器不为零,就判断锁的持有线程是否时当前线程,若不是就等待,若是计数器加一
       当执行moniterexit时,锁对象的计数器减一。

1.2对象头和锁升级

对象头 分 类型指针和MarkWord
    MarkWord在 不同锁状态下 存储 的东西不一样,比如     
    无锁: hashcode 分代年龄 偏向锁标识 锁标识位;
    偏向锁: 线程id 偏向锁标识 锁标识位
    轻量锁:指针 锁标识位
    重量锁:指针 锁标识位

记忆规律:记住 无锁状态下 后面的锁状态存的东西依次递减 根据名字回忆即可 
    
锁升级:
    偏向锁(适用场景:一个线程):第一次获取 到markword 发现是无锁,通过原子的cas设置操作 将markword设置为 偏向锁状态下的markword(线程id设置为当前线程,偏向锁标识设置为1 锁标识位设置为01)该线程 下次获取锁对象 就不用cas设置了 比较线程id即可。
    轻量锁(适用场景:两个线程):获取锁之前 会读取markword 发现是偏向锁且 线程id不是自己,尝试cas设置去修改markword(修改指针 和 锁标识位)
    重量级锁(适用场景:2个线程以上):线程获取锁(设置markword) 就不是 自旋了,进入队列 阻塞了。

    
注:cas (compare and swap) 保证修改操作的原子性  
    1.通过地址读取到原a值
    2.在正式修改为b值前 比较 1或取的a值和实时值
    一样就修改,不一样就说明有其他线程在此期间修改了该值,当前线程的修改操作就不“原子”了,重复步骤1(重新读取)

2.lock

从源码看为什么lock是由cas和aqs构成的?
// 非公平锁
static final class NonfairSync extends Sync {
    ...
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
        }
  ...
}

1.先判断cas设置sate从0到1是否成功,成功就修改持有线程为当前线程
2.不成功 通过 继承来的AQS方法acquire获取锁

2.1CAS

原理:
    1.硬件层面:通过cpu的cmpxchg指令去实现的,若是多核cpu,则先会对总线加锁,只有加锁成功的cpu能执行cas操作
    2.代码层面:比如AutomicInteger 静态代码块里 通过unsafe提供的方法 获取到了value的内存地址偏移量valueoffset,然后就可以用该地址 去对value进行cas原子操作了

注:unsafe顾名思义“不安全”,它为java提供了直接操作内存资源的方法。

缺点:
    1.可能一直自旋  消耗cpu
    2.ABA问题:   
        首先 a b 都观察值是1,然后b 修改 为 2 又 改回1,最后a 比较操作成功。小结:a虽然修改成功 但并不是原子的。可能的问题:当ab 观察的是一个对象的一个属性,但b不止修改了一个属性,a前后观察不是同一个对象了。解决:加版本号。

2.2AQS

数据结构:主要是由int 类型的state 和 FIFO双向链表构成,需要等待的线程封装成node

加锁的过程:
    与minitorEnter 的过程类似 通过逐步比较锁对象的计数器和指针来决定 获取是否
入队等待。


常见问题:
1.简述入队addWaiter()?
    cas原子化的去更新tail为当前线程节点,当失败或读取的原tail为null就会 自旋的去尝试设置tail。

2.为什么需要一个虚拟头节点?


3.队列中 是如何有机会获取锁的?
    最好画图
     只有当你的前一个节点是head你才有机会获取锁资源保证了队列先进先出的特点,当你有资格获取锁却一直失败或没资格,你就会被park中断,当持有资源的线程解锁时,会唤醒head后的第一个不为null且非canceled状态的节点(队列的节点都是自旋获取锁资源的)。

4.如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题?
    不需要 可以执行cancel方法(双向链表删除节点的操作)。


参考:
    阳哥视频
            https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
    

Java内存模型和volatile篇

概念:java针对cpu操作内存和缓存的统一规范,主要体现在多线程的 原子性,有序性(指令重排),可见性。

场景(什么规范呢):cpu,缓存,主内存。多线程之间沟通缓存不直接可见,只能通过主内存沟通。

保证操作间的可见性和有序性之happen-before原则

概念:当一个操作happen-before于另一操作,那么该操作会先执行和保证结果的可见性于第二个。
    (两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。 如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
  )
 
******************例子*****************
 
x = 5    线程A执行
y = x    线程B执行
上述称之为:写后读    
 
 问题?

 
y是否等于5呢?
 
如果线程A的操作(x= 5)happens-before(先行发生)线程B的操作(y = x),那么可以确定线程B执行后y = 5 一定成立;
 
如果他们不存在happens-before原则,那么y = 5 不一定成立。
 
这就是happens-before原则的威力。-------------------》包含可见性和有序性的约束
 
***************************************

方法:如何确定操作间是否有happen-before?
    有8个细则,忽略不考,凭借经验即可

volatile

一、概念:volatile读,缓存失效,直接从主内存中读最新的;volatile写,立即将缓存中的刷回主内存。

二、与JMM的关系:
    保证可见性:volatile写。
    保证有序性:禁止cpu层指令重排,
            1.第一个是volatile读,后面        无论是什么类型读写 都不能重排到 此之前。
            2..第二个是volatile写,前面无论是什么类型读写 都不能重排到 此之后。        
            3.第一个是volatile写且第二个是volatile读,也不能重排。
    不能保证原子性:例如在多线程的时候,i++,volatile读会使缓存失效,导致刚在缓存+1的值丢失了。


三、底层实现: 
    内存屏障:    
        概念:粗分为读屏障--读之前插入屏障,缓存失效从主内存中读。
                写屏障--写操作后插入屏障,写完立刻刷回主内存。

     内存屏障起作用的例子:
        在volatile读后加 loadload(第一个读和第二个读不能交换顺序)和 loadstore 
        
       

volatile的使用

1.多线程任务的相互通知,需要修改玩flag后,立即对其他线程可见。
2.双重检查锁的场景:
    singleton = new SingleTon();
    实际上涉及三个指令,
    1.分配内存空间,
    2.new(),初始化对象
    3.将地址赋值给singleton。
    
    重排后 会将 2 3交换。就会造成线程A走完 1 3 ,线程B此时就可以通过 为空检查,但值是空对象。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值