5.Lock接口及其实现ReentrantLock
jdk1.7.0_79
在java.util.concurrent.locks这个包中定义了和synchronized不一样的锁,重入锁——ReentrantLock,读写锁——ReadWriteLock等。在已经有了内置锁synchronized的情况下,为什么又出现了Lock显示锁呢?本文将以Lock作为Java并发包源码解读的开始.
Lock定义最基本的加锁和解锁操作。
|
Lock |
|
|
void lock(); |
阻塞方式获取锁,直到获取锁后才返回 |
|
void locklnterruptibly(); |
获取锁,除非当前线程被中断 |
|
Condition newCondition(); |
返回一个Condition实例绑定到这个锁实例 |
|
boolean tryLock(); |
不管是否获取到锁,都立即返回,非阻塞 |
|
boolean tryLock(long time, TimeUnit unit); |
在一定时间内阻塞获取锁 |
|
void unlock(); |
释放锁 |
Lock接口有一个实现类——重入锁ReentrantLock。进入ReentrantLock类中我们就发现它对于Lock接口的实现基本上都借助于一个抽象静态内部类Sync,该内部类继承自AbstractQueuedSynchronizer,接着又发现两个静态内部类NonfairSync、FairSync,这两个静态内部类又是继承自刚刚的Sync。这里就要引入两个新的概念了——公平锁与非公平锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。(《Java并发编程实战》)
ReentrantLock你可以称之为重入锁(递归锁)、显示锁、排他锁(独占锁),显示锁很好理解,即线程在获取锁和释放锁的时候都需要代码显示操作。重入锁是什么概念呢?synchronized实际上也是可重入锁,意思就是一个线程已经持有这个锁,当这个线程再次获得这个锁的时候不会被阻塞,而是使其同步状态计数器+1,这样做的目的当然就是为了防止死锁,当线程释放这个锁的时候,同步状态计数器-1,直到递减至0才表示这个锁完全释放完毕,其他线程可以获取。那什么又是排他锁呢?我们直到AQS定义了两种模式下获取锁与释放锁的操作,那就是独占模式和共享模式,所谓独占模式就是只有一个线程能持有这个锁,而共享模式则是这个锁可以由多个线程所持有。例如ReebtrabtReadWriteLock的读锁就能由多个线程所持有。在知道了ReentrantLock的特性之后,我们再来看它其内部实现。
在前两节解析AQS的时候我们就提到,AQS所提供的同步器是实现锁的基础框架,固然ReentrantLock同样也是基于AQS,而ReentrantLock并没有直接实现AQS抽象类,而是将在其内部定义一个Sync内部类来聚合AQS,这样聚合而不是继承的目的是为了将锁的具体实现与锁的使用做一个隔离,锁的使用者关心的是锁如何才能被正确使用,而锁的实现者关心的是锁如何基于AQS被正确的实现。先讨论ReentrantLock$Sync抽象内部类。在讨论前先回顾一下能够被子类重写的AQS方法有哪些:
|
AbstractQueuedSynchronizer |
|
|
protected boolean tryAcquire(int arg) |
子类可实现在独占模式下获取同步状态的具体方法。 |
|
protected boolean tryRelease(int arg) |
子类可实现在独占模式下释放同步状态的具体方法。 |
|
protected int tryAcquireShared(int arg) |
子类可实现在共享模式下获取同步状态的具体方法。 |
|
protected int tryReleaseShared() |
子类可实现在共享模式下释放同步状态的具体方法。 |
|
protected boolean isHeldExclusively() |
当前同步器是否在独占模式下被线程占用,一般表示该方法是否被当前线程所独占。 |
通过查看ReentrantLock$Sync的源码可知,Sync一共对AQS重写了这么几个方法:
protected final boolean tryRelease(int release)
protected final boolean isHeldExclusively()
为什么Sync只重写了这两个方法呢?实际上在ReentrantLock的内部还有另外两个内部类NonfairSync非公平锁和FairSync公平锁,这两个内部类是Sync的具体实现,很显然能够得出,对于锁的获取非公平锁和公平锁的实现是不一样的,而对于锁的释放两者均是相同实现。针对ReentrantLock的非公平锁和公平锁接下来我们来一一探讨他们的不同点和相同点。
在ReentrantLock定义了一个成员变量——sync,并且他提供了两个构造方法,其默认无参构造方法创建的是非公平锁,而有参的构造方法则传入一个boolean类型来决定构造一个公平锁还是非公平锁。
public class ReentrantLock implements Lock {
private final Sync sync;
public ReentrantLock() {
sync = new NofairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
……
}
1.lock()
针对开篇提到的Lock接口定义的方法,我们先来看ReentrantLock对Lock#lock的实现:
public class ReentrantLock implements Lock {
……
public void lock() {
sync.lock();
}
……
}
这个方法是抽象内部类定义的一个抽象方法,从命名可以看到这个类实际上就是AQS的acquire获取锁的具体实现,在这里我们能看到非公平锁和公平锁对获取锁的不同实现,我们先来看非公平锁对Sync#lock的实现:
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquire) {//在ReentrantLock$NonFairLock才终于看到了对AbstractQueuedSynchronizer#tryAcquire的具体实现。
return nonfairTryAcquire(acquires);//而tryAcquire的实现实际上又是在其父类ReentrantLock$Lock中实现的,好像有点绕,一会子类实现,一会父类实现。可以先这么来理解,既然它把tryAcquire的具体实现又定义在了父类,那说明这一定是父类对公共方法的抽取(Extract Method),其他地方一定有用到nonfairTryAcquire方法,不然JDK的作者不会闲的蛋疼。
}
}
ReentrantLock$Sync中非公平锁的获取锁的实现
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;
}
我们说回ReentrantLock$NonFairLock非公平锁的lock阻塞获取锁的实现,在调用非公平锁的lock方法时,就会首先进行“抢锁”操作,也就是compareAndSetState这个方法是利用的CAS底层方法看能否抢占到锁,而不是按照先后获取获取锁的方式放到同步队列中获取锁,公平锁就是这样。既然说到了公平锁获取锁的方式,我们不妨和ReentrantLock$FairLock作一个对比:
static final class FairSync extends Sync {
inal void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {//在ReentrantLock$NonFairLock我们看到它老老实实的实现了AQS定义的tryAcquire方法,而没有调用父类的方法,从这里我们也基本能推断在ReentrantLock中没有其他地方会引用到这个方法。
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true;
}
return false;
}
}
从公平锁(nonfairTryAcquire)和非公平锁(tryAcquire)的两个方法对比可知:公平锁在获取锁的时候首先会判断当前线程是否有前驱节点已经在队列中等待,如果有返回true,则不进行获取锁的操作,这一点就是和公平锁最大的区别,只有当前线程没有前驱节点才获取锁。ReentrantLock默认构造非公平锁,而实际上用得最多的也是非公平锁,公平锁从一定程度上能防止“饥饿”,但非公平锁在性能上却优于公平锁,我们做以下试验得知的确如此(详细试验过程《【试验局】ReentrantLock中非公平锁与公平锁的性能测试》)
2.lockInterruptibly()
这个方法和lock方法的区别就是,lock会一直阻塞下去直到获取到锁,而lockInterruptibly则不一样,它可以响应中断而停止阻塞返回。ReentrantLock对其的实现是调用的Sync的父类AbstractQueuedSynchronizer#acquireInterruptibly方法:
//ReentrantLock#lockInterruptibly
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);//因为ReentrantLock是排它锁,故调用AQS的acquireInterruptibly方法
}
//AbstractQueuedSynchronizer#acquireInterruptibly
public final void acquireInterruptibly(int arg) throws InterruptedException{
if (Thread.interrupted()) //线程是否被中断,中断则抛出中断异常,并停止阻塞
throw new InterruptedException;
if (!tryAcquire(arg)) //首先还是获取锁,具体参照上文
doAcquireInterruptibly(arg);//独占模式下中断获取同步状态
}
通过查看doAcquireInterruptibly的方法实现不难发现它和acquireQueued大同小异,前者抛出异常,后者返回boolean。具体实在不再讨论,参照源码以及《2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放》。
3.tryLock()
此方法为非阻塞式的获取锁,不管有没有获取锁都返回一个boolean值。
//ReentrantLock#tryLock
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
可以看到它实际调用了Sync#nonfairTryAcquire非公平锁获取锁的方法,这个方法我们在上文lock()方法非公平锁获取锁的时候有提到,而且还特地强调了该方法不是在NonfairSync实现,而是在Sync中实现很有可能这个方法是一个公共方法,果然在非阻塞获取锁的时候调用的是此方法。详细解析参照上文。
4.tryLock(long timeout, TimeUnit unit)
此方法是表示在超时时间内获取到同步状态则返回true,获取不到则返回false。由此可以联想到AQS的tryAcquireNanos(int arg, long nanosTimeOut)方法
//ReentrantLock#tryLock
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
果然Sync实际上调用了父类AQS的tryAcquireNanos方法。
//AbstractQueuedSynchronizer#tryAcquireNanos
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();//可以看到前面和lockInterruptibly一样
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);//首先也会先尝试获取锁
}
在doAcquireNanos实际也和acquireQueued、doAcquireInterruptibly差不多,不同的是增加了超时判断。
关于Lock和ReentrantLock介绍到这里,在AQS和这里遗留了一个问题——Condition,在下一节中单独介绍Condition。
5.Lock接口及其实现ReentrantLock的更多相关文章
- jdk1.5多线程Lock接口及Condition接口
jdk1.5多线程的实现的方式: jdk1.5之前对锁的操作是隐式的 synchronized(对象) //获取锁 { } //释放锁 jdk1.5锁的操作是显示的:在包java.util.concu ...
- synchronized关键字,Lock接口以及可重入锁ReentrantLock
多线程环境下,必须考虑线程同步的问题,这是因为多个线程同时访问变量或者资源时会有线程争用,比如A线程读取了一个变量,B线程也读取了这个变量,然后他们同时对这个变量做了修改,写回到内存中,由于是同时做修 ...
- Java多线程的~~~Lock接口和ReentrantLock使用
在多线程开发.除了synchronized这个keyword外,我们还通过Lock接口来实现这样的效果.由Lock接口来实现 这样的多线程加锁效果的优点是非常的灵活,我们不在须要对整个函数加锁,并且能 ...
- 线程同步 Lock接口
同步:★★★★★ 好处:解决了线程安全问题. 弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁. 定义同步是有前提的: 1,必须要有两个或者两个以上的线程,才需要同步. 2,多个线程必须保证使用 ...
- Java基础知识强化之多线程笔记06:Lock接口 (区别于Synchronized块)
1. 简介 我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式 ...
- Java多线程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
在JDK5里面,提供了一个Lock接口.该接口通过底层框架的形式为设计更面向对象.可更加细粒度控制线程代码.更灵活控制线程通信提供了基础.实现Lock接口且使用得比较多的是可重入锁(Reentrant ...
- 多线程里面的关键字,wait, notfiy, 锁(synchronized), lock接口
多线程环境下,必须考虑线程同步的问题,这是因为多个线程同时访问变量或者资源时会有线程争用,比如A线程读取了一个变量,B线程也读取了这个变量,然后他们同时对这个变量做了修改,写回到内存中,由于是同时做修 ...
- java多线程Lock接口简介使用与synchronized对比 多线程下篇(三)
前面的介绍中,对于显式锁的概念进行了简单介绍 显式锁的概念,是基于JDK层面的实现,是接口,通过这个接口可以实现同步访问 而不同于synchronized关键字,他是Java的内置特性,是基于JVM的 ...
- java 锁 Lock接口详解
一:java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的) (1)Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是Reen ...
随机推荐
- 基于Spring开发——自定义标签及其解析
1. XML Schema 1.1 最简单的标签 一个最简单的标签,形式如: <bf:head-routing key="1" value="1" to= ...
- 字典树 Trie树
什么是Trie树? 形如 其中从根节点到红色节点的路径上的字母所连成的字符串即为一个Trie树上所存的字符串. 比如,这个trie树上有ab,abc,bd,dda这些字符串. 至于怎么构建和查找或添加 ...
- java复习(8)---I/O
本节复习java常用i/o,输入输出流. 先放上样例代码.方便参考,可以轻松看懂. package re08; import java.io.*; import java.util.Scanner; ...
- HTTP 协议
HTTP 协议对应 Web 开发者来说都必须要了解的,无论技术背景或首选编程语言是什么,"请求-响应" 对话是驱动 Web 上通信的基础. HTTP 概述 HTTP 协议是 Hyp ...
- javascript——数据类型
在内存中,分为栈.堆.代码段.静态区,为了快速处理复杂的代码,在不同的区间储存不同的数据类型. 数据类型分为初始类型与引用类型,初始类型在栈中存储,变量赋值传值不传址,引用类型在堆中存储,传址不传值. ...
- 读书笔记 effective c++ Item 53 关注编译器发出的警告
许多程序员常常忽略编译器发出的警告.毕竟,如果问题很严重,它才将会变成一个error,不是么?相对来说,这个想法可能在其它语言是无害的,但是在C++中,我敢打赌编译器的实现者对于对接下来会发生什么比你 ...
- 使用Perl提取Excel中的IO_MUX
使用Perl提取Excel中的IO_MUX 关键问题 提取数据 格式化输出 循环嵌套 数据结构构建 坐标映射,逆向提取关键字 描述 在IC集成中,我们使用Excel表格规划设计的IC引脚功能映射需要转 ...
- 使用SSH框架查出的实体集合用AJAX形式刷新到页面JOSONArray报异常
所报异常:net.sf.json.JSONException: java.lang.reflect.InvocationTargetException 问题根源:数据库中查出的日期是java.sql. ...
- Native App和Web App 的差异
开发者们都知道在高端智能手机系统中有两种应用程序:一种是基于本地(操作系统)运行的APP:一种是基于高端机的浏览器运行的WebApp,本文将主要讲解后者. WebApp与Native App有何区别呢 ...
- Springmvc的工作流程
1.向服务器发送http请求,请求被前端控制器DispatcherServlet捕获. 2.DispatcherServlet根据servlet.xml中的配置进行URL解析后,得到(URL),然后根 ...