多线程是复杂的,为了使开发者写出简单、安全的并发程序,JDK提供了大量的API和框架。在上一节,大家已经认识了线程间同步的一个重要手段sychronized,现在,我们来学习一下JDK1.5新增的线程同步工具类Lock。Lock不仅具有sychronized的所有特性,在它的基础上,增加了许多其它的功能,相较于sychronized,Lock更加灵活。当然,我们不是说sychronized完全没有用了,只是Lock的功能更加强大。
在线程同步中,sychronized关键字是一种最简单的方式,要想我们的程序实现更多更好的功能,Lock是必不可少的。写一个简单的例子,进入到Lock的世界中:
从代码中看,大家应该发现了三处与业务逻辑不相关的代码,分别是创建lock对象、lock.lock()、lock.unlock()。这三句代码就相当于线程同步控制,也就是所谓的锁操作,其中lock.lock()是加锁,lock.unlock()是解锁。大家或许就会问了,使用Lock线程同步怎么这么麻烦呢?sychronized一个关键字就能做到自动加锁,解锁,lock还需要我们手动操作,强大在哪里呢?朋友们,别急,听我仔细分析一下。使用Lock进行锁操作,我们能够明显的看出哪里需要加锁,哪里需要解锁,一目了然,使用起来非常灵活。另外,这只是一个简单的例子,丰富多彩的情节总是在后面的。
在上面的代码中,我们使用了try-catch方式,这在实际项目中基本上是一种固定的模式,这是为了防止业务逻辑出现异常,导致资源未释放的问题。
Lock锁也是重入锁,表明一个线程能够连续多次获取锁,但是,这里需要特别注意,多次获取,也就相应的需要进行同等数量释放。如果我们在代码中,多释放锁,会得到一个java.lang.IllegalMonitorStateException,相反,少释放锁,那么当前线程依然持有锁,这是很危险的。大家可以按照这两种情况分别写一段代码,看看结果如何。
锁的获取方式:
lock()、lockInterruptibly()、tryLock()或者tryLock(long timeout,TimeUnit unit)。
1.lock(),锁未被使用,直接获取,否则,永久等待。
2.lockInterruptibly(),当前线程未被中断,则获取锁定,如果已被中断则出现异常。处理死锁,很有帮助。
3.tryLock(),限时等待锁。方法中不加时间,如果线程没有获得锁,则立即返回false,否则,返回true。方法中加时间,则表示限时等待,超过时间未获得锁,则线程自动放弃。避免死锁,很有帮助。
测试lockInterruptibly()方法:
这段代码,很明显,会发生死锁,当线程A获取锁1之后,进行睡眠,线程B获取锁2,之后两个线程互相请求对方持有的锁。
结果:
从结果看出,两个线程都退出了。使用lockInterruptibly()请求锁,当我们使用interrupte()中断时,线程A放弃了对锁2的请求并释放锁1,然后退出,之后线程B获取到锁1,正常退出。虽然两个线程都退出了,但是真正完成工作的只有线程B。lockInterruptibly()进行锁中断,很轻易就解决了死锁。
测试tryLock()方法:
结果:
这这里,我们使用tryLock()方式获取锁,结果输出中,两个时间是一致的,这就表明,使用tryLock()会立即去请求锁,当锁被其它线程所持有的时候,会立马返回false。tryLock(long timeout,TimeUnit unit)也是类似,相对来说,只是在获取锁加了等待时间,时间到达还未获取锁,也会立马返回false。利用tryLock()避免死锁是一种好的方式。
公平锁:
大多数情况下,我们都是使用的非公平锁。通俗来说,锁的获取是随机的,即使线程A先线程B请求锁,线程A也不一定先获取锁,不会按照时间先后顺序,获取资源随机性可能导致饥饿现象,sychronized获取锁就是非公平的。如果我们想要获取公平锁,应该怎样实现呢?下面我们讲一下ReentrantLock的另一构造函数:
通过源码注释,我们发现,当参数fair为true时,锁时公平的,相反,则是非公平的。公平锁可以帮助我们解决饥饿问题,但是公平锁需要维护一个有序队列,实现成本大,性能相对较低,因此,默认情况下,锁都是非公平的。当我们的业务没有特别需求的情况下,我们尽量使用非公平锁。下面展现了公平锁的特点:
上述代码,我们使用new ReentrantLock(true)创建对象,指定锁是公平的。run()方法中,我们循环获取锁,看看输出:
截取一部分输出,大家可以发现,每次获取锁的顺序都是一致的,不会发生一个线程连续把持锁的情况,从而保证了锁的公平性。当我们把fair改为false,输出结果:
可以看到,一个线程会倾向于在此获取已经持有的锁,这种分配方式比较高效,但是无公平性,很容易出现饥饿现象。
其它方法:
int getHoldCount():获取当前线程持有该锁的次数,也就是调用lock()方法的次数。
int getQueueLength():获取正等待请求锁的线程个数。
boolean isFair():判断当前锁是否是公平锁。
boolean isHeldByCurrentThread():判断当前线程是否持有该锁。
boolean isLocked():获取该锁是否已经被任意线程所持有。
这一节,我们大概了解Lock的实现类ReentrantLock的常用方法,它的逻辑控制灵活,在多线程程序中,我们完全可以使用ReentrantLock来代替sychronized。下一节我们将继续围绕ReentrantLock重入锁,学习Condition条件和Semaphore信号量。