锁的粒度

1.synchronized关键字的底层原理是什么

  • synchronized可以对一个对象、类加锁,原子性、可见性、有序性、指令重排

  • 每个对象都有一个关联的monitor,比如一个对象实例就有一个monitor,一个类的class对象也有一个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁。

  • monitor里面有一个计数器,0时说明没人获得锁可以成功加锁,大于0就无法加锁了,会进入阻塞状态。

  • 当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

  • synchronized一般是对对象加锁,对类加锁也就是对类对象加锁。synchronized底层的原理是跟jvm指令和monitor有关系的。每个对象内部都有一个monitor,monitor里面有一个计数器,从0开始的。如果一个线程想对一个对象加锁,就得先获取这个对象关联的monitor的lock锁。如果这个线程想获取monitor的锁,就先判断monitor的计数器是不是为0,如果为0,说明没人获取锁,这个线程就可以获取锁,然后对计数器加1;如果不为0,判断获取锁的线程是不是自己,不是就阻塞,说明已经有其他线程已经获取了锁,这个线程就必须阻塞等待。是就重入,计数器加一。 如果使用了synchronized关键字,在底层编译后的jvm指令中,会有monitorenter和monitorexit两个指令。线程进入synchronized代码片段,执行monitorenter指令就是对monitor计数器加1,;线程出synchronized代码片段,执行monitorexit指令就是对monitor计数器减1

synchronized(myObject) {  -> 类的class对象来走的

// 一大堆的代码

synchronized(myObject) {

// 一大堆的代码

}

}

如果一个线程第一次synchronized获取到了myObject对象的monitor的锁,计数器加1,然后第二次synchronized那里,会再次获取myObject对象的monitor的锁,这个就是重入加锁了,然后计数器会再次加1,变成2.

这个时候,其他的线程在第一次synch那里,会发现说myObject对象的monitor锁的计数器是大于0的,意味着被别人加锁了,然后此时线程就会进入block阻塞状态,什么都干不了,就是等着获取锁。

接着如果出了synchronized修饰的代码片段的范围,就会有一个monitorexit的指令,在底层。此时获取锁的线程就会对那个对象的monitor的计数器减1,如果有多次重入加锁就会对应多次减1,直到最后,计数器是0。

然后后面block住阻塞的线程,会再次尝试获取锁,但是只有一个线程可以获取到锁

 

2.CAS底层实现原理

 

CAS在底层的硬件级别给你保证一定是原子的,同一时间只有一个线程可以执行CAS,先比较再设置,其他的线程的CAS同时间去执行会失败

  • 总结:synchronized 加锁影响性能,可以采用无锁化CAS方式,原理是compare 和 swap,会出现的问题 ABA、无限循环、多变量原子问题。

  • 补充总结:CAS虽然高效的解决了原子操作问题,但仍然存在三大问题:

    • ABA问题:如果变量V初次读取的时候值是A,后来变成了B,然后又变成了A,你本来期望的值是第一个A才会设置新值,第二个A跟期望不符合,但却也能设置新值。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本号来保证CAS的正确性,比较两个值的引用是否一致,如果一致,才会设置新值。 打一个比方,如果有一家蛋糕店,为了挽留客户,绝对为贵宾卡里余额小于20元的客户一次性赠送20元,刺激消费者充值和消费。但条件是,每一位客户只能被赠送一次。此时,如果很不幸的,用户正好正在进行消费,就在赠予金额到账的同时,他进行了一次消费,使得总金额又小于20元,并且正好累计消费了20元。使得消费、赠予后的金额等于消费前、赠予前的金额。这时,后台的赠予进程就会误以为这个账户还没有赠予,所以,存在被多次赠予的可能,但使用AtomicStampedReference就可以很好的解决这个问题。

    • 无限循环问题(自旋):看源码可知,Atomic类设置值的时候会进入一个无限循环,只要不成功,就会不停的循环再次尝试。在高并发时,如果大量线程频繁修改同一个值,可能会导致大量线程执行compareAndSet()方法时需要循环N次才能设置成功,即大量线程执行一个重复的空循环(自旋),造成大量开销。解决无线循环问题可以使用java8中的LongAdder,分段CAS和自动分段迁移

    • 多变量原子问题:只能保证一个共享变量的原子操作。一般的Atomic类,只能保证一个变量的原子性,但如果是多个变量呢?可以用AtomicReference,这个是封装自定义对象的,多个变量可以放一个自定义对象里,然后他会检查这个对象的引用是不是同一个。如果多个线程同时对一个对象变量的引用进行赋值,用AtomicReference的CAS操作可以解决并发冲突问题。 但是如果遇到ABA问题,AtomicReference就无能为力了,需要使用AtomicStampedReference来解决。

3.ConCurrentHashMap实现线程安全的底层原理是什么?

  • jdk1.7以及以前版本,分成多个数组,分段加锁

  • jdk1.8锁粒度优化,使用CAS;粒度到数组的一个元素进行操作使用CAS,后续链表+红黑树部分只用synchronized.

  • ------

  • jdk1.8之前ConcurrentHashMap实现线程安全使用的是分段锁技术,即将一个大数组分成几个小数组,当并发put时,处于同一个小数组的put操作会串行;不同小数组间的put操作不受影响 jdk1.8ConcurrentHashMap优化了锁的细粒度,并发操作时,对数组中每一个位置元素进行CAS。当并发put时,对同一位置进行put操作,如果put失败,说明在这之前有线程对这个位置进行了put成功操作,则对这个位置上的链表或者红黑树使用synchronized加锁;对不同位置put操作是不受影响的

  • ConcurrentHashMap进行put操作的时候,元素为null,则进行cas,不为null,则进行synchronized同步,不为null的时候,为什么也不进行cas呢?

    • 当数组得位置存放得是链表或者红黑树得引用得时候,此时已经发生了一次哈希冲突了,讲道理,哈希冲突得概率有,但不是很高,链表里面得数据进行添加得时候,运用cas操作是比较麻烦得,因为在多线程情况下往链表里面插数据是会报错得,这个时候只能采用sync锁来进行了.cas是比较后set,那对链表来讲,这个操作是实现不了的,链表在内存中不连续,节点的增加记录下上一个的位置就行,cas操作跟谁比较,怎么比较,hashmap的entry是个单向链表,cas操作中链表的指针如果不为null,那就得到另一个地址在进行比较,在这个过程中,链表长度突破8,对于后续的转化红黑树的操作影响也是非常巨大的.总的来讲,就是这个过程中进行cas的消耗,以及对编码的复杂度,都没有sync来的方便快捷,哈希冲突也是个小概率事件,对数组的不同位置加锁或者cas操作,已经完全够用了

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值