ReentraneLock & synchronized & AQS

sychronized  (monitor监视器)  -- 自旋获取锁形式

  把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。可见性则更为微妙;它要对付内存缓存和编译器优化的各种反常行为。

  monitorenter  & monitorexit

ReentranLock -- 阻塞获取锁形式

  Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

  reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

 

   ReentrantLock 构造器的一个参数是 boolean 值,它允许您选择想要一个 公平(fair)锁,还是一个 不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。在现实中,公平保证了锁是非常健壮的锁,有很大的性能成本。要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,应当把公平设置为 false ,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。

 

在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。

 

 

ReentrantLock之lock形式  -- for (;;){} 循环获取锁 -- 自旋

  1、根据构造器创建lock,设定为公平锁or不公平锁

  2、lock,获取锁

    添加至Node队列,包含上Node地址,下Node地址,以及线程信息

    获取锁队列,从head位开始,尝试去获取锁   

      获取方式:获取当前线程信息,得到锁当前状态,对处于state为0的,通过CAS操作,成功后将当前线程设置至可执行线程ExclusiveOwnerThread 。

            如果失败,调用Thread的interrupted(),中断线程

  3、unlock

    更改状态,将ExclusiveOwnerThread 置为空

 

ReentrantLock扩展的功能

   实现可轮询的锁请求 

在内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。而可轮询的锁获取模式具有更完善的错误恢复机制,可以规避死锁的发生。 
如果你不能获得所有需要的锁,那么使用可轮询的获取方式使你能够重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。可轮询的锁获取模式,由tryLock()方法实现。此方法仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。此方法的典型使用语句如下: 

  

Lock lock = ...;   
if (lock.tryLock()) {   
try {   
// manipulate protected state   
} finally {   
lock.unlock();   
}   
} else {   
// perform alternative actions   
}   

  

实现可定时的锁请求 

   当使用内部锁时,一旦开始请求,锁就不能停止了,所以内部锁给实现具有时限的活动带来了风险。为了解决这一问题,可以使用定时锁。当具有时限的活 

   动调用了阻塞方法,定时锁能够在时间预算内设定相应的超时。如果活动在期待的时间内没能获得结果,定时锁能使程序提前返回。可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。 

  只在时间范围内去获取锁,超出时间则认为无法获取锁。

 

 实现可中断的锁获取请求 

  可中断的锁获取操作允许在可取消的活动中使用。lockInterruptibly()方法能够使你获得锁的时候响应中断。

 

ReentrantLock不好与需要注意的地方

(1) lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放
(2) 当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。
 
 
Condition -- 条件变量
  条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。
 
  每一个Lock可以有任意数据的Condition对象,Condition是与Lock绑定的,所以就有Lock的公平性特性:如果是公平锁,线程为按照FIFO的顺序从Condition.await中释放,如果是非公平锁,那么后续的锁竞争就不保证FIFO顺序了。
  await() 操作实际上就是释放锁,然后挂起线程,一旦条件满足就被唤醒,再次获取锁;
  signal() 就是唤醒Condition队列中的第一个非CANCELLED节点线程;
  signalAll() 就是唤醒所有非CANCELLED节点线程。当然了遇到CANCELLED线程就需要将其从FIFO队列中剔除。

  完整的await()操作是安装如下步骤进行的:

    1. 将当前线程加入Condition锁队列。特别说明的是,这里不同于AQS的队列,这里进入的是Condition的FIFO队列。进行2。
    2. 释放锁。这里可以看到将锁释放了,否则别的线程就无法拿到锁而发生死锁。进行3。
    3. 自旋(while)挂起,直到被唤醒或者超时或者CACELLED等。进行4。
    4. 获取锁(acquireQueued)。并将自己从Condition的FIFO队列中释放,表明自己不再需要锁(我已经拿到锁了)

 

 

 

FIFO(固定长度队列)

 

AQS的全称为(AbstractQueuedSynchronizer)

  AQS是链表,有一个head引用来指向链表的头节点,AQS在初始化的时候head、tail都是null,在运行时来回移动。此时,我们最少至少知道AQS是一个基于状态(state)的链表管理方式。

  列的结构就变成了以下这种情况了,通过这样的方式,就可以让执行完的节点释放掉内存区域,而不是无限制增长队列,也就真正形成FIFO了。

 

  AQS获取锁是通过tryAcquire去获取的,本身并没有获取方式,释放锁是release,通过tryRelease。

 

 

原理:

  ReentraneLock 是Lock的一种实现,也是java层次锁的体现,而非synchronized语言特性的形式。ReentraneLock最基本的核心在于AQS,AbstractQuenedSynchronizer提供了一系列的模板方法,

通过将lock和unlock操作分别交给子类去实现,AQS提供了公平锁和非公平锁2种模式。而ReentraneLock是使用的Sync,是AQS的一个子类,使用的是非公平锁。

  AQS会把请求线程放入一个CLH队列中,由线程去尝试获取锁,如果成功,则将当前运行线程设置为该线程,并将state状态从0设置为1,再重入则会继续加1。如果线程尝试去获取锁的时候,发现已经有

线程将锁独占,这时AQS会将线程包装为一个Node放入CLH队列的tail处,然后对线程进行阻塞。在阻塞前会再次的请求获取锁,如果还是没有获取到锁,则将线程阻塞。

 

CLH队列的含义:CLH为队列锁,队列中的Node对标注为是否需要获取锁。

posted @ 2017-02-21 17:30  斌灬小生不才  阅读(1568)  评论(0编辑  收藏  举报