作者个人研发在高并发场景下提供了一个简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。开源半年多以来,已成功为十几家中小企业提供精准定时调度解决方案,经受住了生产环境的考验。为了造福更多的童鞋,这里提供一个开源的框架地址:https://github.com/sunshinelyz/mykit-delay写的比较早,java中提供了synchronized关键字,保证只有一个线程可以访问synchronized代码堵塞。既然提供了synchronized关键字,为什么JavaSDK包中还要提供Lock接口呢?这是在重新发明轮子,多此一举吗?今天,我们就一起来探讨一下这个问题。重新发明轮子?既然JVM提供了synchronized关键字来保证只有一个线程可以访问synchronized代码块,为什么还要提供Lock接口呢?这是在重新发明轮子吗?为什么Java设计者要这样做?让我们带着疑问一起往下看。为什么要提供Lock接口?很多朋友可能听说在Java1.5中,synchronized的性能不如Lock,但是在Java1.6之后,synchronized做了很多的优化,性能提升了很多。既然synchronized关键字的性能已经提升了,为什么还要用Lock呢?如果我们更深层次的思考,不难想到:我们在使用synchronized锁的时候不能主动释放锁,这就会涉及到死锁问题。死锁问题如果要发生死锁,必须具备以下四个必要条件,缺一不可。互斥条件某个资源在一段时间内只被一个线程占用。这时候如果其他线程请求资源,请求线程只能等待。不可让渡条件线程获得的资源在用完之前不能被其他线程强行夺走,即只能由获得该资源的线程释放(只能主动释放)。requestandholdcondition线程已经保留了至少一个资源,但是它又提出了新的资源请求,而该资源已经被其他线程占用了。这时候请求线程被阻塞了,但是它并没有放过已经拿到的资源。循环等待条件当死锁发生时,必然有一个进程等待队列{P1,P2,...,Pn},其中P1等待P2占用的资源,P2等待P3占用的资源,...,pn等待p1占用的资源,形成一个进程循环等待,循环中每个进程占用的资源同时被另外一个进程请求,即前一个进程占用的资源亲切的被当前进程占用后一个过程。synchronized的局限性如果我们的程序使用了synchronized关键字导致死锁,synchronized的关键在于它不能打破“不可解除”的死锁条件。这是因为synchronized在申请资源的时候,如果申请失败,线程直接进入阻塞状态,线程进入阻塞状态,什么也做不了,也不能释放线程已经占用的资源。但是,在大多数场景下,我们希望“不可剥夺”的条件能够被打破。也就是说,对于“非剥夺”条件,当占用了一些资源的线程进一步申请其他资源时,如果申请不成,可以主动释放自己占用的资源,这样非剥夺条件就会发生。被摧毁。如果我们自己重新设计锁来解决synchronized的问题,应该怎么设计呢?解决了问题,了解了synchronized的局限性之后,如果我们要自己实现一个同步锁,应该怎么设计呢?也就是我们在设计锁的时候,如何解决synchronized的局限性?在这里,我觉得可以从三个方面来思考这个问题。(1)中断响应能力。synchronized的问题在于持有锁A后,如果尝试获取锁B失败,则线程进入阻塞状态。一旦发生死锁,就没有机会唤醒被阻塞的线程。但是如果处于阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞线程发送中断信号的时候,它能够被唤醒,那么它就有机会释放一次锁A了握住。这违反了不可剥夺的条件。(2)支持超时。如果线程在一段时间内没有获取到锁,而不是进入阻塞状态,而是返回了错误,那么这个线程还有机会释放曾经持有的锁。这也会破坏不可剥夺性的条件。(3)非阻塞地获取锁。如果尝试获取锁失败,没有进入阻塞状态,而是直接返回,那么这个线程也有机会释放曾经持有的锁。这也会破坏不可剥夺性的条件。体现在Lock接口上,就是Lock接口提供的三个方法,如下所示。//支持中断的APIvoidlockInterruptibly()throwsInterruptedException;//支持超时的APIbooleantryLock(longtime,TimeUnitunit)throwsInterruptedException;//支持非阻塞获取锁的APIbooleantryLock();lockInterruptibly()支持中断。tryLock()的ktryLock()方法是有返回值的,也就是说是用来尝试获取锁的。如果获取成功,则返回true。如果获取失败(即锁已经被其他线程获取),则返回false,也就是说这个方法无论如何都会立即返回。它不会在获取不到锁的时候一直在那里等待。tryLock(longtime,TimeUnitunit)方法tryLock(longtime,TimeUnitunit)方法与tryLock()方法类似,不同的是该方法在获取不到锁时会等待一定时间,在限定时间内如果还没有锁,返回false。如果锁是最初获取的或在等待期间获取的,则返回true。也就是说,对于死锁问题,Lock可以破坏不可剥夺的条件。例如,我们下面的程序代码破坏了死锁的不可解除条件。publicclassTansferAccount{privateLockthisLock=newReentrantLock();privateLocktargetLock=newReentrantLock();//账户余额privateIntegerbalance;//转账操作publicvoidtransfer(TansferAccounttarget,IntegertransferMoney){booleanisThisLock=thisLock.tryLock();if(isThisLockisLockant){arget.tryLock();if(isTargetLock){try{if(this.balance>=transferMoney){this.balance-=transferMoney;target.balance+=transferMoney;}}finally{targetLock.unlock}}}finally{thisLock.unlock();}}}}Exception,Lock下面有一个ReentrantLock,ReentrantLock支持公平锁和非公平锁。在使用ReentrantLock时,ReentrantLock中有两个构造函数,一个是无参构造函数,一个是传入公平参数的构造函数。fair参数表示锁的公平策略。如果传入true,则表示需要构造公平锁,否则表示需要构造非公平锁。如下面的代码片段所示。//无参数构造函数:默认非公平锁publicReentrantLock(){sync=newNonfairSync();}//根据公平策略参数创建锁publicReentrantLock(booleanfair){sync=fair?newFairSync():newNonfairSync();}lock的实现本质上对应一个入口等待队列。如果一个线程没有获得锁,它就会进入等待队列。当一个线程释放锁时,需要从等待队列中唤醒一个正在等待的线程。如果是公平锁,唤醒策略是等待时间长的谁就唤醒谁,非常公平;如果是非公平锁,则不提供这种公平性保证,等待时间短的线程可能先被唤醒。Lock支持公平锁,synchronized不支持公平锁。最后,值得注意的是,在使用Lock加锁时,一定要在finally{}代码块中释放锁,例如如下代码片段所示。try{lock.lock();}finally{lock.unlock();}注:其他synchronized和Lock的详细说明可以自行查看。本文转载自微信公众号“银禾科技”,可通过以下二维码关注。转载本文请联系冰川科技公众号。
