文章目录
Lock锁
Lock是自JDK1.5起在Java并发包(JUC)中提供的锁。与synchronized相比,Lock提供了更加灵活的、可扩展的锁相关操作。支持多个与锁相关的条件。
锁本质上是用于控制多个线程对共享资源访问的工具。 通常,锁提供对共享资源的独占访问,一次只有一个线程可以获取锁,所有对共享资源的访问都需要先获取锁。 但是,某场景下可能允许并发访问共享资源,例如对某个共享资源的读操作,这就要求锁具备这个功能。Lock接口的一个具体实现:ReadWriteLock就可以实现多个线程同时读。这种特性是synchronized不具备的。
synchronized方法或代码块隐式提供了对每个对象关联的监视器锁(monitor)的访问,所有锁的获取和释放必须在一个代码块结构中。即当获取多个锁时,它们必须以相反的顺序释放,并且所有锁必须在它们被获取的同一个代码范围内释放。
虽然synchronized方法和代码块的作用域机制使得我们实现并发编程更加容易,避免了许多涉及锁的常见编程错误。但在某些情况下,我们需要以更灵活的方式使用锁。 比如我们需要先获取A锁,然后获取B锁,接着释放A锁获取C锁,然后释放B锁获取D锁。像这种复杂的场景,synchronized无能为力。而Lock接口的实现允许在不同范围内获取和释放锁,并允许以任何顺序获取和释放多个锁。
这种灵活性也要求我们必须控制好锁的获取和释放。不像synchronized那样,锁的获取和释放是由底层自动实现的,编程中只要使用关键字就行了。
因此在通常情况下,我们应该是遵循下列使用Lock的习惯:
Lock lock=...;
lock.lock();try{// access the resource protected by this lock}finally{
lock.unlock();}
当锁定和解锁发生在不同的作用域时,必须确保持有锁时执行的所有代码都受到 try-finally 或 try-catch 的保护,以保证在必要时释放锁。
Lock的实现提供了多种形式的锁。
1)非阻塞方式获取锁:tryLock();
2)可中断的方式获取锁:lockInterruptibly();
3)获取锁时还可以加入超时时间:tryLock(long, TimeUnit);
Lock实例只是普通对象,它们本身也可以用作synchronized语句中的目标对象。 获取Lock实例的监视器锁(monitor)与调用该实例的任何lock方法没有特定的关系。 建议不要在synchronized方法或者代码块中使用Lock实例作为锁定的对象,以避免混淆。
Lock锁的方法
publicinterfaceLock{voidlock();voidlockInterruptibly()throwsInterruptedException;booleantryLock();booleantryLock(long time,TimeUnit unit)throwsInterruptedException;voidunlock();ConditionnewCondition();}
lock
以阻塞的方式获取锁,如果获取锁失败,则该线程进入休眠状态,线程调度被暂停,直到获得锁。
lockInterruptibly
与lock方法相比,该方法可以响应中断。
如果获取锁成功则立即返回。否则线程进入休眠状态,暂停被线程调度,直到下列情况发生:
1)获取到锁
2)其它线程中断了该线程,并且支持锁获取中断
如果当前线程在获取锁时被中断(并且支持中断获取锁)则该方法会抛出一个InterruptedException。
tryLock
以非阻塞方式获取锁,如果锁可用则返回true;如果不可用则立即返回false,而不是阻塞线程。
一贯用法如下:
Lock lock=...;if(lock.tryLock()){try{// 业务逻辑A}finally{
lock.unlock();}}else{// 业务逻辑B}
tryLock(long time, TimeUnit unit)
和tryLock方法相比,多了支持设置等待时间和中断响应。
如果锁可用,则此方法立即返回true。 如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到以下三种情况之一发生:
1)锁被当前线程获取到
2)其他一些线程中断当前线程,并且支持中断获取锁
3)经过了指定的等待时间
如果当前线程在获取锁时被中断(支持中断获取锁的前提下),则该方法会抛出一个InterruptedException。
unlock
有加锁,必然有释放锁。unlock方法就是用于释放锁。
newCondition
Lock锁提供了一个与之相关的条件对象(Condition)。该方法就是返回绑定到此Lock实例的新Condition实例。
在等待Condition条件之前,当前线程必须先持有与Condition相关联的锁。 调用Condition.await()方法将在等待之前自动释放锁,并在等待返回之前重新获取锁。
后面会介绍具体的案例。
Lock锁的优势
当一个线程调用了synchronized方法之后,如果没有成功获取到锁,就立即陷入了等待,没有第二种选择。而Lock锁可以选择不同的方式,可以立即陷入等待,也可以立即返回,也可以等待一段时间。具体看你调用哪个API方法。
Lock锁的使用案例
Lock是一个接口,其有多种实现类,常用的有ReentrantLock、ReentrantReadWriteLock等,我们一一举例。
ReentrantLock
ReentrantLock是Lock接口最常用的实现类,它一个可重入的锁,可以认为和synchronized具有相同的语义,但更具扩展性。
ReentrantLock锁会被最近一次成功获取到锁的但还未释放锁的线程拥有。ReentrantLock的lock方法会在成功获取到锁时返回,如果检测到当前线程已经拥有了该锁,则该方法立即返回。
ReentrantLock的构造函数接受一个可选的公平参数。当设置为为true,即公平锁时,锁倾向于授予等待时间最长的线程。反之,不能保证任何特定的锁的获取顺序,即非公平。
注意,在高并发场景下,多个线程访问的公平锁的程序可能会显示出较低的总体吞吐量(即更慢,通常慢得多)。并且在公平性方面(保证不出现饿死的情况)和非公平锁的差异也很小。
另外,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的众多线程之一可能会连续多次获得它,而其他活动线程一直没能够持有该锁。特别要强调的是,使用没有设置等待时间的tryLock()方法是不遵守公平性设置的。
ReentrantLock典型的使用模式如下:
classX{privatefinalReentrantLock lock=newReentrantLock();// ...publicvoidmethod(){
lock.lock();// block until condition holdstry{// ... method body}finally{
lock.unlock()}}}
举例1:快速入门
下面的例子中,方法method1中会对成员变量count自增,而自增操作不是一个原子操作,有读数据、加法操作、写数据多个步骤。因此需要加锁保证多线程环境下的并发安全。
加锁时,能保证输出的结果是正确的累计值;而不加锁时,输出的最终结果可能小于预期。可以对比下加锁和不加锁的运行结果。
publicclassTestLock{privateReentrantLock lock=newReentrantLock();privateint count=0;publicvoidmethod1(){//lock.lock();//注释这行代码取消加锁try{Thread.sleep(1000);System.out.println(++count);}catch(InterruptedException e){
e.printStackTrace();}finally{//lock.unlock();}}publicstaticvoidmain(String[] args){TestLock testLock=newTestLock();//创建多个线程对共享变量同时进行操作for(int i=0; i<20000; i++){newThread(newRunnable(){@Overridepublicvoidrun(){
testLock.method1();}}).start();}}}
举例2:基本使用
publicclassTest1{privateLock lock=newReentrantLock();publicvoidmethod1(){try{
lock.lock();System.out.println("method1 business");}finally{
lock.unlock();}}publicvoidmethod2(){try{
lock.lock();System.out.println("method2 business");}finally{
lock.unlock();}}publicstaticvoidmain(String[] args){Test1 test=newTest1();Thread thread1=newThread(()->{for(int i=0; i<10; i++){
test.method1();try{Thread.sleep(200);}catch(InterruptedException e){
e.printStackTrace();}}});Thread thread2=newThread(()->{for(int i=0; i<10; i++){
test.method2();try{Thread.sleep(500);}catch(InterruptedException e){
e.printStackTrace();}}});
thread1.start();
thread2.start();}}
上述案例展示了多个方法共享一把锁的场景,运行程序,我们可以看到程序正常输出20次,两个线程交替执行完成。
如果我们忘记了释放锁,此时会发生什么情况呢?比如注释掉第一个方法的释放锁操作。
publicvoidmethod1(){try{
lock.lock();System.out.println("method1 business");}finally{//lock.unlock();}}
此时只输出了10次method1 business
,并且程序没有终止。因为method1 先被调用,并且一直持有锁没有释放,导致method2获取锁失败,一直等待 。然而method1 可以不断重新获取锁(可重入的),因此第一个线程循环调用method1方法后输出。
上述使用Lock的方式和synchronized类似,都是一直等待,直到获得锁。
在实际开发中要遵循try{...}finally{...}
这种使用Lock锁的范式,防止锁得不到释放。
上述代码method2方法改进为使用tryLock
,则可以避免程序阻塞,虽然获取锁失败,但是程序最终能执行完成,示例如下。
publicvoidmethod2(){// try {// lock.lock();// System.out.println("method2 business");// } finally {// lock.unlock();// }boolean success=false;try{
success= lock.tryLock(1000,TimeUnit.MILLISECONDS);}catch(InterruptedException e){
e.printStackTrace();}if(success){System.out.println("lock success");}else{System.out.println("lock fail");}}
举例3:可重入性
下列例子中,虽然get方法中也要获取锁,但是并不会导致死锁,因为ReentrantLock 支持重入。
publicclassTest02extendsThread{privateReentrantLock lock=newReentrantLock();publicvoidget(){
lock.lock();try{System.out.println("get方法"+Thread.currentThread().getName());}finally{
lock.unlock();}}publicvoidset(){
lock.lock();try{System.out.println("set方法"+Thread.currentThread().getName());get();//虽然get方法中也要获取锁,但是不会死锁,因为ReentrantReadWriteLock 支持重入}catch(Exception e){
e.printStackTrace();}finally{
lock.unlock();}}@Overridepublicvoidrun(){set();}publicstaticvoidmain(String[] args){for(int i=0; i<4; i++){Test02 ss=newTest02();
ss.start();}}}//输出
set方法Thread-0
get方法Thread-0
set方法Thread-1
set方法Thread-2
get方法Thread-1
get方法Thread-2
set方法Thread-3
get方法Thread-3
公平锁、非公平锁
如果在绝对时间上,先对锁进行获取的请求一定会先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。
ReentrantLock类提供了一个构造函数,通过传入fair参数控制锁是否是公平的。
1)公平锁
公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁每次都是依次从队首取值。
2)非公平锁
在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接争抢到锁的。
公平锁是先到先得,按序进行。非公平锁就是不排队直接拿,失败再说。公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但由于先尝试争抢,失败后才阻塞,因此最大限度减少线程切换,具有更大的吞吐量。
ReentrantReadWriteLock
前面提到锁(如ReentrantLock、synchronized)都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问。在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,那么应该允许多个线程能同时读取共享资源。但是如果有一个线程去写这些共享资源,就不应该再有其它线程对该资源进行读或写(也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。
JDK5引入的ReentrantReadWriteLock 就可以来实现读写锁。
案例1:快速入门
分别使用读锁和写锁,从运行效果可以发现读锁是可以共享的,而写锁是独占的;
因此读锁可以近乎同时完成操作,而写锁是排他的,需要等待前一个线程操作完成并释放掉锁,下一个线程才能进行操作。
publicclassReadWriteLockTest{privateReadWriteLock readWriteLock=newReentrantReadWriteLock();publicvoidmethod1(){//readWriteLock.readLock().lock();
readWriteLock.writeLock().lock();try{Thread.sleep(1000);System.out.println("method1 business");}catch(Exception e){
e.printStackTrace();}finally{//readWriteLock.readLock().unlock();
readWriteLock.writeLock().unlock();}}publicstaticvoidmain(String[] args){ReadWriteLockTest readWriteLockTest=newReadWriteLockTest();IntStream.range(0,10).forEach(i->newThread(readWriteLockTest::method1).start());}}
案例2:自定义缓存
我们以一个自定义缓存的例子来演示读写锁的使用。
publicclassCache{staticMap<String,Object> map=newHashMap<String,Object>();staticReentrantReadWriteLock rwl=newReentrantReadWriteLock();staticLock r= rwl.readLock();staticLock w= rwl.writeLock();// 获取一个key对应的valuepublicstaticfinalObjectget(String key){
r.lock();try{//为了演示出效果,get操作再细分如下,开始—>结束视为原子步骤System.out.println("正在做读的操作,key:"+ key+" 开始");Thread.sleep(100);Object object= map.get(key);System.out.println("正在做读的操作,key:"+ key+",value:"+ object+" 结束");System.out.println();return object;}catch(InterruptedException e){
e.printStackTrace();}finally{
r.unlock();}return key;}// 设置key对应的value,并返回旧有的valuepublicstaticfinalObjectput(String key,Object value){
w.lock();try{//为了演示效果,set操作再细分如下,开始—>结束整个过程视为原子步骤System.out.println("正在做写的操作,key:"+ key+",value:"+ value+"开始.");Thread.sleep(100);Object object= map.put(key, value);System.out.println("正在做写的操作,key:"+ key+",value:"+ value+"结束.");System.out.println();return object;}catch(InterruptedException e){
e.printStackTrace();}finally{
w.unlock();}return value;}// 清空所有的内容publicstaticfinalvoidclear(){
w.lock();try{
map.clear();}finally{
w.unlock();}}publicstaticvoidmain(String[] args){newThread(newRunnable(){@Overridepublicvoidrun(){for(int i=0; i<10; i++){Cache.put(i+"", i+"");}}}).start();newThread(newRunnable(){@Overridepublicvoid