Java并发中lock的使用
简介
在java并发编程中,我们常常要考虑多个线程之间的同步问题,而这时候,synchronized关键字是一个不错的选择,使用synchronized可以实现对临界资源的同步互斥访问,但是synchronized粒度比较大,在使用它时,可能会造成一些性能上的问题,如响应中断等局限性问题。
相比之下,java中的Lock提供了比synchronized更为通用的、更为广泛的锁操作,它能更好的处理线程的同步问题。
synchronized的缺点
synchronized是java中的关键字,是java的内置特性之一,基于JVM层面实现。
当一个代码块被synchronized关键字修饰,然后当一个线程首先获取到了对应的锁之后,并执行该代码块时,其他线程只能一直等待知道占有线程的资源释放锁为止。也就是说,当占有锁的线程执行完这个代码块时,线程会释放对锁的占用;或者占有锁的线程发生异常,然后JVM自动释放对锁的占用。
仔细分析一下这个过程,会存在这样的问题:
当占用锁的线程由于IO或者其他的原因(sleep方法),使得该线程在某些阻塞了,但是此时线程又不会释放锁,所以其他需要得到锁的线程就会一直等待,从而影响了整个程序的运行效率,甚至会出现死锁的情况。(A线程在A1同步代码块中试图去执行B1同步代码块,而B线程在B1同步代码块中试图去执行A1同步代码块,此时,A线程和B线程就会互相等待,从而出现死锁。)
同时,使用synchronized关键字,在阻塞等待过程中的线程,是无法被中断的。
举个实际点的例子,当多个线程进行读写操作时,使用synchronized同步嘛,读和写会发生冲突,写和写也会发生冲突,这时候使用synchronized同步加锁是没有问题的,但是读跟读呢,好像两者并不相互影响。使用synchronized同步的话,就会导致性能问题了。
Lock的优点
相比于synchronized关键字,当我们使用Lock来解决这些同步问题时,可以实现更广泛的锁操作,实现更灵活的锁结构。
比如说会有以下优点:
- 对于多个线程进行读写操作时,Lock提供了一个特殊的锁,ReentrantReadWriteLock锁,来使得多个线程可以同时进行读操作。
- 当一个线程要获得一个同步锁时,可以即时(或者延时)返回取得锁的结果,成功或者不成功,而不是想synchronized一样,在那里一直阻塞等待(使用tryLock()或者tryLock(long time, TimeUnit unit))。
- 在等待获得锁的时候,我们可以直接中断这个等待,也就是说,能够响应中断(lockInterruptibly())。
但是相比synchronized关键字,使用Lock的同步锁,并不会自动释放该锁,该锁必须手动释放。当没有主动释放锁时,就有可能会出现死锁的情况。一般建议将加锁放在try里面,然后在finally中释放锁。
Lock的注意事项
三种形式的锁获取(可中断、不可中断和定时)在其性能特征、排序保证或其他实现质量上可能会有所不同。而且,对于给定的 Lock
类,可能没有中断正在进行的 锁获取的能力。因此,并不要求实现为所有三种形式的锁获取定义相同的保证或语义,也不要求其支持中断正在进行的锁获取。实现必需清楚地对每个锁定方法所提供的语义和保证进行记录。还必须遵守此接口中定义的中断语义,以便为锁获取中断提供支持:完全支持中断,或仅在进入方法时支持中断。
由于中断通常意味着取消,而通常又很少进行中断检查,因此,相对于普通方法返回而言,实现可能更喜欢响应某个中断。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。
Lock接口
在java.util.concurrent.locks包中,Lock接口是一个最基本的接口之一。
方法
- void lock() //获取锁
- void lockInterruptibly() //如果当前线程没有被中断,则获取锁
- Condition newCondition() //返回绑定到此Lock实例的新Condition实例
- boolean tryLock() //仅在调用时锁空闲状态时才获取锁,返回获取结果
- boolean tryLock(long time, TimeUnit unit) //如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁,返回获取结果
- void unlock() //释放锁
newCondition()方法一般是用在线程协作的情况下的,在这里暂时不说。
lock()
这个方法会比较常用,实现起来的效果跟synchronized关键字一样。使用lock()方法尝试去获取锁,如果获取不到锁,则会一直等待,等待过程中,不可响应中断。
1 | Lock lock = ...; |
这里之所以使用try-catch-finally结构,是为了保证无论在什么情况下,这个锁都可以被释放。
tryLock()与tryLock(long time, TimeUnit unit)
这两个方法都是有返回值的,tryLock()方法会即时的返回获取锁的结果,如果在调用的时候成功获取到了锁,则返回true,否则返回false,相比lock()方法,它不会在那里一直等待,直到获取成功。tryLock(long time, TimeUnit unit)方法类似于tryLock()方法,但是它允许等待一段时间,在这段时间内,如果得到锁,返回true,否则在这段时间后,返回false。time参数指定时间,而unit参数指定时间单位。
值得注意的是,tryLock(long time, TimeUnit unit)支持响应中断,而tryLock()不支持,关于响应中断,见下面的lockInterruptibly()方法。
1 | Lock lock = ...; |
lockInterruptibly()
同lock()方法一样,会一直等待获取锁,但是,它是可以响应中断的,也就是说,在它等待获取锁的过程中,可以由于调用Thread.interrupt()方法来中断这个阻塞的等待过程,从而抛出一个中断异常。
在一个线程正常执行时,是不会被interrupt()方法中断的,只有在阻塞等待过程中的线程才会被interrupt()方法中断。
1 | function() throws InterruptedException{ |
ReentrantLock
ReentrantLock,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的类,并且在ReenTrantLock中提供了更多的方法,以实现更加广泛的锁操作。
构造方法
ReentrantLock() //创建一个ReentrantLock的实例
ReentrantLock(boolean fair) //创建一个具有给定公平策略的ReentrantLock
这里说一下第二个构造方法,公平参数的作用。当设置为true时,在多个线程的争用下,这些锁将倾向于将访问全授予等待时间最长的线程。否则此锁将无法保证任何特定的访问顺序。与采用默认设置(不公平)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。
常用方法
- boolean isFair() //该锁是否是公平的
- boolean isLocked() //该锁是否被获取了
- boolean isHeldByCurrentThread() //当前线程是否获得该锁
- int getQueuedLength() //返回正等待获取此锁的线程估计数
- boolean hasQueuedThread(Thread thread) //指定线程是否在等该获取该锁
- boolean hasQueuedThreads() //是否有线程在等待获取该锁
例子
- 关于公平参数的
1 | import java.util.concurrent.locks.*; |
结果
1 | root@dcLunatic:~# java ReentrantLockTest1 |
两次执行结果,两次b都会比c先被调度执行函数waitTime,但是在第一次的时候,是b先获得锁,在第二次的时候却是c先获得锁了,按照先后执行顺序而言,这里的b在等待获取锁的时间应该会比c要长一点点的。但他们在等待获取锁的机会是相同的,所以谁先谁后都是有可能的。
如果这里要使得b如果比c执行了waitTime函数,然后b必先在c之前得到锁的话,就可以考虑加上公平参数了。(当然,上面你也可以加Thread.sleep(10)来保证b在c之前执行)