天天看点

Java多线程——Lock独占重入锁与读写锁详解

Java多线程——Lock独占重入锁与读写锁详解

一、独占式重入锁——ReentrantLock

  1、重入的实现原理

   ①重入概念:

   表示能够对共享资源重复加锁,即当前线程再次获取锁时不会被阻塞。
  ReentrantLock究竟是怎样实现可重入性的?

   总结一下ReentrantLock实现可重入性:

同一线程再次获取锁时计数自增,释放锁时计数自减直到等于0才释放成功

   ②重入的获取

    如果该同步状态不为0,表示此时同步状态(锁)已被线程获取,再判断持有同步状态的线程是否是当前线程,如果是,同步状态再次+1并返回true,表示持有线程重入同步块。(类似于monitor)

    执行流程:lock()->acquire()->tryAcquire()->nonfairTryAcquire()

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 当前锁未被任何线程占用,当前线程可直接获取该锁,同步状态+1
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 体现可重入性:若当前线程是占有该锁的线程,则计数++
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
           

     a)若当前状态==0(设置当前线程持有同步状态的线程)或者当前线程是持有同步状态的线程(同步状态+1),返回true,acquire()的tryAcquire()就不用继续判断了,lock也会直接将持有同步状态线程(当前线程)返回回去

     b)若当前状态!=0或当前线程不是持有同步状态的线程,说明当前线程没有获取到同步状态,acquire()的tryAcquire()再次获取锁失败,继续接下来的流程(加入同步队列…)即可。

   ④重入的释放

     当且仅当同步状态减为0并且持有线程为当前线程时表示锁被正确释放,否则调用setState()将减1后的状态设置为同步状态。若非持有锁线程调用了tryRelease()方法会抛出 IllegalMonitorStateException异常

    执行流程:unlock()->release()->tryRelease()

protected final boolean tryRelease(int releases) {
			// 同步状态-1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
            	// 只有同步状态==0了,才说明锁可以正常释放
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 否则,将同步状态设置为c(同步状态-1),返回false,锁不能释放
            setState(c);
            return free;
        }
           

    tryRelease()会判断当前同步状态-1后的值

     a)若为0,返回true,说明同步状态为初始状态(锁被完全释放,唤醒其他线程竞争该锁)

     b)若不为0,返回flase,说明同步状态不是初始状态(锁没有被完全释放)

  可重入锁有什么特点?

  2、可重入锁的

特点

   ①在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;

   ②由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

  什么是非公平锁?什么是公平锁?

  3、公平锁OR非公平锁

   ①公平锁与非公平锁的概念

     公平锁:锁的获取顺序符合时间上的顺序,即等待时间最长的线程最先获取锁

     非公平锁:不是公平锁

   ②公平锁与非公平锁的特点

if (!hasQueuedPredecessors() &&
                 compareAndSetState(0, acquires))
           

     a)公平锁:获取同步状态并且要是

同步队列中的第⼀个节点

(通过hasQueuedPredecessors()实现)

     b)非公平锁:只要获取了同步状态即成功获取锁(同步队列显得无意义了)

  非公平锁与公平锁有什么区别?

   ③

非公平锁(常用默认)与公平锁的对比

     公平锁执行acquire()的第一步tryAcquire()尝试获取锁时,需要多加一个判断(hasQueuedPredecessors),判断当前节点在同步队列中是否存在前驱节点,若存在,则该节点绝对不会获取同步状态(后面的就不用执行了),若不存在,说明该节点是同步队列的首节点(等待最久了),才会有继续获取同步状态的可能。(从源头上扼杀获取同步状态的可能),而非公平锁的同步队列中的每一个结点都有可能获取到同步状态。

     a)公平锁保证了获取到锁的线程一定是等待时间最长的线程,保证了请求资源时间上的绝对顺序,需要频繁的进行上下文切换,性能开销较大,效率较低。

     b)非公平锁保证系统有更大的吞吐量(效率较高),但是会造成线程“饥饿现象”(有的线程可能永远无法获取到锁)

二、读写锁——ReentrantReadWriteLock

  究竟什么是读写锁?写程序对应独占锁?而读程序对应共享锁?

  1、读写锁的定义

   读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。

   读写锁维护了一个读锁和一个写锁,通过分离读锁和写锁,是并发性大大提高

    ①

写线程-独占锁

能够获取到锁的

前提条件

:没有任何读/写线程拿到锁

    ②

读线程-共享锁

能够获取到锁的

前提条件

:没有写线程拿到锁

  为什么前提条件是这样设定?

   读写锁要保证写锁的操作对读锁可见,若允许读锁在已被获取的情况下对写锁的获取,name正在运行的其他读线程就无法感知到当前写线程的操作

    ③当写锁被获取到,阻塞后续(非当前写操作线程)的读写操作。当读锁被获取到,阻塞后续(非当前写操作线程)的写操作,不阻塞读操作。

  读锁与无锁到底是不是一回事

    PS:

读锁 != 无锁

:当有写线程写的时候,所有读线程都必须全部停止,但如果是无锁的话,其他线程就不会停(无锁的话,不同线程之间互不干扰)。

  读写锁是怎么记录读锁和写锁的状态?

    ④

同步状态的低16位表示写锁获取次数,高16位表示读锁获取次数

  写锁的逻辑是怎么实现?

  2、写锁-独占锁-WriteLock

    ①写锁获取逻辑

     a)当读锁已被读线程获取或者写锁已被其他写线程获取,则写线程获取写锁失败;

     b)否则,当前同步状态没有被任何读写线程获取,当前线程获取写锁成功并且支持重入。

    ②写锁

注意事项

     a)读写锁确保写锁的操作对读锁可见,不允许读锁在已被获取的情况下对写锁获取(正在运行的其他读线程无法感知到当前写线程的操作)

     b)

只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

    ③写锁的释放逻辑同独占式锁的释放(release)

  读锁的逻辑是怎么实现?

  3、读锁-共享式锁-tryAcquireShared()

   当写锁被其他线程获取后,读锁获取失败,其他情况均可成功。

  ReentrantReadWriteLock该如何应用?

   ReentrantReadWriteLock中有许多操作读锁写锁的方法。

   getReadLockCount():返回读锁被获取的次数(不等于线程数,可能一个线程多次获取读锁)

   getReadHoldCount():当前线程获取读锁的次数(保存在ThreadLocal中)

   isWriteLocked():写锁是否被获取

   getWriteHoldCount():写锁被获取的次数

  4、读写锁实例

  

如何线程安全的使用HashMap?

public class ReentrantReadWriteLockTest {
    
    // HashMap是线程不安全的,使用读写锁,使得线程安全
    static Map<String,Object> map = new HashMap<>();
    static ReentrantReadWriteLock reentrantReadWriteLock 
            = new ReentrantReadWriteLock();
    static Lock readLock = reentrantReadWriteLock.readLock();
    static Lock writeLock = reentrantReadWriteLock.writeLock();
    
    // 获取一个key对应的value——读锁
    public static final Object get(String key){
        readLock.lock();
        try {
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }
    
    // 设置key所对应的value,并返回旧的value
    public static final Object put(String key,Object value){
        writeLock.lock();
        try {
            return map.put(key, value);
        }finally {
            writeLock.unlock();
        }
    }
}
           

  5、读写锁相关事项

    ①读锁和写锁是两个锁,但同一个线程是可以拥有两把锁的。

    ②若当前同步状态为S

     写状态获取 S=S+1        写状态释放 S=S-1         低16位(直接加减)

     读状态获取 S=S+(1<<16)        读状态释放S=S-(1<<16)     高16位(左移16位)

    ③读写锁应用

    应用于“缓存”的实现(操作系统内存也是个缓存-多读少写)

  6、锁降级

  锁降级到底指什么?

   锁降级:当前线程把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

  获取读锁是必要的吗?

   

注意

:一定要按照这样的顺序,必须存在获取读锁这步操作,如果先释放了写锁,该写锁被其他线程获取并修改了内容,此时,当前线程就不能感知到数据的更新。而若获取读锁,即便释放写锁后其他线程会被阻塞(当前线程有读锁,写锁不能被调用),直到当前线程释放了读锁,其他线程才会获取到写锁。保证了数据可见性。

    写锁可以降级为读锁,而读锁不能升级为写锁。

继续阅读