作者|王磊来源|Java中文社区(ID:javacn666)授权请联系(微信ID:GG_Stone)在JDK1.5之前,synchronized的性能比较低,但是在JDK1.5中,官方推出了一个重量级函数Lock,改变了锁入的格局Java一举成名。在JDK1.5之前,我们讲锁的时候只能使用内置的锁synchronized,而现在我们的锁实现多了一个显式锁Lock。在本文中,我们重点介绍Lock。Lock简介Lock是一个顶层接口,它的所有方法如下图所示:它的子类列表如下:我们通常使用ReentrantLock来定义它的实例,它们之间的关系如下图所示:”PS:Sync是同步锁的意思,FairSync是公平锁,NonfairSync是非公平锁。使用ReentrantLock学习任何技能都是从使用开始的,我们也不例外。先看ReentrantLock的基本使用:publicclassLockExample{//创建锁对象privatefinalReentrantLocklock=newReentrantLock();publicvoidmethod(){//锁操作lock.lock();try{//业务代码……}finally{//释放锁lock.unlock();}}}ReentrantLock创建后有两个关键操作:加锁操作:lock()释放锁操作:unlock()ReentrantLock中的陷阱1.ReentrantLock是非公平锁默认。很多人(尤其是新手朋友)认为ReentrantLock默认实现的是公平锁。事实上,情况并非如此。ReentrantLock默认是非公平锁(这个主要是出于性能考虑),比如下面的代码:importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建一个锁对象String[]args){//定义线程任务Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){//加锁lock.lock();try{//打印执行线程的名称System.out.println("Thread:"+Thread.currentThread().getName());}finally{//释放锁lock.unlock();}}};//创建多个线程for(inti=0;i<10;i++){newThread(runnable).start();}}}上面程序的执行结果如下:从上面的执行结果可以看出ReentrantLock默认是非公平锁。因为线程的名字是按照创建顺序递增的,如果是公平锁,那么线程的执行应该是有序递增的,但是从上面的结果可以看出执行和线程的打印是乱序的,这意味着请注意ReentrantLock默认情况下是一个非公平锁。设置ReentrantLock为公平锁也很简单。创建ReentrantLock时只需要设置一个true的构造参数,如下代码所示:importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象(公平锁)privatestaticfinalReentrantLocklock=newReentrantLock(true);publicstaticvoidmain(String[]args){//定义线程任务Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){//locklock.lock();try{//打印执行线程的名字System.out.println("Thread:"+Thread.currentThread().getName());}finally{//释放锁lock.unlock();}}};//创建多个线程for(inti=0;i<10;i++){newThread(runnable).start();}}}上面程序的执行结果如下:从上面的结果可以看出,当我们显式设置ReentrantLock为true时,构造参数后,ReentrantLock就变成了一个公平锁,线程获取锁的顺序变得有序。其实从ReentrantLock的源码我们也可以看出是公平锁还是非公平锁。ReentrantLock的部分源码实现如下:publicReentrantLock(){sync=newNonfairSync();}publicReentrantLock(booleanfair){sync=fair?newFairSync():newNonfairSync();}从上面的源码可以看出,ReentrantLock默认会创建一个非公平锁。如果构造参数的值在创建时显式设置为true,则会创建一个公平锁。2.在finally中释放锁在使用ReentrantLock的时候,一定要记得释放锁,否则锁会一直被占用,其他使用锁的线程会一直等待下去,所以我们在使用ReentrantLock的时候,一定要在finally释放锁,这样就可以保证锁一定会被释放。反例importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){//锁操作lock.lock();System.out.println("Hello,ReentrantLock.");//异常这里会报错,导致不能正常释放锁intnumber=1/0;//释放锁lock.unlock();System.out.println("释放锁成功!");}}上面程序的执行结果如下:从上面的结果可以看出,发生异常时锁并没有正常释放,会导致其他使用锁的线程永久处于等待状态。正例importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){//锁操作lock.lock();try{System.out.println("Hello,ReentrantLock.");//这里会报异常intnumber=1/0;}finally{//释放锁lock.unlock();System.out.println("释放锁成功!”);}}}上面程序的执行结果如下:从上面的结果可以看出,虽然方法中出现了异常情况,但是并不影响ReentrantLock锁的释放操作,使得其他线程使用这个锁可以正常获取和运行。3.锁无法释放。锁操作次数和解锁操作次数必须一一对应,不能多次释放一个锁,因为这样会导致程序报错。反例一个锁对应两次解锁操作,导致程序报错终止执行。示例代码如下:importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){//锁操作lock.lock();//第一次释放锁try{System.out.println("执行业务1~");//业务代码1...}finally{//释放锁lock.unlock();System.out.println("Lockreleaselock");}//第二次释放锁try{System.out.println("Executebusiness2~");//业务代码2......}finally{//释放锁lock.unlock();System.out.println("lockreleaselock");}//最后的打印操作System.out.println("programexecutionFinished.");}}上面程序的执行结果如下:从上面的结果,可以看出,在执行第二次unlock时,程序报错终止执行,导致异常后的代码无法正常执行。4.不要把锁放在try代码中。在使用ReentrantLock时,需要注意不要将加锁操作放在try代码中。这样会导致锁没有执行成功就释放了锁,导致程序执行异常。反例importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){try{//Exceptionhereintnum=1/0;//锁操作lock.lock();}finally{//释放锁lock.unlock();System.out.println("Lockreleaselock");}System.out.println("Programexecutioncompleted.");}}上面程序的执行结果如下:来自以上结果,可以看出,如果把加锁操作放在try代码中,可能会导致两个问题:释放锁的操作在加锁不成功的情况下执行,导致新的异常;释放锁的异常会覆盖掉程序原来的异常,从而增加了排错的难度。小结本文介绍Java中显式锁Lock及其子类ReentrantLock的使用及注意事项。Lock在Java中占据了一半的锁,但是在使用的时候有4个问题需要注意:默认情况下,ReentrantLock是非公平锁,不是公平锁;加锁和释放锁的次数必须一致,否则会造成线程阻塞或程序异常;加锁操作一定要放在try代码之前,避免解锁成功释放锁异常;释放锁一定要放在finally里,否则线程会阻塞。
