Synchronized 和CAS优化

11 篇文章 0 订阅

我直接开始爆肝!!!


一、Synchronized 锁策略

1.乐观锁:认为接下来的任务大概率不会有锁竞争  (正常情况下乐观锁的效率更高)

悲观锁:认为接下来的任务锁竞争的概率很大~ 

2.轻量级锁:加锁和解锁的速度比较快,更高效   //乐观锁很可能是个轻量级锁

重量级锁  :加锁和解锁的速度比较慢  //悲观锁很可能是个重量级锁

3.自旋锁(轻量级锁的实现):如果加锁失败,就一直围绕这个锁,一直访问锁是否被占用,一旦释放就可以第一时间获取到这个锁

挂起等待锁(重量级锁的实现):如果加锁失败,就不会再看这个锁,去给其他的加锁。

4.互斥锁:synchronized就是互斥锁 ,加锁就是单纯的加锁(进代码块),出代码块就自动解锁

读写锁:锁分为读锁和写锁,读锁不会有线程安全问题,所以就不会有锁竞争,速度很快,除了写锁之外,写锁和写锁之间,写锁和读锁之间都有线程安全问题  ,读写锁一般使用于一写多读的情况~

5.可重入锁:如果一个线程对锁加锁两次,如果不死锁就是可重入锁

不可重入锁:如果一个线程对锁加锁两次,如果死锁就是不可重入锁

6.公平锁:线程是遵循先来后到原则的,解锁之后就,按照线程来的顺序加锁

不公平锁:线程不遵循先来后到原则,解锁之后,多个线程随即调度,互相竞争锁

二、synchronized特点

1.既是悲观锁又是乐观锁

2.既是轻量级锁又是重量级锁

3.轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待实现的

4.synchronized是非公平锁

5.synchronized是可重入锁

6.synchronized是互斥锁不是读写锁

三、synchronized 优化

1.synchronized的关键策略:锁升级

这里需要注意的就是偏向锁:偏向锁并不是真的加锁,而是在锁的头部进行一个 标记(属于那个线程),如果没有锁竞争就不会真正的加锁,只有出现锁竞争,才会取消偏向锁,真正加锁(轻量级锁/自旋锁),从而降低程序的开销
 

 2.锁消除

锁消除就是编译器来判断当前的锁是否可以消除,一个程序代码中,使用了synchronized,但是可能并没有在多线程环境下,此时编译器就会做出优化,将synchronized消除,从而省去加锁解锁的开销

3.锁粗化

一段代码中如果频繁出现加锁解锁的情况,但是可能并没有其他线程来抢占这个锁,这种情况编译器就会自动优化,避免频繁的加锁解锁浪费资源。把加锁和解锁整成一次,虽然并发的程度降低了,但是你频繁的加速和解锁的开销是要大于这个的

 四、CAS是什么?

CAS全称是Compare and swap 就是比较和交换,一个CAS涉及到以下操作~

比较寄存器A和内存M 的 数值,如果相同,就讲寄存器B的值交换给内存M(其实就是赋值,寄存器中的值不用在意)

CAS的伪代码:   伪代码是不能正常运行的,只是方便理解~

expectValue就是寄存器A,swapValue就是寄存器,比较内存和寄存器A的数值是否相同,如果相同就把寄存器B的值赋值给内存

boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
  &address = swapValue;
       return true;
  }
   return false;
}

这里的伪代码,并非原子的,如果是多线程运行,可能就会出现BUG

但是事实上我们的CAS操作,只是一条CPU指令(原子的)(靠CPU的支持),这一条指令就可以完成上面这段代码的功能!!

我们就可以使用CAS来让我们不加锁,也能保证线程安全了~~

五、CAS实现的操作

1.实现原子类

原子类是可以保证线程执行的时候++,-- 的时候线程安全的类

先来看一下原子类怎么用吧~~

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger num=new AtomicInteger(0);   //自带的原子类(原子类的操作都是线程安全的)

        Thread t1=new Thread(()->{
            for (int i = 0; i <50000; i++) {
                num.getAndIncrement();  //原子类内置的++方法,都是安全的  想当于num++
                num.incrementAndGet();  //相当于++num
//                num.decrementAndGet();   //相当于--num
//                num.getAndDecrement();  //相当num--
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000; i++) {
                num.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(num.get());  //get方法获取数值
    }
}

这样我们使用原子类,就可以不用加锁也可以实现线程安全的++ -- 等方法的操作了

 那么CAS是如何实现原子类的呢??

伪代码:

class AtomicInteger {
   private int value;  //初始化的数值
   public int getAndIncrement() {
       int oldValue = value;    //oldValue 可以视为是个寄存器   此处用变量表示
       while ( CAS(value, oldValue, oldValue+1) != true) {   //判断value和oldValue 是否相等
           oldValue = value;                            //如果相等就交换oldValue+1(相当于++了)
      }                                            //和oldValue的值 然后CAS返回true结束循环~
       return oldValue;                        //如果不相同,则CAS啥都不干返回false 进入循环
  }                                        //重新设置oldValue的值
}

上述代码就可以用CAS实现原子类~,原子类并不涉及加解锁操作,所以可以更高效的完成多线程的自增自减操作~

上述代码还有需要注意的地方:为什么我上面已经int oldValue = value,我下面还要CAS对比value和oldValue呢??这里不是一定是一样的嘛???

事实上 这里的value和oldValue还真可能不一样~  想象一个场景

如果t1线程,运行到赋值完了,where之前,t2线程来了,t2线程执行,执行完了,此时的value就已经+1了,回到t1线程,此时的value就和之前的oldValue不同了~

所以此时我们需要判断两次,如果真因为多线程导致value不一样了,那就判断false 进入循环体,再次赋值就OK了

2.实现自旋锁

伪代码:

public class SpinLock {
   private Thread owner = null;  //用来记录当前的锁被那个线程持有
   public void lock(){
       // 通过 CAS 看当前锁是否被某个线程持有.
       // 如果这个锁已经被别的线程持有, 那么就自旋等待.
       // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
       while(!CAS(this.owner, null, Thread.currentThread())){  //如果是空的就立即加锁 然后结束
      }                                                     //如果锁被别持有了就一直等待,判断
  }                                                      //一解锁就立即加锁~(一释放就能立即获取)                                                //如果被占用,就会一直忙等
   public void unlock (){
       this.owner = null;
  }
}

六、CAS的ABA问题

有一种情况~,如果我的Value数值本来是100,然后我线程A给他减了50,线程B又给他加了50,数值前后是一样的,但是这到底算变没变过呢???

虽然这种情况大多数不会出现问题,但是难免还是会有特殊情况:滑稽老铁,去银行存钱,账户里有100,老铁要取出50块钱,此时两个线程 t1 t2,他俩都能执行这个任务,我们期望的是线程1执行完,账户扣了50 剩余50,另一个线程在t1扣钱的时候再等待,轮到他是时候,发现存款已经变成50了,就不再扣钱了~  

这是我们期望的,但是如果t1线程在扣钱的时候,扣完了变成了50,就在t2线程还没执行的时候,滑稽哥的朋友给他转了50扣钱,此时存款又变成100了,线程t2一看,前面那老哥没干活啊,还得看我,又给你扣了五十块钱,虽然结果都是50扣钱,但是这样是扣了你两次,这就出现BUG了~~

如何解决ABA问题

要解决ABA问题其实很简单,我们只需要定义一个版本号,来确定当前数值便没变过,如果发生修改操作,版本号就+1(或者-1),后面线程要执行的时候对比一下版本号(如果之前的版本号小于现在的,那么就不能执行)就可以了。

带入到刚才滑稽老哥的场景,初始版本号为1,第一次线程t1和t2都获取,任务要将100扣款为50,当前版本号为1,线程1执行成功,存款余额为50,版本号变为2,线程1执行完,在线程2还没执行的时候,滑稽朋友存款50,版本号变为3,此时到t2线程,来判断版本号和开始获取的1,如果版本号小于当前版本号,就操作失败~ 

总结

以上介绍本文的全部内容了,如果有任何问题欢迎私信改正或交流哦~欢迎大佬们.感谢您的支持

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值