本文概览:介绍了ReentrantLock的实现。在ReentrantLock中定义了一个具体同步器,通过这个同步器实现锁的lockunlock操作,并介绍了公平性和非公平性两种机制。

1 Lock

1.1  Lock介绍

在JDK5的并发包中有一个子包为java.concurrent.locks,它下面定义了Lock,ReadWriteLock,Condition三个接口,分别为重入锁,读写锁,锁条件判断。

(1)Lock的具体锁为Reebtrantlock。

(2)ReadWriteLock的具体的锁为ReebtrantReadWritelock

(3)Conditiron需要由具体锁来获取。Lock相当于synchronized,Codition相当于wait/notify/notifyAll

1.2 ReentrantLock和AQS的比较

ReentrantLock实现了AQS的tryAcquire和tryRelease接口逻辑,这是线程争抢锁和释放锁逻辑。对于tryAcquire,提供了NofairSyn和FairSyn两种机制。

1.3 Lock和Synchronized比较

有了synchronized还引入Lock,说明Lock扩展了synchronized的功能,具体扩展功能如下:

  • A synchronized block makes no guarantees about the sequence in which threads waiting to entering it are granted access。 Lock可以保证线程顺序执行。
  • 使用synchronized可能发生线程饥饿,即某一个线程可能一直被阻塞无法执行。可以使用Lock来解决线程饥饿. 通过公平同步器FairSyn来实现。
  • You cannot pass any parameters to the entry of a synchronized block. Thus, having a timeout trying to get access to a synchronized block is not possible。可以设置超时
  • The synchronized block must be fully contained within a single method. A Lock can have it’s calls to lock() and unlock() in separate methods.  即lock()和unlock()方法可以出现在不同函数中,不一定必须在一个函数中。

1.4 两个重要特性

如下是一个Lock简单实现:

除了线程互斥上锁的功能,一个锁还应该具有两个重要特性:

  • 可重入。当前线程可以重复获取锁。
  • 公平性,解决线程饥饿。

2 ReetrantLock的具体同步器Sync

因为为排它模式,所以需要实现如下两个方法

  • protected boolean tryAcquire(int arg) 排它的获取这个同步器,并设置同步器的状态。
  • protected boolean tryRelease(int arg)  排它模式下释放同步器。并设置同步器的状态。

2.1 tryAcquire(int arg)

tryAcquire作用就是获取同步器的策略。包括如下两步:

  • 第一步 判断同步器状态是否被占用
  • 第二步 如果没有被占用,则修改同步器状态并且设置同步器当前线程的值。

目前主要包括公平性和非公平性两种情况,如下。

2.1.1 对于NonfairSync

1、作用

新的任务会和Head->Next的节点线程进行抢夺同步器了。所以极端会出现线程饥饿现象:每一次执行完unlock操作,就会新来一个线程,假设每一次都是这个新来的线程获取到同步器,而FIFO的对列Head->next线程节点一直无法获取同步器,此时就是FIFO对列的线程节点就是饥饿现象,即一直获取不到同步器。

2、实现

通过“ if (compareAndSetState(0, 1))”实现了抢占的功能,

2.1.2 对于FairSync

1、FiaSync作用

NofairSyn同步器会出现线程饥饿,为了解决线程饥饿,引入了FairSyn同步器,保证获取同步器的顺序就是先来先得。

2、实现

在FairSync的tryAcquire()相当于nofairTryAcquire( )函数多了一个hasQueuedPredecessors判断,如下代码

发现公平性锁的tryAcqurie代码比非公平性锁的代码多了一个hasQueuedPredecessors判断,所以现在问题核心就是了解hasQueuedPredecessors函数:

总结 hasQueuedPredecessors的作用:通过hasQueuedPredecessors这个函数就可以确保,有线程需要获取同步期时,如果说此时FIFO对列中已经有阻塞线程了,那么此时就不会进行抢占同步器,直接进入到阻塞对列

3、 公平性锁在进行hasQueuedProcessors判断时,为什么判断当前线程是阻塞队列的第一个节点?

当判断阻塞队列中含有任务时,此时还需要判断当前这个线程就是head->next的线程,如果是,此时就不阻塞了。举例:head->h1->h2  ,此时h1执行tryAcuqired()调用hasQueuedProcessor时,如果只判断阻塞队列是否存在元素,那么此时就会进行阻塞,但是这个阻塞队列的第一个元素是它自己,所以不能进行阻塞。

2.1.3 FairSync与NofairSyn的区别

非公平锁和公平锁的区别就是获取锁的方式不同:

(1)公平性锁可以保证获取锁是按任何使用同步器的前后顺序一次获取锁

(2)非公平性的锁是抢占式的获取锁。是新线程和Head-First节点抢占锁。

(3)举例来说明上述的区别

就是在执行一个unlock操作之后,此时如果恰好过来一个任务

  • 对于非公平性锁,这个任务就会和Head->Next的节点线程进行抢夺同步器了;
  • 对于公平性锁,则直接就会进入到阻塞对列。

2.2  tryRelease( int arg )

源码如下

3 ReentrantLock 对外接口

3.1 初始化

1、默认

2、可选择公平性锁或者非公平性锁

3.2 lock()

通过同步器的acquire操作来实现,如下是公平性同步器的lock操作

3.3 lockInterruptibly()

1. 作用

这个是为了使lock()操作具有interrupt功能。

先说说线程的打扰机制,每个线程都有一个 打扰 标志。这里分两种情况,

  • 情况1  线程在sleep或wait,join, 此时如果别的进程调用此进程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException;(thread在做IO操作时也可能有类似行为,见java thread api)
  • 情况2 此线程在运行中, 则不会收到提醒。但是 此线程的 “打扰标志”会被设置, 可以通过isInterrupted()查看并 作出处理。

所以。在线程执行interrupt时:

  • lockInterruptibly()属于情况1
  • lock()属于情况2

2 实现

通过AQS的acquireInterruptibly来实现:

3.4 tryLock()

1、代码实现

2、作用

如果没有获取锁,那么这个线程就不会被阻塞,而是执行其他操作。(对于lock()方法,如果没有获取锁,则此线程就会被阻塞),tryLock()方法使用如下:

3.5 tryLock(long timeout, TimeUnit timeUnit)

1. 解析

在没有获取到锁时,可以等待timout的时间,超过这个时间就会执行没有获取锁的情况。即就是在tryLock()方法增加了一个等待时间而已。

2. 通过同步器的tryAcquirenanos来实现

最终通过LockSupport.parkNanos实现

3.6 unlock()

通过同步器AQS的release操作来实现

4 应用举例

4.1 锁使用模板

根据资源分类:

  • 资源是一个对象,那么对对对象进行操作时(如增加、较少等操作)需要添加锁。
  • 资源对象是一个函数,那么在调用这个函数时添加锁

具体函数如下

4.2 具体实例

如下一个具体代码举例:

5 相关问题

5.1 为什么得到一个condition实例对象时要首先得到一个lock对象

首先理解synchronized与wait/notify/notifyAll关系:

有synchronized的地方不一定有wait,notify,但是又wait-notify的地方必须和Synchronized一块使用。即调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {…} 代码段内。

因为lock作用相当于synchronized; condition相当于wait/notify/notifyAll。所以,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。

5.2 如何保证下次线程执行时仍然从Lock.lock()地方开始执行

就是通过LockSupport.unpark和LockSupport.park来记录执行程序的位置,所以下一次程序重新开始执行的地方就是LockSupport.park的地方。

5.3 Lock、Condition和同步器三个类型关系

1、Condition是基于同步器对象存在的,因为具体condition都是定义在一个同步器类中。如

AbstractQueuedSynchronizer定义了一个具体的condition类:ObjectCondition.

2、Lock的操作接口都是通过同步器提供的接口来实现的。

(全文完)

 

 

 

分类&标签