作者: 刘荣杰 | 来源:发表于2021-06-24 15:26 被阅读0次

    在看了MYSQL和JAVA高并发编程之后,对锁有了新的认识,但是有一些细节也不明白,现在尝试把自己的理解写出来,和自己对话,将自己不理解的点给具体定位。

    锁介绍

      我们一定听过很多锁的类别,什么独占锁与非独占锁,读锁与写锁,悲观锁与乐观锁,轻量锁与重量锁等之类的,有些锁是MYSQL中提到,有些锁是JAVA中提到,到底这些锁是什么,现在我们来好好整理学习一下,其实所谓锁更多的是一种思想,在并发场景下,多线程或者说多个事务同时请求同一个资源,那么我们应该怎么办,采取什么措施保证运行的正确性。

    面临问题

      任何技术的出现都不是无缘无故的,在引出锁之前,我们先讲讲没有锁所面临的问题。

    1.同时操作:
    我们都知道一个数在计算机中以二进制形式存储:
    原来一个数 int i=0; ---------->00000000 00000000 00000000 00000000;
    一个线程: i=1; ---------->00000000 00000000 00000000 00000001;
    另一个线程 i=65536; ---------->00000000 00000001 00000000 00000000;
    两个线程同时进行操作就会出现一些不可预计的情况,在我们上层看来特别奇怪。
    例如 最终 i=65537;--------->00000000 00000001 00000000 00000001;
    这里这是举一下例子,计算机底层存在操作的最小单元避免这种情况发生,计算机如何实现这种最小单元避免?
    2.同时运算:
    原来一个数 int i=0;
    一个线程 i++;
    另一个线程 i++;
    我们预想的结果是 i=2;
    结果常常结果是 i=1;
    因为两个线程同时获取的i值为0,通过运算得到结果1,回去赋值也是1;

    正是由于并发事务存在这些问题,我们期望保证资源访问的唯一性,即同一时间段只能有一个线程访问共有资源,于是出现了锁,所谓锁,顾名思义,我用的时候把该资源锁起来不让其他用,用完了再释放。

    锁分类

    锁的思想就是我用我锁住,用完释放,别人接着用,思想简单透彻。

    //伪代码
    int i=1;
    //线程1
    lock(i){
      i++;
    }
    //线程2
    lock(i){
    i++;
    }
    
    

    如上,1,2线程进行运算之前都会锁定i再运算,另一个线程查找时发现资源被锁定就会进入阻塞态,等待锁被释放,然后另一个线程重新进入运行态,获取获取资源然后锁定,进行运算。

    但是加锁解锁以及线程阻塞都是非常消耗资源的,所以再不同的业务场景中就有了不同的优化手段,达到同样的目的但是较少开销,于是有了不同锁的分类。
    下面的锁更多的是思想。

    自旋锁与互斥锁

    业务场景:我们预期线程持有锁的时间非常短,就像刚才锁住之后只执行了i++,这个时候另一个线程还需要进入阻塞态吗?显然不需要,只需要循环一下等待锁释放运行就可以了,这样可以避免阻塞资源消耗,这就是自旋锁。
    自旋锁:当加锁失败后,不会陷入阻塞而是会循环等待,有的会加上自旋次数上限,自适应自旋,避免陷入阻塞开销,但要注意循环CPU空转开销。
    互斥锁:当加锁失败后,陷入阻塞。

    乐观锁与悲观锁

    业务场景:我们预期线程冲突特别少,这样我们不用线程都加锁解锁,消耗太大,一般采用CAS(Compare And Swap操作)和版本号机制来减少加锁解锁。
    CAS:Compare and Swap我们取出数据直接运算,更新的时候拿原始数据跟现在数据进行比较,相等说明没有其他进程操作可以更新,不行的化说明其他线程操作了,我们需要重新取数据进行运算。
    版本号机制:每次更新时校验一下版本号,版本号没变化说明没有其他进程操作,我们可以更新于是更新并将版本号+1;

    乐观锁:预期线程冲突特别少情况下,不用加锁解锁资源浪费,就更新时校验一下中途资源是否被更新。
    悲观锁:预期冲突特别多,要用时加锁不让别人用。

    读写锁,独占锁与非独占锁

    业务场景:我们对数据的操作分为读操作和写操作,读操作是可以同时进行的,因为互不干扰,所以读操作不需要加锁。

    读写锁:读时不加锁,只有写的时候才需要加锁。
    独占锁:该资源只能被一个线程使用,独自占领。
    非独占锁:资源可以共享。

    Java中从偏向锁到轻量级锁到重量级锁


    这边文章将从偏向锁到轻量级锁再到重量级锁的全过程讲的特别清晰。
    就是synchronized底层锁的实现,JVM为了适应不同场景的使用会对锁进行一系列的优化,包括锁粗化,偏向锁,轻量级锁等。

    锁粗化:

    ConcurrentMap map=new ....;
    for(int i=0;i<10;i++){
    map.put(i,i);
    }
    

    很明显在这种情况下,每次put进行加锁解锁很不明智,JVM虚拟机会进行锁粗化,整体for循环加锁。

    从偏向锁到重量级锁:

    一开始我们预想没有冲突,只有一个线程使用该资源,我们加锁之后就不用释放,当线程再次请求时直接使用,这就是偏向锁。

    可是当有少量的冲突时偏向锁显然不行,所以会进化为轻量级锁,所谓轻量级锁即发生冲突较少,直接自旋等待解锁,这就是轻量级锁。

    当冲突较多或者锁持有时间较长,肯定不能一直自旋啊,所以自旋次数到上限后会升级为重量级锁,并且阻塞。详情参考上面链接。

    从偏向锁到重量级锁是一个过程,冲突越来越严重,锁逐渐升级,这样既避免了在冲突较少时直接重量级锁浪费资源,可避免了冲突较多时偏向锁和轻量级锁不能满足条件。

    相关文章

      网友评论

          本文标题:

          本文链接:https://www.haomeiwen.com/subject/zauwyltx.html