美文网首页
JUC学习笔记-锁

JUC学习笔记-锁

作者: 胖小白_d797 | 来源:发表于2018-12-17 20:13 被阅读0次

    1.为什么需要锁

    解决多个线程访问同一个可变的状态变量时的安全问题。

    2.内置锁 - synchronized

    任何一个非null对象都可以作为。内置锁是可重入的非公平锁。在JVM中,内置锁也叫对象监视器;

    • 使用方法

      • 1.作用在方法上,持有对象实例this的锁

          public synchronized void doSomething() {
              //方法体
          }
        
      • 2.作用在代码块上,持有对象实例lockObject的锁

          public int doSomething(){
        
              synchronized(lockObject) {
        
                //一次只能有一个线程进入
        
              }
          }
        
      • 3.作用在静态方法上,持有类(this.getClass)的锁

          public synchronized static void doSomething() {
             // todo
          }
        
      • 4.作用在类上,持有类(class)的锁

          class ClassName {
              public void method() {
                 synchronized(ClassName.class) {
                   // todo
                 }
              }
          }
        
    • 实现原理
      synchronized的加锁和释放都是由JVM提供,在软件层面依赖JVM,当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:

      Contention List:所有请求锁的线程将被首先放置到该竞争队列

      Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List。

      Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set。

      OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck。

      Owner:获得锁的线程称为Owner。

      !Owner:释放锁的线程。

      lock1.jpg

    新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现 EntryList为空则从ContentionList中移动线程到EntryList,下面说明下ContentionList和EntryList 的实现方式:

    • [ ] ContentionList 虚拟队列
      ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指 针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行, 通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock- Free的队列.因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。
    lock2.jpg
    • [ ] EntryList

    EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对 ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到 EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给 OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。

    OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不 发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒, 则再次转移到EntryList。

    • [ ] 自旋锁

    那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通 过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响 锁的性能

    缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋), 在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法 获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非 常重要的性能提高。自旋锁有个更贴切的名字:自旋-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。

    线程在进入等待队列ContentionList时,也即第一步操作前.首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了 Ready线程的锁。

    • [ ] 偏向锁

    在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题.
    现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操 作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个 线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

    • Mark Word

    HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等.

    • [ ] 未被锁定状态:


      lock4.png
    • [ ] 其他状态:


      lock3.png

    openjdk\hotspot\src\share\vm\oops\oop.hpp下oopDesc类是JVM对象的顶级基类,故每个object都包含markOop。

       class oopDesc {
         friend class VMStructs;
        private:
         volatile markOop  _mark;//markOop:Mark Word标记字段
         union _metadata {
           Klass*      _klass;//对象类型元数据的指针
           narrowKlass _compressed_klass;
         } _metadata;
       
         // Fast access to barrier set.  Must be initialized.
         static BarrierSet* _bs;
       
        public:
         markOop  mark() const         { return _mark; }
         markOop* mark_addr() const    { return (markOop*) &_mark; }
       
         void set_mark(volatile markOop m)      { _mark = m;   }
       
         void    release_set_mark(markOop m);
         markOop cas_set_mark(markOop new_mark, markOop old_mark);
       
         // Used only to re-initialize the mark word (e.g., of promoted
         // objects during a GC) -- requires a valid klass pointer
         void init_mark();
       
         Klass* klass() const;
         Klass* klass_or_null() const volatile;
         Klass** klass_addr();
         narrowKlass* compressed_klass_addr();
       ....省略...
       }
    

    在HotSpot虚拟机中,最终采用ObjectMonitor类实现monitor。
    openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp源码如下:

            ObjectMonitor() {
             _header       = NULL;//markOop对象头
             _count        = 0;
             _waiters      = 0,//等待线程数
             _recursions   = 0;//重入次数
             _object       = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
             _owner        = NULL;//指向获得ObjectMonitor对象的线程或基础锁
             _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set;
             _WaitSetLock  = 0 ;
             _Responsible  = NULL ;
             _succ         = NULL ;
             _cxq          = NULL ;
             FreeNext      = NULL ;
             _EntryList    = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
             _SpinFreq     = 0 ;
             _SpinClock    = 0 ;
             OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
             previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
             }
    

    3.显示锁 - Lock

    synchronized存在局限性,例如:

    1. 占有锁的线程执行时间较长,不想让其他线程无期限地等待下去
    2. 读写锁中,读操作线程之间不发生冲突
    3. 无法得知线程有没有成功获取到锁
    lock5.png lock6.png
    • 共享锁
      ReentrantLock使用的是独占锁,Semaphore,CountDownLatch,ReentrantReadWriteLock使用的是共享锁。
      独占锁与共享锁的实现大同小异。

      • 独占锁:

      state为0代表有资源,state大于1代表锁已经被其他线程获取。
      独占锁是只有头节点获取锁,其余节点的线程继续等待,等待锁被释放后,才会唤醒下一个节点的线程;
      独占锁的同步状态state值在0和1之间切换,保证同一时间只能有一个线程是处于活动的,其他线程都被阻塞,参考ReentranLock。当owner线程再次重入时,state在1到N间切换。
      独占锁是一种悲观锁。

      • 共享锁:

      state值在整数区间内(自定义实现),如果state值<0则阻塞,否则不阻塞。
      共享锁是只要头节点获取锁成功,若剩余资源大于0,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。
      共享锁是一种乐观锁,允许多个线程同时访问共享资源。

    相关文章

      网友评论

          本文标题:JUC学习笔记-锁

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