并发编程中Lock, synchronized和 ReadWriteLock的异同、重入锁 和不可重入锁的区别

最近在做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();

2)tryLock()是当前线程主动尝试获取锁资源,如果获取成功会有返回值true,失败即返回false,这样的话拿到锁的线程会去执行锁住的资源,而没拿到资源的会马上返回false,不会等待,伪代码如下:
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();

4)lockinterruptibly()这个方法是表示多个线程去抢夺锁资源,threadA线程抢到资源并执行的时候,其他线程在等待状态中,但是我们可以通过interrupt()这个方法使指定没有抢到锁资源的线程中断等待。
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();
    }
}

三、ReadWriteLock(读多写少的场景,读读共享,其他全互斥),适用于读多写少的并发情况

管理一组锁,一个是只读的锁,一个是写锁,读锁可以在没有写锁的时候被多个线程所持有,但是写锁是独占的。一个获得了读锁的线程必须能看到前一个释放的写锁的更新的内容。
每次只能有一个写线程,但同时可以有多个线程并发地读写数据。
它可以实现读写锁,当读取的时候线程会获得read锁,其他线程也可以获得read锁同时并发的去读取(读读不互斥,可以同时加多把read锁),但是写程序运行获取到write锁的时候,其他线程是不能操作的,write是排它锁。
上面的两种锁的read或者write没有抢到锁的线程都会阻塞或者中断,它也是个接口,里面有两种方法:readLock()和writeLock(),它的一个实现类是ReentrantReadWriteLock,其添加了可重入的特性.
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
1、线程进入读锁的前提条件:
1)没有其他线程的写锁
2)没有写请求,或者有写请求但调用线程和持有锁线程是同一个线程
2、进入写锁的前提条件:
1)没有其他线程的读锁
2)没有其他线程的写锁
3、锁降级:从写锁变成读锁(ReentrantReadWriteLock不支持锁升级)
4、锁升级:从读锁变成写锁(ReentrantReadWriteLock支持锁降级)
读锁是可以被多线程共享的,写锁是单线程独占的,也就是说写锁的并发限制比读锁高
5、例子:ReentrantReadWriteLock不支持锁升级
如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,属于锁升级,在同一个线程中发生的
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过程中,可能会有其他线程竞争到写锁,或者更新数据,则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题

 
  
 
  
 
  
五、重入锁和不可重入锁
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的
synchronized和ReentrantLock都是可重入锁
可重入锁的意义在于防止死锁
实现原理:通过为每个锁关联一个请求计数器和一个占有它的线程,当计数器为0时,认为锁未被占有,线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置位1;如果同一个线程再次请求这个锁,计数器将递增;每次
占用线程退出同步块,计数器值将递减,直到计数器为0,锁被释放
例子:

比如说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



你可能感兴趣的:(并发编程中Lock, synchronized和 ReadWriteLock的异同、重入锁 和不可重入锁的区别)