可重入锁

使用Java进行多线程开发,使用锁是一个几乎不可避免的问题。今天,就让我们来聊一聊这个基础,但是又特别特别重要的话题。

首先,让我们来看一下,到底什么是锁? 以及,为什么要使用锁?

如果有2个线程,需要访问同一个对象User。一个读线程,一个写线程。User对象有2个字段,一个是名字,一个是手机号码。

image

当User对象刚刚创建出来的时候,姓名和手机号码都是空。然后,写线程开始填充数据。最后,就出现了以下令人心碎的一幕:

image

可以看到,虽然写线程先于读线程工作,但是, 由于写姓名和写电话号码两个操作不是原子的。这就导致读线程只读取了半个数据,在读线程看来,User对象的电话号码是不存在。

为了避免类似的问题,我们就需要使用锁。让写线程在修改对象前,先加锁,然后完成姓名和电话号码的赋值,再释放锁。而读线程也是一样,先取得锁,再读,然后释放锁。这样就可以避免发生这种情况。

如下图所示:

image

什么是重入锁?

好了,现在大家知道我们为什么要使用锁了。那什么是重入锁呢。通常情况下,锁可以用来控制多线程的访问行为。那对于同一个线程,如果连续两次对同一把锁进行lock,会怎么样了?对于一般的锁来说,这个线程就会被永远卡死在那边,比如:


voidhandle(){

lock();

lock();//和上一个lock()操作同一个锁对象,那么这里就永远等待了

unlock();

unlock();

}

这个特性相当不好用,因为在实际的开发过程中,函数之间的调用关系可能错综复杂,一个不小心就可能在多个不同的函数中,反复调用lock(),这样的话,线程就自己和自己卡死了。

所以,对于希望傻瓜式编程的我们来说,重入锁就是用来解决这个问题的。重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程卡死。因此,如果我们使用的是重入锁,那么上述代码就 可以正常工作。你唯一需要保证的,就是unlock()的次数和lock()一样多。这样是不是方便很多呢?

Java中的重入锁

Java中的锁都来自与Lock接口,如下图中红框内的,就是重入锁。

image

重入锁提供的最重要的方法就是lock()

void lock():加锁,如果锁已经被别人占用了,就无限等待。

这个lock()方法,提供了锁最基本的功能,拿到锁就返回,拿不到就等待。因此,大规模得在复杂场景中使用,是有可能因此死锁的。因此,使用这个方法得非常小心。

如果要预防可能发生的死锁,可以尝试使用下面这个方法:

boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,等待timeout时间。同时,可以响应中断。

这是一个比单纯lock()更具有工程价值的方法,如果大家阅读过JDK的一些内部代码,就不难发现,tryLock()在JDK内部被大量的使用。

与lock()相比,tryLock()至少有下面一个好处:

可以不用进行无限等待。直接打破形成死锁的条件。如果一段时间等不到锁,可以直接放弃,同时释放自己已经得到的资源。这样,就可以在很大程度上,避免死锁的产生。因为线程之间出现了一种谦让机制

可以在应用程序这层进行进行自旋,你可以自己决定尝试几次,或者是放弃。

等待锁的过程中可以响应中断,如果此时,程序正好收到关机信号,中断就会触发,进入中断异常后,线程就可以做一些清理工作,从而防止在终止程序时出现数据写坏,数据丢失等悲催的情况。

当然了,当锁使用完后,千万不要忘记把它释放了。不然,程序可能就会崩溃啦~

void unlock() :释放锁

此外, 重入锁还有一个不带任何参数的tryLock()。

public boolean tryLock()

这个不带任何参数的tryLock()不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false,特别适合在应用层自己对锁进行管理,在应用层进行自旋等待。

重入锁的实现原理

重入锁内部实现的主要类如下图:

image

重入锁的核心功能委托给内部类Sync实现,并且根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。

实现重入锁的方法很简单,就是基于一个状态变量state。这个变量保存在AbstractQueuedSynchronizer对象中


privatevolatileintstate;

当这个state==0时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数。因此,lock()的最简单的实现是:


finalvoidlock(){

// compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁

if(compareAndSetState(0,1))

setExclusiveOwnerThread(Thread.currentThread());

else

//如果修改不成功,说明别的线程已经使用了这个锁,那么就可能需要等待

acquire(1);

}

下面是acquire() 的实现:


publicfinalvoidacquire(intarg){

//tryAcquire() 再次尝试获取锁,

//如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,

//同时宣布获得所成功,这正是重入的关键所在

if(!tryAcquire(arg) &&

// 如果获取失败,那么就在这里入队等待

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

//如果在等待过程中 被中断了,那么重新把中断标志位设置上

selfInterrupt();

}

公平的重入锁

默认情况下,重入锁是不公平的。什么叫不公平呢。也就是说,如果有1,2,3,4 这四个线程,按顺序,依次请求锁。那等锁可用的时候,谁会先拿到锁呢?在非公平情况下,答案是随机的。如下图所示,可能线程3先拿到锁。

image

如果你是一个公平主义者,强烈坚持先到先得的话,那么你就需要在构造重入锁的时候,指定这是一个公平锁:


ReentrantLock fairLock =newReentrantLock(true);

这样一来,每一个请求锁的线程,都会乖乖的把自己放入请求队列,而不是上来就进行争抢。但一定要注意,公平锁是有代价的。维持公平竞争是以牺牲系统性能为代价的。如果你愿意承担这个损失,公平锁至少提供了一种普世价值观的实现吧!

那公平锁和非公平锁实现的核心区别在哪里呢?来看一下这段lock()的代码:


//非公平锁 

finalvoidlock(){

//上来不管三七二十一,直接抢了再说

if(compareAndSetState(0,1))

setExclusiveOwnerThread(Thread.currentThread());

else

//抢不到,就进队列慢慢等着

acquire(1);

}

//公平锁

finalvoidlock(){

//直接进队列等着

acquire(1);

}

从上面的代码中也不难看到,非公平锁如果第一次争抢失败,后面的处理和公平锁是一样的,都是进入等待队列慢慢等。

对于tryLock()也是非常类似的:


//非公平锁 

finalbooleannonfairTryAcquire(intacquires){

finalThread current = Thread.currentThread();

intc = getState();

if(c ==0) {

//上来不管三七二十一,直接抢了再说

if(compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

returntrue;

}

}

//如果就是当前线程占用了锁,那么就更新一下state,表示重复占用锁的次数

//这是“重入”的关键所在

elseif(current == getExclusiveOwnerThread()) {

//我又来了哦~~~

intnextc = c + acquires;

if(nextc <0)// overflow

thrownewError("Maximum lock count exceeded");

setState(nextc);

returntrue;

}

returnfalse;

}

//公平锁

protectedfinalbooleantryAcquire(intacquires){

finalThread current = Thread.currentThread();

intc = getState();

if(c ==0) {

//先看看有没有别人在等,没有人等我才会去抢,有人在我前面 ,我就不抢啦

if(!hasQueuedPredecessors() &&

compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

returntrue;

}

}

elseif(current == getExclusiveOwnerThread()) {

intnextc = c + acquires;

if(nextc <0)

thrownewError("Maximum lock count exceeded");

setState(nextc);

returntrue;

}

returnfalse;

}

Condition

Condition可以理解为重入锁的伴生对象。它提供了在重入锁的基础上,进行等待和通知的机制。可以使用 newCondition()方法生成一个Condition对象,如下所示。


privatefinalLock lock =newReentrantLock();

privatefinalCondition condition = lock.newCondition();

那Condition对象怎么用呢。在JDK内部就有一个很好的例子。让我们来看一下ArrayBlockingQueue吧。ArrayBlockingQueue是一个队列,你可以把元素塞入队列(enqueue),也可以拿出来take()。但是有一个小小的条件,就是如果队列是空的,那么take()就需要等待,一直等到有元素了,再返回。那这个功能,怎么实现呢?这就可以使用Condition对象了。

实现在ArrayBlockingQueue中,就维护一个Condition对象


lock =newReentrantLock(fair);

notEmpty = lock.newCondition();

这个notEmpty 就是一个Condition对象。它用来通知其他线程,ArrayBlockingQueue是不是空着的。当我们需要拿出一个元素时:


publicEtake()throwsInterruptedException{

finalReentrantLock lock =this.lock;

lock.lockInterruptibly();

try{

while(count ==0)

// 如果队列长度为0,那么就在notEmpty condition上等待了,一直等到有元素进来为止

// 注意,await()方法,一定是要先获得condition伴生的那个lock,才能用的哦

notEmpty.await();

//一旦有人通知我队列里有东西了,我就弹出一个返回

returndequeue();

}finally{

lock.unlock();

}

}

当有元素入队时:


publicbooleanoffer(E e){

checkNotNull(e);

finalReentrantLock lock =this.lock;

//先拿到锁,拿到锁才能操作对应的Condition对象

lock.lock();

try{

if(count == items.length)

returnfalse;

else{

//入队了, 在这个函数里,就会进行notEmpty的通知,通知相关线程,有数据准备好了

enqueue(e);

returntrue;

}

}finally{

//释放锁了,等着的那个线程,现在可以去弹出一个元素试试了

lock.unlock();

}

}

privatevoidenqueue(E x){

finalObject[] items =this.items;

items[putIndex] = x;

if(++putIndex == items.length)

putIndex =0;

count++;

//元素已经放好了,通知那个等着拿东西的人吧

notEmpty.signal();

}

因此,整个流程如图所示:

image

重入锁的使用示例

为了让大家更好的理解重入锁的使用方法。现在我们使用重入锁,实现一个简单的计数器。这个计数器可以保证在多线程环境中,统计数据的精确性,请看下面示例代码:


publicclassCounter{

//重入锁

privatefinalLock lock =newReentrantLock();

privateintcount;

publicvoidincr(){

// 访问count时,需要加锁

lock.lock();

try{

count++;

}finally{

lock.unlock();

}

}

publicintgetCount(){

//读取数据也需要加锁,才能保证数据的可见性

lock.lock();

try{

returncount;

}finally{

lock.unlock();

}

}

}

总结

可重入锁算是多线程的入门级别知识点,所以我把他当做多线程系列的第一章节,对于重入锁,我们需要特别知道几点:

对于同一个线程,重入锁允许你反复获得通一把锁,但是,申请和释放锁的次数必须一致。

默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁

重入锁的内部实现是基于CAS操作的。

重入锁的伴生对象Condition提供了await()和singal()的功能,可以用于线程间消息通信。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容