JMM之Java中锁概念的分类总结

  在Java的并发编程中不可避免的涉及到锁。从不同维护可以将锁进行不同的分类,如下:

  1、乐观锁和悲观锁(根据读写操作的比例划分)

    乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
    java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败

    悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
    java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

  2、自旋锁

    自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗
    线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    

  3、可重入锁

    此处可重入锁指广义上的可重入锁,而不是单指 JAVA 下的 ReentrantLock。 可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。

  4、公平与非公平锁(根据加锁前是否检查检查排队情况划分)

    公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。

    非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。

      1) 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列;
      2)Java 中的 synchronized 是非公平锁, ReentrantLock 默认的 lock()方法采用的是非公平锁。

  5、共享锁和独占锁(根据java并发包提供的加锁模式划分)

    独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

    共享锁则允许多个线程同时获取锁,并发访问 共享资源,如: ReadWriteLock。 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

      1)AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。    

      2)java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问或者被一个 写操作访问,但两者不能同时进行。

  6、重量级锁与轻量级锁(根据是否依赖操作系统的Mutex Lock实现锁)

    Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此, 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁” 。 JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。
    JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

    “轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前, 先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

    Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。 偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。 引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令, 而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所
以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过, 轻量级锁是为了在线程交替执行同步块时提高性能, 而偏向锁则是在只有一个线程执行同步块时进一步提高性能

    引入以上锁概念后,锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

  7、同步锁和死锁

    当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
    死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

    在计算机系统中,如果系统的资源分配策略不当,更常见的可能是发生程序员写的程序有错误的情况导致进程因竞争自不当而产生死锁的现象。产生死锁的四大必要条件:

      1)互斥条件:一个资源每次只能被一个进程使用。

      2)请求与保持条件:当一个进程因请求资源而被阻塞时,对已获得的资源不会释放。

      3)不可剥夺条件:进程已获得的资源,在未使用完之前不能强行被剥夺。

      4)循环等待条件:若干进程之间形成一个钟收尾相接的循环等待资源关系。

    只要系统发送死锁,这些条件必然成立,若以上有一条不满足就不会发送死锁。
  8、
锁优化:锁优化是为了提高锁的效率,具体方法有如下几种

    减少锁持有时间:只用在有线程安全要求的程序上加锁

    减小锁粒度:将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

    锁分离:最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据

    锁粗化:通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度, 如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 

    锁消除:锁消除是在编译器级别的事情。 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。

 





  

    

 





posted on 2021-12-28 13:32  池塘里洗澡的鸭子  阅读(91)  评论(0编辑  收藏  举报