ABA问题
CAS进行操作的关键是通过值没有发生变化来作为"没有其他线程穿插执行"判定依据
但这种问题不够严谨,近端情况下,有另一个线程穿插进来,把值从A->B->A
ABA问题如果真的出现了,其实大部分情况下也不会产生bug,虽然另一个线程穿插执行,由于值又改回去了,此时逻辑上也不一定会产生bug
只要让判定的数值按照一个方向增长即可,有增有减就有可能出现ABA
但是针对账户余额这样的概念,本身就应该要能增能减,可以引入一个额外的变量"版本号"
约定每次修改都要让版本号自增
synchronized原理
1.锁升级
无锁->偏向锁(此时不是真的加锁,只是做了一个标记)->自旋锁->重量级锁
偏向锁不是真的加锁,当锁竞争出现的时候,偏向锁就会升级成轻量级锁,
这样就可以能够保证效率,又能保证线程安全,核心思想和"懒汉模式"一样
能不加锁就不加锁,加锁意味着开销
2.锁消除
编译器会针对当前你写的加锁的代码做出判定,如果编译器觉得这个场景不需要加锁,
此时就会把你写的synchronized给优化掉
3.锁粗化
synchronized里头代码越多就认为锁的粒度越粗
将连续多次对同一个对象加锁、解锁的操作,合并为一次范围更大的加锁操作,从而减少锁的获取和释放次数,提升性能。
Reentrantlock也是一个可重入锁,使用上和synchronized类似,但是需要手动解锁
优势:1.加锁时有两种方式lock和trylock
2.提供了公平锁的实现
3.提供了更强大的等待通知,搭配了Condition类,实现等待通知的
Semaphore(信号量)
开发中如果遇到需要申请资源的场景,就可以使用信号量来实现了,
锁本质上就是资源为一的信号量
加锁操作,P操作,1->0
解锁操作,V操作,0->1
CountDownLatch
这个东西主要适用于多个线程来完成一系列任务的时候,用来衡量任务的进度是否完成
比如需要把一个大的任务,拆成多个小的任务,让这些任务并发的去执行
就可以使用使用countDownLatch来判定这些任务是否全都完成
主要有两个方法
- await调用的时候就会阻塞,就会等待其他线程完成任务,所有线程都完成了任务以后,此时这个await才会继续往下走
- countDown告诉countDownLatch,我当前这个子任务已经完成了
copyOnWriteArrayList写实拷贝
比如两个线程使用同一份ArrayList,如果两个线程读就直接读
如果某个线程需要修改,就会把ArratList复制出一份副本,修改线程的话就修改这个副本,
与此同时另一个线程仍然可以读取数据,一旦修改完成就会使用修改好的这份数据,替代掉原来的数据
缺点:这个ArrayList不能太大(拷贝成本高),更适合多个线程读,一个线程去修改
应用场景:服务器的配置更新
ConcunrrentHashMap
ConcunrrentHashMap最核心的改进就是把一个全局的大锁改进成了每个链表独立的小锁,这样做,大幅度降低了锁冲突的概率。就是把每个链表的头节点作为锁对象,而java 8以前是基于分段式锁实现的
充分利用了CAS特性,把一些不必要加锁的环节给省略加锁了,比如需要用变量记录hash表中的元素个数,此时就可以使用原子操作(CAS)修改元素个数
针对读操作没有加锁,读和读,读和写之间都不会有锁竞争
针对扩容操作做出了单独的优化,原本HashMap扩容时需要把所有的元素都拷贝一遍,而ConcunrrentHashMap则是把扩容中一次搬运操作分成多次,每次只搬运一部分数据,避免这单次操作过于卡顿