美文网首页
java并发-独占锁与共享锁,公平锁与非公平锁,重入锁与非重入锁

java并发-独占锁与共享锁,公平锁与非公平锁,重入锁与非重入锁

作者: IT菜鸟学习 | 来源:发表于2018-11-25 17:05 被阅读0次

    java并发-乐观锁与悲观锁,独占锁与共享锁,公平锁与非公平锁,重入锁与非重入锁

    java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁

    从宏观上分类,锁的类型可分为乐观锁和悲观锁

    1. 乐观锁
      乐观锁是一种读多写少,遇到并发写的性能可能会变低,每次去拿数据的时候别人都认为不会修改,所以不会上锁,但是在更新的时候会判断一下在这期间别人是否更新了数据,才去在写之前先读取版本号,然后加锁(比较更上一次版本号,如果一样则更新),如果失败则重复读-比较-写的操作。
      java乐观锁基本都通过CAS操作实现的,cas是一种原子操作,比较当前值与传入值是否一样,一样则更新,额否则失败。
    2. 悲观锁
      悲观锁就是悲观思想,任务写多,遇到并发写可能性能比较高。每次去拿数据都认为已经被别人修改,所有每次读取数据都会上锁,这样别人想读取数据就会block直到拿到锁。java中的悲观锁是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取,获取不到,才会转为悲观锁,如RetreenLock。

    基础知识二:java线程阻塞的代价

    java的线程是映射到操作系统上原生线程之上的,如果阻塞或者唤醒一个线程需要操作系统的介入,需要在用户态与核心态之间切换,这种切换回消耗大量的系统资源。因为用户态与核心态都有各自专用的内存空间,专用的寄存器等,用户态切换到核心态需要传递许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态结束后切换到用户态继续工作。

    1. 如果线程转态切换是一个高频操作时,这将会消耗很多cpu的处理时间。
    2. 如果对于需要同步简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略是非常糟糕的。
      synchronized会导致争用不到锁的进入阻塞状态,所有说是java中一个重量级的同步操作,别成为重量级锁,为了缓解上述性能问题,jvm从1.5开始,引入了轻量级锁和偏量锁,默认启用自旋锁,他们都属于乐观锁。
      明确理解java中线程切换的代价,是理解java中各种锁优缺点的基础之一。

    基础知识三:markword

    在介绍java之前,先介绍一个markword,markword是java对象数据结构的一部分,这里只做markword的详细介绍,因为对象markword对java的各种类型的锁密切相关;
    markword数据的长度在32位和64位的虚拟机(未开启压缩指正)分别为32bit和64bit,它的最后2位是锁的状态标志位,用来标记当前对象所处的状态,觉得了markword存储的内容,如下表所示:

    状态 标志位 存储内容
    未锁定 01 对象哈希码、对象分代年龄
    轻量级锁定 00 指向锁记录的指针
    膨胀(重量级锁定) 10 执行重量级锁定的指针
    GC标记 11 空(不需要记录信息)
    可偏向 01 偏向线程ID、偏向时间戳、对象分代年龄

    32位的虚拟机在不同状态下的markword结构如下图


    image.png

    了解markword结构,有助于后面了解java锁的加解锁过程。

    小结

    前面 提到了java的四种锁,重量级锁、自旋锁、轻量级锁、偏向锁
    不同的锁有不同的特点,每种锁只有在特定的场合下,才能发挥最大的作用,java中没有那种锁能在不同环境中都能有出色的效率,引入这么多锁的原因就是为了应付不同的场景。
    前面提到了重量级锁是悲观锁,自旋锁、轻量级锁、偏向锁都是乐观锁,我们都有了一些了解,但是具体的如何使用这几种锁呢,这个就是后面分析他们的特性了;

    java中的锁

    synchronized 和ReentrantLock是可重入锁

    ReentrantLock中可重入锁实现

    这里看非公平锁的锁获取方法:

    final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                //就是这里
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    

    在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。

    与可重入锁synchronized和Lock不同的就是自旋锁。
    public class SpinLock {
       private AtomicReference<Thread> owner =new AtomicReference<>();
       public void lock(){
           Thread current = Thread.currentThread();
           while(!owner.compareAndSet(null, current)){
           }
       }
       public void unlock (){
           Thread current = Thread.currentThread();
           owner.compareAndSet(current, null);
       }
    }
    

    对于自旋锁来说,没有 int c = getState(),private volatile int state维护

    1. 若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
    2. 若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了。实际上不应释放锁。(采用计数次进行统计)

    转自:https://blog.csdn.net/zqz_zqz/article/details/70233767
    https://www.zhihu.com/question/23284564
    https://blog.csdn.net/rickiyeat/article/details/78314451

    相关文章

      网友评论

          本文标题:java并发-独占锁与共享锁,公平锁与非公平锁,重入锁与非重入锁

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