最近在做MVCC的project,其中使用到了ReadWriteLock锁机制,特此写篇博客来记录一下
一、synchronized
它可以锁住一个方法或者一段代码块,伪代码如下:
//锁住方法
public synchronized void test(){
doSomething...
}
//锁住代码块
public synchronized void test(){
doSomething...
synchronized( any object ){
锁住的代码块
}
}
有多个线程threadA,threadB....threadN等,他们同时去执行一段被synchronized修饰的代码块,如果threadB抢到了同步锁,那么其他的线程必须等待threadB的所有操作完成后自动释放锁,threadB只有两种情况下会释放锁:
1、threadB执行完了这段代码段,这个锁就会被释放
2、当threadB执行这段代码块抛出异常的时候,JVM虚拟机也会释放这个锁
如果threadB执行时间特别长,那么其他的线程就会一直等待,造成资源的浪费,效率较低
二、Lock
它规避了synchronized的缺点,是一个接口,里面有如下四个锁方法和一个解锁方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
}
Lock有一个实现类:ReentrantLock,它实现了Lock里面的方法,但是使用Lock的时候必须注意:它不会像synchronized执行完成之后或者抛出异常之后自动释放锁,而是需要主动释放unlock(),所以必须在使用Lock的时候加上try{}catch{}finally{}块,并且在finally中释放占用的资源。
Lock和synchronized最大的区别就是当使用synchronized,一个线程抢占到锁资源,其他线程必须一直等待,而使用Lock,一个线程抢占到锁资源,其他线程可以不等待或者设置等待时间,实在抢不到可以去做其他的业务逻辑
1)lock()这个方法用来获取锁,如果已经被其他线程抢占则等待,最后需要自己释放资源,伪代码如下:
Lock lock = new ReentrantLock();
//线程1
new Thread(){
run(){
dosomething...
lock.lock();
try{
dosomething...
}catch(Exception e){
catch exception...
}finally{
lock.unlock();
}
}
}.start();
//线程2
new Thread(){
run(){
dosomething...
lock.lock();
try{
dosomething...
}catch(Exception e){
catch exception...
}finally{
lock.unlock();
}
}
}.start();
Lock lock = new ReentrantLock();
//线程1
new Thread(){
run(){
dosomething...
boolean flag = lock.tryLock();
if(flag ){
try{
dosomething...
}catch(Exception e){
catch exception...
}finally{
lock.unlock();
}
}
}
}.start();
//线程2
new Thread(){
run(){
dosomething...
boolean flag = lock.tryLock();
if(flag ){
try{
dosomething...
}catch(Exception e){
catch exception...
}finally{
lock.unlock();
}
}
}
}.start();
3)tryLock(long time, TimeUnit unit) 这个方法和上面的很相似,不过这个会等待一段时间,如果在等待期间获取锁,即当前线程顺利执行资源,反之,不等待,伪代码如下,5秒之内抢不到就中断:
Lock lock = new ReentrantLock();
//线程1
new Thread(){
run(){
dosomething...
boolean flag = lock.tryLock(5, TimeUnit.SECONDS);
if(flag ){
try{
dosomething...
}catch(Exception e){
catch exception...
}finally{
lock.unlock();
}
}
}
}.start();
//线程2
new Thread(){
run(){
dosomething...
boolean flag = lock.tryLock(5, TimeUnit.SECONDS);
if(flag ){
try{
dosomething...
}catch(Exception e){
catch exception...
}finally{
lock.unlock();
}
}
}
}.start();
Lock lock = new ReentrantLock();
ThreadA.start();
ThreadB.start();
dosomething....
ThreadB.interrupt();
run(){
dosomething...
boolean flag = lock.lockInterruptibly();
try{
dosomething...
}catch(Exception e){
catch exception...
}finally{
lock.unlock();
}
}
public interface ReadWriteLock {
Lock readLock();//返回读锁
Lock writeLock();//返回写锁
}
伪代码如下:
private ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
ThreadA.do{
read();
write();
}
ThreadB.do{
read();
write();
}
//读操作
read(){
rrwl.readLock().lock();
try{
read something...
}catch(exception e){
catch exception...
}finally{
rrwl.readLock().unlock();
}
}
//写操作
write(){
rrwl.writeLock().lock();
try{
write something...
}catch(exception e){
catch exception...
}finally{
rrwl.writeLock().unlock();
}
}
四、ReentrantReadWriteLock分析,可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock
ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.readLock().lock();
System.out.println("get readLock.");
rtLock.writeLock().lock();
System.out.println("blocking");
ReentrantReadWriteLock支持锁降级,不会产生死锁:
ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
System.out.println("writeLock");
rtLock.readLock().lock();
System.out.println("get read lock");
以上代码虽然不会导致死锁,但没有正确的释放锁,从写锁降级为读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放unlock(),否则别的线程永远也获取不到写锁。
例子:
class CachedData {
2 Object data;
3 volatile boolean cacheValid;
4 final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
5
6 public void processCachedData() {
7 rwl.readLock().lock();
8 if (!cacheValid) {
9 // Must release read lock before acquiring write lock
10 rwl.readLock().unlock();
11 rwl.writeLock().lock();
12 try {
13 // Recheck state because another thread might have,acquired write lock and changed state before we did.
14 if (!cacheValid) {
15 data = ...
16 cacheValid = true;
17 }
18 // 在释放写锁之前通过获取读锁降级写锁(注意此时还没有释放写锁)
19 rwl.readLock().lock();
20 } finally {
21 rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁
22 }
23 }
24
25 try {
26 use(data);
27 } finally {
28 rwl.readLock().unlock();
29 }
30 }
31 }
以上代码加锁的顺序为:
1、rwl.readLock().lock();
2、rwl.readLock().unlock();
3、rwl.writeLock().lock();
4、rwl.readLock().lock();
5、rwl.writeLock().unlock();
6、rwl.readLock().unlock();
以上过程整体讲解:
1、多个线程同时访问该缓存对象时,都加上当前对象的读锁,之后其中某个线程优先查看data数据是否为空。【加锁顺序序号:1】
2、当前查看的线程发现没有值则释放读锁立即加上写锁,准备写入缓存数据(加锁顺序序号:2,3)
3、为什么还会再次判断是否为空值(!cacheValid)是因为第二个、第三个线程获得读的权利时也需要判断是否为空,否则会重复写入数据。
4、写入数据后先进行读锁的降级后再释放写锁【写锁顺序序号:4和5】
5、最后数据返回前释放最终的读锁【加锁顺序序号:6】
如果不使用锁降级功能,如先释放写锁,然后获得读锁,在这个get过程中,可能会有其他线程竞争到写锁,或者更新数据,则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题
五、重入锁和不可重入锁所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的
例子:
比如说A类中有个方法public synchronized methodA1(){
methodA2();
}
而且public synchronized methodA2(){
//具体操作
}
也是A类中的同步方法,当当前线程调用A类的对象methodA1同步方法,如果其他线程没有获取A类的对象锁,
那么当前线程就获得当前A类对象的锁,然后执行methodA1同步方法,方法体中调用methodA2同步方法,
当前线程能够再次获取A类对象的锁,而其他线程是不可以的,这就是可重入锁。
不可重入锁:所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
六、ReenTrantLock可重入锁和synchronized的区别与总结
可重入性:
从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
锁的实现:
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
性能的区别:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
功能区别:
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
ReenTrantLock独有的能力:
1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
ReenTrantLock实现的原理:
简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
什么情况下使用ReenTrantLock:
答案是,如果你需要实现ReenTrantLock的三个独有功能时。
参考:http://blog.csdn.net/qq_20641565/article/details/53208909
参考:https://www.cnblogs.com/liang1101/p/6475555.html?utm_source=itdadao&utm_medium=referral