悲观锁,乐观锁,自旋锁,偏向锁,轻量级锁,CAS,版本号机制总结

线程同步的各种方式(悲观锁,乐观锁,自旋锁,偏向锁,轻量级锁,CAS,版本号机制总结)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhangjingao/article/details/86516038

前言

  最近研究了好久的java的多线程和线程同步及锁的实现方式,自我感觉小有所得,整理出来以后自己回顾,其中包括悲观锁,乐观锁以及两者的实现方式,包括自旋锁,自适应自旋锁,偏向锁,轻量级锁,重量锁,版本号机制,CAS操作。
  

目录

  • 悲观锁
  • 乐观锁
  • 区别和联系
  • 悲观锁的实现方式
    • 自旋锁
    • 自适应自旋锁
    • 偏向锁
    • 轻量级锁
    • 重量级锁
  • 乐观锁的实现方式
    • 版本号机制
    • CAS机制
        
        

悲观锁

  顾名思义,其实就是在获取数据的时候假设有线程也在惦记那数据,但是要实现线程同步啊,所以就先独占资源,让其他线程阻塞,先加锁,然后独占资源,再处理数据,直到释放锁。这样就实现了线程同步,它靠的是加锁。
  应用: java的synchronized锁,ReentranLock;数据库的行锁,表锁,都是悲观锁思想的体现。

乐观锁

  看了悲观锁,那就很明显就能明白乐观锁了,同样的道理,乐观锁的思想就是它觉得没人在用这个数据,很乐观嘛,认为没线程在占用资源,所以不会加锁。那怎么实现数据同步呢?就是在当它更新数据的时候会检测下在此期间有没有其他线程使用了该数据,可以使用版本号机制和CAS算法(compare and swap)实现。
  应用:java的java.util.concurrent.atomic包下的原子类(使用CAS实现),数据库的write_condition机制。

联系和区别

  悲观锁和乐观锁并不是一种锁实现方式,而是一种锁的思想
  都实现了线程同步。
  悲观锁因为独占资源,所以比较在读多写少的情况下,这样比较影响性能,因为数据都不变嘛,这样的情况下还加了把锁,但是在写多的情况下,这个机制就非常适合。它适合写多读少的场景。
  乐观锁因为不加锁,实现同步使用的是操作系统的CAS操作,CAS操作又是一个耗费资源的操作,所以在乐观锁碰到写多的时候就比较糟糕了。它适合读多写少的场景。

  两者并不好说优劣,因为存在即合理,两者适用场景不同。我个人建议使用synchronized,在jdk1.6之后,引入了轻量级锁,偏向锁,在线程冲突小的时候,可以获得和CAS差不多的性能,在线程冲突较多的时候,性能比CAS就好得多。
  
  

悲观锁的实现方式(这里介绍synchronized机制)

自旋锁

  自旋锁,就是当线程在竞争锁时,发现,哎,锁被占用了,那我能被阻塞吗?如果被阻塞之后还会再次唤醒,这个过程中,操作系统要在用户态和内核态之中进行切换,这个操作是很耗费cpu性能的,但是有时候很快锁就被owner线程给释放了,这样就没必要切换状态。这个时候呢,自旋锁就是在竞争不到锁的时候,先不阻塞,进入自旋锁,这个锁在我理解中不是一种锁,而是一种竞争锁的状态,在这个过程中,他就是做一些无意义的操作(如:几次空循环),然后在这个过程中会再次竞争锁,如果还没有竞争到,则会阻塞。自旋锁在锁持有时间长,锁竞争不激烈的情况下更能突出性能。
  优点:
  - 自旋锁减少了线程在阻塞和唤醒之间切换的频率,那么就是降低了操作系统在用户态和内核态的切换,降低了cpu的损耗,提高了线程在竞争期间的性能。
  缺点:
  - 在单核处理器中,并不存在所谓的并行,所以说当owner线程在占据cpu时,由于时间片轮转运行到等待线程时,等待线程在那自旋没有用的,因为owner线程此时没有办法释放锁,那么此时自旋就会很多余,当时现在的电脑基本都是4核,6核还有8核的,所以这个问题在以前存在,现在基本还好。
  - 自旋锁的时间无法判断,如果线程持有锁的时间短,小于或者等于自旋的时间,那么就很舒服,自旋过了刚好获得了锁,但是很多时候这个锁持有时间长短就要看代码设计了,所以无法预测,这就很难受。
  这些问题在1.6引入了自适应自旋锁之后得到了优化。
  

自适应自旋锁

  java在jdk1.6之后引入了自适应自旋锁,主要是为了解决自旋锁自旋时间不合理问题的。对于之前的自旋锁而言,自旋时间不好控制,而且分配也不合理,很可能owner线程很快就释放锁了,但是就因为自旋时间不合理错过了而导致cpu在用户态和内核态切换。
  自适应自旋锁是根据上一个线程竞争这个锁所需要的时间来决定当前线程自旋的时间。对于操作系统来说,上一个线程竞争锁所需要的时间极有可能是这次所需的时间,所以自旋一般控制在10-100之内。如果上一个线程很快就获得了锁,那么就认为很快就能获得锁,所以就允许自旋时间长一点,比如100个循环;如果上一个线程很久才获得锁,那么就认为很难获得锁,就让自旋时间短一点或者直接阻塞,以免多余浪费cpu。自旋后仍然失败,那么就阻塞当前线程。
  优点:
  - 优点很明显,优化了自旋时间不合理的问题,动态分配自旋时间。
  缺点
  - 依旧没有完全解决时间不合理问题
  

偏向锁

  有些时候,在整个同步周期内是没有竞争的,在这时,又只有一个线程在运行,这个时候没有其他线程在和它争抢cpu,那么这个时候进行的加锁,释放锁,重入锁等等操作都是多余且降低性能的。所以这个时候就进入了偏向锁的状态。
  在这个过程中,线程可以忽略这些锁,不会进行锁的操作,就是好像在偏向这个线程一样,只有初始化的时候使用一次锁的操作,也就是它整个过程只有进入偏向锁这个状态使用CAS切换了下状态,其他时候任何的锁和CAS都不会做,偏向锁就是力取在无竞争的情况下将同步都去掉。如果此时再有其他线程竞争锁,那么偏向锁会膨胀为轻量级锁
  什么叫在整个同步周期内是没有竞争的? 看下面的代码


public class ThreadTemp {

    public static void main(String[] args) {


        NoCompete compete = new NoCompete(10,11);
        Thread thread1 = new Thread(compete,"线程1");
        Thread thread2 = new Thread(compete,"线程2");
        thread1.start();
        thread2.start();
    }

}

class NoCompete implements Runnable {

    private int a, b;

    NoCompete(int a, int b) {
        this.a = a;
        this.b = b;
    }

//    同步方法,无实际竞争
    private synchronized void count1() {
//        ....do something
        System.out.println("线程名称:"+Thread.currentThread().getName()+": "+a+"+"+b+"= "+(a+b));
    }

//    同步方法,有实际竞争
    private synchronized void count2() {
        a++;
        b++;
        System.out.println("线程名称:"+Thread.currentThread().getName()+": "+a+"+"+b+"= "+(a+b));
    }

    @Override
    public void run() {
        try {
            //睡1s,模仿做了一些业务逻辑,拉长业务线,容易看到抢占cpu
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count1();
    }
}


  因为用到的是同一个对象,两个线程都会竞争这一个对象锁。但是count1和count2就不一样了。
  在count1中从未动过数据,这就是没有实际竞争,实际上没有必要保证线程同步,完全可以放心的在两个线程间切换cpu,分析证明很多同步代码块,很多一部分并没有实际的竞争,并不需要线程同步。
  在count2中两个线程对a,b进行了修改,这个时候不同的执行情况就会得到不同的执行效果,这个时候就是有实际竞争,就要保证线程同步,如果不保证线程同步,就会出现读脏数据等异常情况,得不到实际的理想执行效果。

轻量级锁

  轻量级锁是相对于重量级锁而言的,当没有锁竞争,但是有多个线程在使用锁的时候,这个时候就是用CAS获得轻量级锁,注意!使用的是CAS操作,这是在没有锁竞争的情况下。这样做的好处就是避免了使用传统的重量级锁互斥量mutex的操作
  在轻量级锁过程中,不会阻塞后来线程,CPU可以在多个线程中交替运行。
  如果轻量级锁遭遇了 锁竞争,哎呦,那么不仅需要CAS操作还需要互斥量,性能比之重量级锁还差,所以这时候它会膨胀为重量级锁。

  

重量级锁

  就是咱们作为菜鸟的时候理解的那种锁,可重可重的,哈哈。就是直接阻塞掉后来线程,owner没有释放锁,其他线程就只能等待owner线程释放锁。内部实现靠的是操作系统的互斥量mutex来实现的

膨胀过程

  锁的膨胀过程是不可逆的,只能越来越高级,这样做的目的是加快获得和释放锁的速度。
  偏向锁->轻量级锁->重量级锁,自旋锁(自适应自旋锁)是用来降低阻塞和调用之间的切换造成的操作系统从用户态到内核态而执行的一段过程。

锁类别特点
偏向锁无实际锁竞争,且只有一个线程使用锁。
轻量级锁无实际锁竞争,多个线程交替得到锁,允许短时间的锁竞争。
重量级锁有实际竞争,且锁竞争时间长


乐观锁的实现方式

  写完悲观锁的实现方式,看看乐观锁的实现方式。乐观锁有两种方式,一种版本号机制,一种是CAS操作。

版本号机制

  所谓版本号机制就是给所有未修改时期的数据一个版本号,当且仅当自己所拿的数据的版本号大于等于数据当前的版本号,那么允许你进行操作。
  举栗子:(游戏场景)线程A(攻击100血量)和线程B(攻击100血量)对数据M(血量200)进行操作。
  数据M当前版本号是1,那么A来了后拿到M(200血量)是1,B来了拿到M(200血量)也是1,然后A修改的时候检测版本号相等可以修改,M血量减到了100,版本号变为了2;这时候B来了也要修改,发现:哎,不对,版本号比M的小,被人动过了,就重新更新该数据,再次检测版本号,直到修改成功。

CAS操作

  全称是compare and swap,他的伪代码是这样的

void compareAndSwap (V v , object a, object b) {
	If (v == a) {
		v = b;
	}
}

  它有三个参数,一个数据地址V,一个原始值A,一个新值B,拿地址V上的值和A比较,如果相等,就将新值B赋值给V上的数据。
  这个操作并没有引入锁,但是实现了线程同步,所以也叫非阻塞同步(Non-blocking Synchronization))。

  但是他有几个问题

  1. CAS的ABA问题:
      当A自增了又自减了回到了原来的值,这个时候CAS操作并不能检测出来,但是在A被修改这段时间或许也做了其他操作,比如修改了其他数据的值等影响同步的操作。
    在jdk1.5之后,java提供了AtomicStampedReference类,其中的compareAndSwap方法既可以保证引用是否等于预期引用,还能保证当前标志是否等于预期标志,如果全部相等才会将内存的值赋值为新的值。
  2. 自旋耗费cpu
      在CAS执行时,如果数据没有更新成功,就会陷入自旋,如果长期不成功,就会一直自旋直到成功,这给CPU增加了很大的运行开销。
  3. 只能保证一个变量的原子操作
      因为它就一个原始数据参数,所以只能比较一个值。从jdk1.5之后,可以通过封装AtomicReference类对象来保证引用多个数据的原子一致性。
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值