多线程(五)可重入锁、读写锁

本系列文章:
  多线程(一)线程与进程、Thread
  多线程(二)Java内存模型、同步关键字
  多线程(三)线程池
  多线程(四)显式锁、队列同步器
  多线程(五)可重入锁、读写锁
  多线程(六)线程间通信机制
  多线程(七)原子操作、阻塞队列
  多线程(八)并发容器
  多线程(九)并发工具类
  多线程(十)多线程编程示例

一、ReentrantLock

  ReentrantLock(可重入锁),主要利用CAS+AQS队列来实现,支持公平锁和非公平锁。
  ReentrantLock使用示例:

	private Lock lock = new ReentrantLock();
	 
	public void test(){
	    lock.lock();
	    try{
	        doSomeThing();
	    }catch (Exception e){
	    }finally {
	        lock.unlock();
	    }
	}

  ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

  • 1、ReentrantLock对象是非公平锁
      如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
  • 2、ReentrantLock对象是公平锁
      如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

1.1 ReentrantLock的特点*

  • 1、可重入锁
      可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
      关于可重入性,示例:
	static ReentrantLock lock = new ReentrantLock();
	
	public static void main(String[] args) {
		method1();
	}
	
	public static void method1() {
		lock.lock();
		try {
			log.debug("execute method1");
			method2();
		} finally {
			lock.unlock();
		}
	}

	public static void method2() {
		lock.lock();
		try {
			log.debug("execute method2");
			method3();
		} finally {
			lock.unlock();
		}
	}
	
	public static void method3() {
		lock.lock();
		try {
			log.debug("execute method3");
		} finally {
			lock.unlock();
		}
	}
  • 2、可中断锁
      可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
  • 3、公平锁与非公平锁
      公平锁是指多个线程同时尝试获取同一把锁时,锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO;而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。公平锁会影响性能。
      ReentrantLock提供了两个构造方法:
	public ReentrantLock() {
	    sync = new NonfairSync();
	}
	 
	public ReentrantLock(boolean fair) {
	    sync = fair ? new FairSync() : new NonfairSync();
	}

  默认构造方法初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。

  • 4、可以进行超时设置

1.2 重入性(特点1)

  要支持重入性,就要解决两个问题:

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
  2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

  简单来说,ReentrantLock内部自定义了同步器Sync,在加锁的时候通过CAS算法,将线程对象放到一个双向链表中。每次获取锁的时候,检查当前维护的那个线程ID和当前请求的线程ID是否 一致。如果一致,同步状态加1,表示锁被当前线程获取了多次。

1.2.1 获取锁

  以非公平锁为例,要判断当前线程能否获得锁,核心方法为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;
	        }
	    }
		//2.若被占有,检查占有线程是否是当前线程
	    else if (current == getExclusiveOwnerThread()) {
			// 3. 再次获取,计数加一
	        int nextc = c + acquires;
	        if (nextc < 0) // overflow
	            throw new Error("Maximum lock count exceeded");
	        setState(nextc);
	        return true;
	    }
	    return false;
	}

  为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功,每次重新获取都会对同步状态进行加一的操作。

1.2.2 释放锁

  还是以非公平锁为例,核心方法为tryRelease:

	protected final boolean tryRelease(int releases) {
		//1. 同步状态减1
	    int c = getState() - releases;
	    if (Thread.currentThread() != getExclusiveOwnerThread())
	        throw new IllegalMonitorStateException();
	    boolean free = false;
	    if (c == 0) {
			//2. 只有当同步状态为0时,锁成功被释放,返回true
	        free = true;
	        setExclusiveOwnerThread(null);
	    }
		// 3. 锁未被完全释放,返回false
	    setState(c);
	    return free;
	}

  需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。

1.2.3 重入锁使用示例
	public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {
            lock.lock();
            System.out.println("lock"+i);
            
        }

        for(int i=1;i<=3;i++){
            try {
            	
            } finally {
                lock.unlock();
                System.out.println("unlock"+i);
            }
        }
	}

  结果:

lock1
lock2
lock3
unlock1
unlock2
unlock3

1.3 非公平锁和非公平锁(特点2)

  公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
  公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
  非公平锁:加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。

  1. 非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列。
  2. Java中的synchronized是非公平锁,ReentrantLock默认的lock()方法采用的是非公平锁。
  • 公平策略与非公平策略
      简单来说,如果一个线程先申请锁,先获得锁,就表示使用了公平策略。如果某个线程后申请锁,却先获得了锁,就表示使用了非公平策略。
      一般来说,非公平调度策略的吞吐率较高。它的缺点是:从申请者个体的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较大,即有的线程很快就能申请到资源,而有的线程则要经历若干次暂停与唤醒才能成功申请到资源,极端情况下可能导致饥饿现象
      公平调度策略的吞吐率较低,这是其维护资源独占权的授予顺序的开销比较大(主要是线程的暂停与唤醒所导致的上下文切换)的结果。其优点是,从个体申请者的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较小,即每个资源申请者申请到资源所需的时间基本相同,并且不会导致饥饿现象。
1.3.1 非公平锁

  即NonfairSync。

  • 1、获取锁
      源码:
	final void lock() {
	    if (compareAndSetState(0, 1))
	        setExclusiveOwnerThread(Thread.currentThread());
	    else
	        acquire(1);
	}

  NonfairSync中lock方法的逻辑:用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的线程要去排队。
   “非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
  如果有三个线程去竞争锁,假设线程A的CAS操作成功了,线程B和C就要执行AQS中的acquire方法。

	public final void acquire(int arg) {
	    if (!tryAcquire(arg) &&
	        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	        selfInterrupt();
	}

  这个方法的逻辑在AQS章节已介绍,不再赘述。

  • 2、释放锁
      即unlock()方法:
	public void unlock() {
	    sync.release(1);
	}
  
	public final boolean release(int arg) {
	    if (tryRelease(arg)) {
	        Node h = head;
	        if (h != null && h.waitStatus != 0)
	            unparkSuccessor(h);
	        return true;
	    }
	    return false;
	}

  unlock()方法的流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。
  tryRelease源码:

	/**
	 * 释放当前线程占用的锁
	 * @param releases
	 * @return 是否释放成功
	 */
	protected final boolean tryRelease(int releases) {
	    // 计算释放后state值
	    int c = getState() - releases;
	    // 如果不是当前线程占用锁,那么抛出异常
	    if (Thread.currentThread() != getExclusiveOwnerThread())
	        throw new IllegalMonitorStateException();
	    boolean free = false;
	    if (c == 0) {
	        // 锁被重入次数为0,表示释放成功
	        free = true;
	        // 清空独占线程
	        setExclusiveOwnerThread(null);
	    }
	    // 更新state值
	    setState(c);
	    return free;
	}

  这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。

1.3.2 公平锁

  即FairSync。公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1)。fairSync的lock()源码:

    final void lock() {
        acquire(1);
    }

  acquire是AQS中的模板方法,会调用子类(FairSync)的tryAcquire(int acquires)的方法:

	protected final boolean tryAcquire(int acquires) {
	    final Thread current = Thread.currentThread();
	    int c = getState();
	    if (c == 0) {
	        if (!hasQueuedPredecessors() &&
	            compareAndSetState(0, acquires)) {
	            setExclusiveOwnerThread(current);
	            return true;
	        }
	    }
	    else if (current == getExclusiveOwnerThread()) {
	        int nextc = c + acquires;
	        if (nextc < 0)
	            throw new Error("Maximum lock count exceeded");
	        setState(nextc);
	        return true;
	    }
	    return false;
	  }
	}

  这段代码的逻辑与nonfairTryAcquire基本一致,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁

1.3.3 非公平锁和公平锁的比较

  先用一个例子测试一下:

	public class TestDemo {
		
		static Lock lock = new ReentrantLock(true);
		
		public static void main(String[] args) throws InterruptedException {
		    for(int i=0;i<5;i++){
		        new Thread(new ThreadDemo(i)).start();
		    }
		}
	
		static class ThreadDemo implements Runnable {
		    Integer id;
		    public ThreadDemo(Integer id) {
		        this.id = id;
		    }
	
	        @Override
		    public void run() {
		        try {
		            TimeUnit.MILLISECONDS.sleep(10);
		        } catch (InterruptedException e) {
		            e.printStackTrace();
		        }
		        for(int i=0;i<2;i++){
		             lock.lock();
		             System.out.println("获得锁的线程:"+id);
		             lock.unlock();
		        }
	        }
		}
	}

  公平锁的测试结果,可以看到线程几乎是轮流的获取到了锁:

  再将上述代码改成非公平锁实现,可以看出线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题:

  公平锁与非公平锁的比较:

  • 1、公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序;而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象
  • 2、公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。

1.4 可以响应中断(特点3)

  当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly(),该方法可以用来解决死锁问题。
  接下来看个例子:两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。此时可以使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。示例:

public class TestDemo {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
		//该线程先获取锁1,再获取锁2
        Thread thread = new Thread(new ThreadDemo(lock1, lock2));
        //该线程先获取锁2,再获取锁1
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
        thread.start();
        thread1.start();
        thread.interrupt();//是第一个线程中断
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        
        @Override
        public void run() {
            try {
                firstLock.lockInterruptibly();
                TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
                secondLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常结束!");
            }
        }
    }
}

  结果:

1.5 超时机制(特点4)

  在ReetrantLock中,tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。
  超时机制避免了线程无限期的等待锁释放

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
	public final boolean tryAcquireNanos(int arg, long nanosTimeout)
	        throws InterruptedException {
	    if (Thread.interrupted())
	        throw new InterruptedException();
	    return tryAcquire(arg) ||
	        doAcquireNanos(arg, nanosTimeout);
	}

  如果线程被中断了,那么直接抛出InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。

	/**
	 * 在有限的时间内去竞争锁
	 * @return 是否获取成功
	 */
	private boolean doAcquireNanos(int arg, long nanosTimeout)
	        throws InterruptedException {
	    // 起始时间
	    long lastTime = System.nanoTime();
	    // 线程入队
	    final Node node = addWaiter(Node.EXCLUSIVE);
	    boolean failed = true;
	    try {
	        // 又是自旋!
	        for (;;) {
	            // 获取前驱节点
	            final Node p = node.predecessor();
	            // 如果前驱是头节点并且占用锁成功,则将当前节点变成头结点
	            if (p == head && tryAcquire(arg)) {
	                setHead(node);
	                p.next = null; // help GC
	                failed = false;
	                return true;
	            }
	            // 如果已经超时,返回false
	            if (nanosTimeout <= 0)
	                return false;
	            // 超时时间未到,且需要挂起
	            if (shouldParkAfterFailedAcquire(p, node) &&
	                    nanosTimeout > spinForTimeoutThreshold)
	                // 阻塞当前线程直到超时时间到期
	                LockSupport.parkNanos(this, nanosTimeout);
	            long now = System.nanoTime();
	            // 更新nanosTimeout
	            nanosTimeout -= now - lastTime;
	            lastTime = now;
	            if (Thread.interrupted())
	                //相应中断
	                throw new InterruptedException();
	        }
	    } finally {
	        if (failed)
	            cancelAcquire(node);
	    }
	}

  该方法流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试。
  用超时机制解决死锁的例子:

	public class TestDemo {
	    static Lock lock1 = new ReentrantLock();
	    static Lock lock2 = new ReentrantLock();
	    public static void main(String[] args) throws InterruptedException {
			//该线程先获取锁1,再获取锁2
	        Thread thread = new Thread(new ThreadDemo(lock1, lock2));
	        //该线程先获取锁2,再获取锁1
	        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
	        thread.start();
	        thread1.start();
	    }
	
	    static class ThreadDemo implements Runnable {
	        Lock firstLock;
	        Lock secondLock;
	        public ThreadDemo(Lock firstLock, Lock secondLock) {
	            this.firstLock = firstLock;
	            this.secondLock = secondLock;
	        }
	        @Override
	        public void run() {
	            try {
	                while(!lock1.tryLock()){
	                    TimeUnit.MILLISECONDS.sleep(10);
	                }
	                while(!lock2.tryLock()){
	                    lock1.unlock();
	                    TimeUnit.MILLISECONDS.sleep(10);
	                }
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            } finally {
	                firstLock.unlock();
	                secondLock.unlock();
	                System.out.println(Thread.currentThread().getName()+"正常结束!");
	            }
	        }
	    }
	}

  结果:

Thread-0正常结束!
Thread-1正常结束!

1.6 tryLock、lock和lockInterruptibly的区别*

  这3个方法都用来获取锁。

  1. tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false。
  2. lock能获得锁就返回true,不能的话一直等待获得锁
  3. lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock不会抛出异常,而lockInterruptibly会抛出异常。触发InterruptedException异常之后,线程的中断标志会被清空,即置为false。

1.7 ReentrantLock是如何实现可重入性的*

  • 什么是可重入性
      一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。
  • ReentrantLock如何实现可重入性
      ReentrantLock使用内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计算重入次数(可以理解为计数器),避免频繁的持有释放操作带来的线程问题。
      当state的值不为0时,表示锁已经被一个线程占用了,这时会做一个判断current==getExclusiveOwnerThread(),这个方法返回的是当前持有锁的线程,这个判断是看当前持有锁的线程是不是自己,如果是自己,那么将state的值+1,表示重入返回即可。

二、ReentrantReadWriteLock

  有这样的场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
  针对这种场景,可以用读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
  读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞

2.1 ReentrantReadWriteLock的特点*

  读写锁有以下三个重要的特性:

  • 1、支持公平锁和非公平锁
      支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 2、可重入锁
      读锁和写锁都支持线程重进入。
  • 3、锁可以降级
      遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
      一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁

2.2 ReentrantReadWriteLock源码分析

	public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
	
	    /** 读锁 */
	    private final ReentrantReadWriteLock.ReadLock readerLock;
	    /** 写锁 */
	    private final ReentrantReadWriteLock.WriteLock writerLock;
	    final Sync sync;
	    
	    /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
	    public ReentrantReadWriteLock() {
	        this(false);
	    }
	
	    /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
	    public ReentrantReadWriteLock(boolean fair) {
	        sync = fair ? new FairSync() : new NonfairSync();
	        readerLock = new ReadLock(this);
	        writerLock = new WriteLock(this);
	    }
	
	    /** 返回用于写入操作的锁 */
	    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
	    /** 返回用于读取操作的锁 */
	    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
	
	    abstract static class Sync extends AbstractQueuedSynchronizer {...}
	    static final class NonfairSync extends Sync {...}
	    static final class FairSync extends Sync {...}
	    public static class ReadLock implements Lock, java.io.Serializable {...}
	    public static class WriteLock implements Lock, java.io.Serializable {...}
	    //...
	}

  ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现。ReadWriteLock非常简单,只定义了两个接口:

	public interface ReadWriteLock {
	    Lock readLock();
	    Lock writeLock();
	}

  ReentrantReadWriteLock有五个内部类:

2.2.1 Sync
  • Sync类的属性
	abstract static class Sync extends AbstractQueuedSynchronizer {
	    //版本序列号
	    private static final long serialVersionUID = 6317671515068378041L;        
	    //高16位为读锁,低16位为写锁
	    static final int SHARED_SHIFT   = 16;
	    //读锁单位
	    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
	    //读锁最大数量
	    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
	    //写锁最大数量
	    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
	    //本地线程计数器
	    private transient ThreadLocalHoldCounter readHolds;
	    //缓存的计数器
	    private transient HoldCounter cachedHoldCounter;
	    //第一个读线程
	    private transient Thread firstReader = null;
	    //第一个读线程的计数
	    private transient int firstReaderHoldCount;
	}
  • Sync类的构造函数
	Sync() {
	    // 本地线程计数器
	    readHolds = new ThreadLocalHoldCounter();
	    // 设置AQS的状态
	    setState(getState());
	}
  • Sync类的两个内部类
      Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter。
      HoldCounter主要与读锁配套使用:
	//计数器
	static final class HoldCounter {
	    //表示某个读线程重入的次数,用来计数
	    int count = 0;
	    // 获取当前线程的TID属性的值
	    final long tid = getThreadId(Thread.currentThread());
	}

  ThreadLocalHoldCounter:

	//本地线程计数器
	static final class ThreadLocalHoldCounter
	    extends ThreadLocal<HoldCounter> {
	    //重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
	    public HoldCounter initialValue() {
	        return new HoldCounter();
	    }
	}
2.2.2 读写状态的设计

  同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,在ReentrantLock中的state仅仅表示是否锁定,不用区分是读锁还是写锁。但读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。
  读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。

  假设当前同步状态值为S,get和set的操作如下:

  1. 获取写状态:
      S & 0x0000FFFF:将高16位全部抹去。
  2. 获取读状态:
      S>>>16:无符号补0,右移16位。
  3. 写状态加1:
      S+1。
  4. 读状态加1:
      S+(1<<16)即S + 0x00010000。

  当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
  根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

2.3 写锁的获取与释放

  写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
  WriteLock类中的lock和unlock方法:

	public void lock() {
	    sync.acquire(1);
	}
	
	public void unlock() {
	    sync.release(1);
	}
2.3.1 写锁的获取

  即Sync类中的tryAcquire方法:

	protected final boolean tryAcquire(int acquires) {
	    //当前线程
	    Thread current = Thread.currentThread();
	    //获取状态
	    int c = getState();
	    //写线程数量(即获取独占锁的重入数)
	    int w = exclusiveCount(c);
	
	    //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
	    if (c != 0) {
	        // 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
	        // 如果写锁状态不为0且写锁没有被当前线程持有返回false
	        if (w == 0 || current != getExclusiveOwnerThread())
	            return false;
	
	        //判断同一线程获取写锁是否超过最大次数(65535),支持可重入
	        if (w + exclusiveCount(acquires) > MAX_COUNT)
	            throw new Error("Maximum lock count exceeded");
	        //更新状态
	        //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
	        setState(c + acquires);
	        return true;
	    }
	
	    //到这里说明此时c=0,读锁和写锁都没有被获取
	    //writerShouldBlock表示是否阻塞
	    if (writerShouldBlock() ||
	        !compareAndSetState(c, c + acquires))
	        return false;
	
	    //设置锁为当前线程所有
	    setExclusiveOwnerThread(current);
	    return true;
	}

	//返回占有写锁的线程数量
	static int exclusiveCount(int c) { 
		//直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。
		//这样计算是因为写锁数量由state的低十六位表示。
		return c & EXCLUSIVE_MASK; 
	}

  获取写锁的步骤:

  • (1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。
  • (2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。
  • (3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。
  • (4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
  • (5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。


  该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
  写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

2.3.2 写锁的释放

  即Sync类中的tryRelease方法:

	protected final boolean tryRelease(int releases) {
	    //若锁的持有者不是当前线程,抛出异常
	    if (!isHeldExclusively())
	        throw new IllegalMonitorStateException();
	    //写锁的新线程数
	    int nextc = getState() - releases;
	    //如果独占模式重入数为0了,说明独占模式被释放
	    boolean free = exclusiveCount(nextc) == 0;
	    if (free)
	        //若写锁的新线程数为0,则将锁的持有者设置为null
	        setExclusiveOwnerThread(null);
	    //设置写锁的新线程数
	    //不管独占模式是否被释放,更新独占重入数
	    setState(nextc);
	    return free;
	}

  写锁的释放过程:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。

2.4 读锁的获取与释放

  读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。获取读锁的实现从Java 5到Java 6变得复杂许多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。

2.4.1 读锁的获取

  即Sync类中的tryAcquireShared方法:

	protected final int tryAcquireShared(int unused) {
	    // 获取当前线程
	    Thread current = Thread.currentThread();
	    // 获取状态
	    int c = getState();
	
	    //如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
	    if (exclusiveCount(c) != 0 &&
	        getExclusiveOwnerThread() != current)
	        return -1;
	    // 读锁数量
	    int r = sharedCount(c);
	    /*
	     * readerShouldBlock():读锁是否需要等待(公平锁原则)
	     * r < MAX_COUNT:持有线程小于最大数(65535)
	     * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
	     */
	     // 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
	    if (!readerShouldBlock() &&
	        r < MAX_COUNT &&
	        compareAndSetState(c, c + SHARED_UNIT)) {
	        //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
	        if (r == 0) { // 读锁数量为0
	            // 设置第一个读线程
	            firstReader = current;
	            // 读线程占用的资源数为1
	            firstReaderHoldCount = 1;
	        } else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入
	            // 占用资源数加1
	            firstReaderHoldCount++;
	        } else { // 读锁数量不为0并且不为当前线程
	            // 获取计数器
	            HoldCounter rh = cachedHoldCounter;
	            // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
	            if (rh == null || rh.tid != getThreadId(current))
	                // 获取当前线程对应的计数器
	                cachedHoldCounter = rh = readHolds.get();
	            else if (rh.count == 0) // 计数为0
	                //加入到readHolds中
	                readHolds.set(rh);
	            //计数+1
	            rh.count++;
	        }
	        return 1;
	    }
	    return fullTryAcquireShared(current);
	}

  在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
  读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。

2.4.2 读锁的释放

  即Sync类中的tryReleaseShared方法:

	protected final boolean tryReleaseShared(int unused) {
	    // 获取当前线程
	    Thread current = Thread.currentThread();
	    if (firstReader == current) { // 当前线程为第一个读线程
	        // assert firstReaderHoldCount > 0;
	        if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
	            firstReader = null;
	        else // 减少占用的资源
	            firstReaderHoldCount--;
	    } else { // 当前线程不为第一个读线程
	        // 获取缓存的计数器
	        HoldCounter rh = cachedHoldCounter;
	        if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
	            // 获取当前线程对应的计数器
	            rh = readHolds.get();
	        // 获取计数
	        int count = rh.count;
	        if (count <= 1) { // 计数小于等于1
	            // 移除
	            readHolds.remove();
	            if (count <= 0) // 计数小于等于0,抛出异常
	                throw unmatchedUnlockException();
	        }
	        // 减少计数
	        --rh.count;
	    }
	    for (;;) { // 无限循环
	        // 获取状态
	        int c = getState();
	        // 获取状态
	        int nextc = c - SHARED_UNIT;
	        if (compareAndSetState(c, nextc)) // 比较并进行设置
	            // Releasing the read lock has no effect on readers,
	            // but it may allow waiting writers to proceed if
	            // both read and write locks are now free.
	            return nextc == 0;
	    }
	}

  此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。

2.5 读写锁的互斥性测试

  • 1、基础代码
	public class ReadWriteLockTest {
	    private ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();
	
	    //获取写锁
	    public  void getW(Thread thread) {
	    	try {
	    		rw1.writeLock().lock();
	    		long start = System.currentTimeMillis();
	    		while (System.currentTimeMillis() - start <= 10){
	    			System.out.println(thread.getName() + "正在写操作");
	    		}
	    		System.out.println(thread.getName() + "写操作完成");
		    } catch (Exception e) {
		        e.printStackTrace();
		    } finally {
		        rw1.writeLock().unlock();
		    }
	
	    }
	    
	    //获取读锁
	    public  void getR(Thread thread) {
		    try {
		        rw1.readLock().lock();
		        long start = System.currentTimeMillis();
		        while (System.currentTimeMillis() - start <= 10){
		            System.out.println(thread.getName() + "正在读操作");
		        }
		        System.out.println(thread.getName() + "读操作完成");
		    } catch (Exception e) {
		        e.printStackTrace();
		    } finally {
		        rw1.readLock().unlock();
		    }
	    }
	}  
  • 2、并发读
    public static void main(String[] args) {
        final ReadWriteLockTest test = new ReadWriteLockTest();

        new Thread(){
            @Override
            public void run() {
                test.getR(Thread.currentThread());
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                test.getR(Thread.currentThread());
            }
        }.start();
    }

  结果:

Thread-1正在读操作
Thread-0正在读操作
Thread-1读操作完成
Thread-0读操作完成

  可以看到读线程间是不用排队的。

  • 3、并发写
    public static void main(String[] args) {
        final ReadWriteLockTest test = new ReadWriteLockTest();

        new Thread(){
            @Override
            public void run() {
                test.getW(Thread.currentThread());
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                test.getW(Thread.currentThread());
            }
        }.start();
    }

  结果:

  可以看出写线程获取锁是互斥的。

  • 4、并发读写
    public static void main(String[] args) {
        final ReadWriteLockTest test = new ReadWriteLockTest();

        new Thread(){
            @Override
            public void run() {
                test.getR(Thread.currentThread());
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                test.getW(Thread.currentThread());
            }
        }.start();
    }

  结果:

  可以看出读写线程获取锁也是互斥的。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值