CAS自旋锁与互斥锁优劣分析

        加锁的目的只有一个就是保证共享资源在任意时间内,只有一个线程可以访问,以此避免数据共享导致错乱的问题。自旋锁和互斥锁只是通过不同的方式对锁进行实现。锁没有对的只有合适的。

互斥锁

        最为常见的互斥锁就是synchronized,通过synchronized关键字以同步方法和同步代码块的方式,为方法和代码块上的对象加锁。使得同一时刻,在这个对象上的多个线程,只能由持有这个对象锁的单个线程进行代码的调用执行。

        互斥锁相较于自旋锁最大的不同在于,互斥锁加锁失败后,线程释放CPU,给其他线程。

 

        当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。

这个过程就存在两次线程的上下文切换开销:

  • 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;
  • 接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。 

        线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。 

自旋锁

        cas的全称为Compare-and-Swap。自旋锁与互斥锁不同,互斥锁在获取锁失败后会释放cpu内存资源,自旋锁不会自己释放资源。自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系,我们需要清楚的知道这一点。

        加锁失败时互斥锁用线程切换来回应,而自旋锁则是用等待回应。

         不过自旋锁可能会出现ABA问题:CAS算法通过比较变量值是否相同来修改变量值以保证原子性,但如果一个内存地址的值本身是A,线程1准备修改为C。在这期间,线程2将值修改为B,线程3将值修改为A,线程1获取内存地址的值还是A,故修改为C成功。但获取的A已不再是最开始那一个A。这就是经典的ABA问题,A已不再是A。

两个方法,1、增加版本号;2、增加时间戳。

  • 增加版本号:让值的修改从A-B-A-C变为1A-2B-3A-4C;这样在线程1 中就能判别出1A不是当前内存中的3A,从而不会更新变量为4C。
  • 增加时间戳:值被修改时,除了更新数据本身外,还必须更新时间戳。对象值以及时间戳都必须满足期望,写入才会成功。JDK提供了一个带有时间戳的CAS操作类AtomicStampedeReference。

        通过这种询问不切换线程的方法成功的减少了线程的上下文切换开销 ,不过不断的询问也就意味着会不断占据着cpu

优劣分析

         一般情况使用互斥锁。如果我们明确知道被锁住的代码的执行时间很短(这样的场景最普遍,就算不普遍也要改代码让这种场景普遍),那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败时,并不会主动产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,那这个忙等待的时间相对应也很短。

        不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。

猜你喜欢

转载自blog.csdn.net/m0_58366209/article/details/127960468