Java多线程系列—“JUC锁”之 共享锁和ReentrantReadWriteLock

在这里插入图片描述

前言

本文对Java的“共享锁”进行介绍,JUC中的共享锁有CountDownLatch, CyclicBarrier, Semaphore, ReentrantReadWriteLock等;本章会以ReentrantReadWriteLock为例对共享锁进行说明。内容包括

  • ReadWriteLock 和 ReentrantReadWriteLock介绍
  • ReadWriteLock 和 ReentrantReadWriteLock函数说明
  • ReentrantReadWriteLock 类图结构
  • ReentrantReadWriteLock原理
  • ReentrantReadWriteLock示例

ReadWriteLock 和 ReentrantReadWriteLock介绍

ReadWriteLock维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。
“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。
“写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。
注意:不能同时存在读取锁和写入锁!

ReadWriteLock 和 ReentrantReadWriteLock函数说明

// 返回用于读取操作的锁。
Lock readLock()
// 返回用于写入操作的锁。
Lock writeLock()
// 创建一个新的 ReentrantReadWriteLock,默认是采用“非公平策略”。
ReentrantReadWriteLock()
// 创建一个新的 ReentrantReadWriteLock,fair是“公平策略”。fair为true,意味着公平策略;否则,意味着非公平策略。
ReentrantReadWriteLock(boolean fair)
// 返回一个 collection,它包含可能正在等待获取读取锁的线程。
protected Collection<Thread> getQueuedReaderThreads()
// 返回一个 collection,它包含可能正在等待获取读取或写入锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回一个 collection,它包含可能正在等待获取写入锁的线程。
protected Collection<Thread> getQueuedWriterThreads()
// 返回用于读取操作的锁。
ReentrantReadWriteLock.ReadLock readLock()
// 返回用于写入操作的锁。
ReentrantReadWriteLock.WriteLock writeLock()

ReentrantReadWriteLock 类图结构

ReentrantReadWriteLock 类图结构
从类图结构看出:

  • ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了"获取读锁的readLock()函数" 和 “获取写锁的writeLock()函数”。
  • ReentrantReadWriteLock中包含:sync对象,读锁readerLock和写锁writerLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。读锁ReadLock和写锁WriteLock中也都分别包含了"Sync对象",它们的Sync对象和ReentrantReadWriteLock的Sync对象 是一样的,就是通过sync,读锁和写锁实现了对同一个对象的访问。
  • 和"ReentrantLock"一样,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类。Sync也包括"公平锁"FairSync和"非公平锁"NonfairSync。sync对象是"FairSync"和"NonfairSync"中的一个,默认是"NonfairSync"。

共享锁源码说明

ReadLock源码
public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    // ReentrantReadWriteLock的AQS对象
    private final Sync sync;

    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }

    // 获取“共享锁”
    public void lock() {
        sync.acquireShared(1);
    }

    // 如果线程是中断状态,则抛出一场,否则尝试获取共享锁。
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    // 尝试获取“共享锁”
    public  boolean tryLock() {
        return sync.tryReadLock();
    }

    // 在指定时间内,尝试获取“共享锁”
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    // 释放“共享锁”
    public  void unlock() {
        sync.releaseShared(1);
    }

    // 新建条件
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }

    public String toString() {
        int r = sync.getReadLockCount();
        return super.toString() +
            "[Read locks = " + r + "]";
    }
}

下面,分别从“获取共享锁”和“释放共享锁”两个方面对共享锁进行说明。

获取锁

获取共享锁的思想(即lock函数的步骤),是先通过tryAcquireShared()尝试获取共享锁。尝试成功的话,则直接返回;尝试失败的话,则通过doAcquireShared()不断的循环并尝试获取锁,若有需要,则阻塞等待。doAcquireShared()在循环中每次尝试获取锁时,都是通过tryAcquireShared()来进行尝试的。下面看看“获取共享锁”的详细流程。

  • lock()
public void lock() {
    sync.acquireShared(1);
}
  • acquireShared()
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

说明:acquireShared()首先会通过tryAcquireShared()来尝试获取锁。
尝试成功的话,则不再做任何动作(因为已经成功获取到锁了)。
尝试失败的话,则通过doAcquireShared()来获取锁。doAcquireShared()会获取到锁了才返回。

  • tryAcquireShared()
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    // 获取“锁”的状态
    int c = getState();
    // 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 获取“读取锁”的共享计数
    int r = sharedCount(c);
    // 如果“不需要阻塞等待”,并且“读取锁”的共享计数小于MAX_COUNT;
    // 则通过CAS函数更新“锁的状态”,将“读取锁”的共享计数+1。
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 第1次获取“读取锁”。
        if (r == 0) { 
            firstReader = current;
            firstReaderHoldCount = 1;
        // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程
        } else if (firstReader == current) { 
            firstReaderHoldCount++;
        } else {
            // HoldCounter是用来统计该线程获取“读取锁”的次数。
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            // 将该线程获取“读取锁”的次数+1。
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

说明:tryAcquireShared()的作用是尝试获取“共享锁”。
如果在尝试获取锁时,“不需要阻塞等待”并且“读取锁的共享计数小于MAX_COUNT”,则直接通过CAS函数更新“读取锁的共享计数”,以及将“当前线程获取读取锁的次数+1”。
否则,通过fullTryAcquireShared()获取读取锁。

  • fullTryAcquireShared()
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        // 获取“锁”的状态
        int c = getState();
        // 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // 如果“需要阻塞等待”。
        // (01) 当“需要阻塞等待”的线程是第1个获取锁的线程的话,则继续往下执行。
        // (02) 当“需要阻塞等待”的线程获取锁的次数=0时,则返回-1。
        } else if (readerShouldBlock()) {
            // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程
            if (firstReader == current) {
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != current.getId()) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 如果当前线程获取锁的计数=0,则返回-1。
                if (rh.count == 0)
                    return -1;
            }
        }
        // 如果“不需要阻塞等待”,则获取“读取锁”的共享统计数;
        // 如果共享统计数超过MAX_COUNT,则抛出异常。
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 将线程获取“读取锁”的次数+1。
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount。
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,
            // 则将firstReaderHoldCount+1。
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != current.getId())
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                // 更新线程的获取“读取锁”的共享计数
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

说明:fullTryAcquireShared()会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1。

  • doAcquireShared()
private void doAcquireShared(int arg) {
    // addWaiter(Node.SHARED)的作用是,创建“当前线程”对应的节点,并将该线程添加到CLH队列中。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取“node”的前一节点
            final Node p = node.predecessor();
            // 如果“当前线程”是CLH队列的表头,则尝试获取共享锁。
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 如果“当前线程”不是CLH队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待,
            // 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

说明:doAcquireShared()的作用是获取共享锁。
它会首先创建线程对应的CLH队列的节点,然后将该节点添加到CLH队列中。CLH队列是管理获取锁的等待线程的队列。如果“当前线程”是CLH队列的表头,则尝试获取共享锁;否则,则需要通过shouldParkAfterFailedAcquire()判断是否阻塞等待,需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。
doAcquireShared()会通过for循环,不断的进行上面的操作;目的就是获取共享锁。需要注意的是:doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!

释放锁

释放共享锁的思想,是先通过tryReleaseShared()尝试释放共享锁。尝试成功的话,则通过doReleaseShared()唤醒“其他等待获取共享锁的线程”,并返回true;否则的话,返回flase。

  • unlock
public  void unlock() {
    sync.releaseShared(1);
}

说明:该函数实际上调用releaseShared(1)释放共享锁。

  • releaseShared()
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

说明:releaseShared()的目的是让当前线程释放它所持有的共享锁。它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放共享锁。

  • tryReleaseShared()
protected final boolean tryReleaseShared(int unused) {
    // 获取当前线程,即释放共享锁的线程。
    Thread current = Thread.currentThread();
    // 如果想要释放锁的线程(current)是第1个获取锁(firstReader)的线程,
    // 并且“第1个获取锁的线程获取锁的次数”=1,则设置firstReader为null;
    // 否则,将“第1个获取锁的线程的获取次数”-1。
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    // 获取rh对象,并更新“当前线程获取锁的信息”。
    } else {
 
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        // 获取锁的状态
        int c = getState();
        // 将锁的获取次数-1。
        int nextc = c - SHARED_UNIT;
        // 通过CAS更新锁的状态。
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

说明:tryReleaseShared()的作用是尝试释放共享锁。

  • doReleaseShared()
private void doReleaseShared() {
    for (;;) {
        // 获取CLH队列的头节点
        Node h = head;
        // 如果头节点不为null,并且头节点不等于tail节点。
        if (h != null && h != tail) {
            // 获取头节点对应的线程的状态
            int ws = h.waitStatus;
            // 如果头节点对应的线程是SIGNAL状态,则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。
            if (ws == Node.SIGNAL) {
                // 设置“头节点对应的线程状态”为空状态。失败的话,则继续循环。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒“头节点的下一个节点所对应的线程”。
                unparkSuccessor(h);
            }
            // 如果头节点对应的线程是空状态,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态。
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果头节点发生变化,则继续循环。否则,退出循环。
        if (h == head)                   // loop if head changed
            break;
    }
}

说明:doReleaseShared()会释放“共享锁”。它会从前往后的遍历CLH队列,依次“唤醒”然后“执行”队列中每个节点对应的线程;最终的目的是让这些线程释放它们所持有的锁。

公平共享锁和非公平共享锁

和互斥锁ReentrantLock一样,ReadLock也分为公平锁和非公平锁。

公平锁和非公平锁的区别,体现在判断是否需要阻塞的函数readerShouldBlock()是不同的

  • 在公平共享锁中,如果在当前线程的前面有其他线程在等待获取共享锁,则返回true;否则,返回false。
  • 在非公平共享锁中,它会无视当前线程的前面是否有其他线程在等待获取共享锁。只要该非公平共享锁对应的线程不为null,则返回true。

ReentrantReadWriteLock示例

public class RWLockTest {

	/**
	 * ReentrantReadWriterLock类提供两把锁,一把用于写操作,一把用于读操作,
	 * 通过在ReadWriterLock接口声明的readLock获取。
	 * 并可以使用lock(),unLock(),tryLock()对象
	 * 用于写操作锁,通过在ReadWriterLock接口声明的writeLock获取。
	 * 并可以使用lock(),unLock(),tryLock()方法,当对象获得读锁时,不能去修改该对象
	 */
	public static void main(String[] args) {
		PrincePOJO prince=new PrincePOJO();
		//创建多个读取线程对象
		Reader[] reader=new Reader[5];
		Thread[] threadsReader=new Thread[5];
		for(int i=0;i<5;i++){
			reader[i]=new Reader(prince);
			threadsReader[i]=new Thread(reader[i]);
		}
		//创建一个写线程对象
		Thread threadWriter=new Thread(new Writer(prince));
		//启动线程
		for(int i=0;i<5;i++){
			threadsReader[i].start();
		}
		threadWriter.start();
	}

	static class Writer implements Runnable {
		private PrincePOJO price;
		public Writer(PrincePOJO price){
			this.price=price;
		}
		//循环修改价格
		@Override
		public void run() {
			for(int i=0;i<1;i++){
				System.out.println(Thread.currentThread().getName()+":attempt to modify the price");
				price.setPrice(Math.random()*10 ,Math.random()*8);
				System.out.println(Thread.currentThread().getName()+":price have been modfied");
				try {
					Thread.sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

	static class Reader implements Runnable {
		private PrincePOJO price ;
		public Reader(PrincePOJO price){
			this.price=price;
		}
		//读取10次价格
		@Override
		public void run() {
			for(int i=0;i<3;i++){
				System.out.printf("%s: price1:%f-price2:%f\n",Thread.currentThread().getName(),price.getPrice1(),price.getPrice2());
			}
		}

	}

	static class PrincePOJO {
		private double price1;
		private double price2;
		//声明一个ReadWriteLock对象
		private ReadWriteLock lock;

		public PrincePOJO(){
			this.price1=10.00;
			this.price2=8.00;
			lock=new ReentrantReadWriteLock();
		}
		//读取price1
		public double getPrice1(){
			lock.readLock().lock();
			double value=price1;
			lock.readLock().unlock();
			return value;
		}
		//读取price2
		public double getPrice2(){
			lock.readLock().lock();
			double value=price2;
			lock.readLock().unlock();
			return value;
		}
		//设置价格
		public void setPrice(double price1,double price2){
			lock.writeLock().lock();
			this.price1=price1;
			this.price2=price2;
			lock.writeLock().unlock();
		}
	}
}
运行结果
Thread-1: price1:10.000000-price2:8.000000
Thread-5:attempt to modify the price
Thread-4: price1:10.000000-price2:8.000000
Thread-4: price1:10.000000-price2:8.000000
Thread-4: price1:4.354122-price2:3.533028
Thread-3: price1:10.000000-price2:8.000000
Thread-3: price1:4.354122-price2:3.533028
Thread-3: price1:4.354122-price2:3.533028
Thread-0: price1:10.000000-price2:8.000000
Thread-0: price1:4.354122-price2:3.533028
Thread-0: price1:4.354122-price2:3.533028
Thread-2: price1:10.000000-price2:8.000000
Thread-2: price1:4.354122-price2:3.533028
Thread-5:price have been modfied
Thread-1: price1:10.000000-price2:8.000000
Thread-2: price1:4.354122-price2:3.533028
Thread-1: price1:4.354122-price2:3.533028
分析说明
  • 观察Thread-0和Thread-4的运行结果,我们发现,Thread-0启动并获取到“读取锁”,在它还没运行完毕的时候,Thread-1,2,3,4也启动了并且也成功获取到“读取锁”。
    因此,“读取锁”支持被多个线程同时获取。
  • 观察Thread-5这三此“写入锁”的线程。只要“写入锁”被某线程获取,则该线程运行完毕了,才释放该锁。
    因此,“写入锁”不支持被多个线程同时获取。
发布了91 篇原创文章 · 获赞 27 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/xhwwc110/article/details/104469510