ReentrantLock(重入锁)源码分析

简介

ReentrantLock的实现不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,能够控制这个锁是否是公平的。
而锁的名字也是说明了这个锁具备了重复进入的可能,也就是说能够让当前线程多次的进行对锁的获取操作,这样的最大次数限制是Integer.MAX_VALUE,约21亿次左右。
事实上公平的锁机制往往没有非公平的效率高,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了,但是并不是任何场景都是以TPS作为唯一指标的,因为公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

实现分析

在ReentrantLock中,对于公平和非公平的定义是通过对同步器AbstractQueuedSynchronizer的扩展加以实现的,也就是在tryAcquire的实现上做了语义的控制。

非公平的获取语义:

/**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;

/**
     * 设置当前拥有独占访问权的线程
     * {@code null}参数表示没有线程拥有访问权限。否则,此方法不会强加任何同步或{@code volatile}字段访问
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
/**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         * tryLock方法是对该方法的调用
         */
        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();   AbstractQueuedSynchronizer通过一个int变量(stats)来表示同步状态(0为未被同步,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);    //为什么这里不用CAS设置???
                return true;
            }
            return false;
        }

上述逻辑主要包括:

  • 如果当前状态为初始状态,那么尝试设置状态;
  • 如果状态设置成功后就返回;
  • 如果状态被设置,且获取锁的线程又是当前线程的时候,进行状态的自增;
  • 如果未设置成功状态且当前线程不是获取锁的线程,那么返回失败。

公平的获取语义:

/**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        @ReservedStackAccess
        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;
        }

上述逻辑相比较非公平的获取,仅加入了当前线程(Node)之前是否有前置节点在等待的判断。hasQueuedPredecessors()方法命名有些歧义,其实应该是currentThreadHasQueuedPredecessors()更为妥帖一些,也就是说当前面没有人排在该节点(Node)前面时候队且能够设置成功状态,才能够获取锁。

释放语义:

@ReservedStackAccess
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())  //什么时候才会发生这种情况???
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {   //锁已经被全部释放
                free = true;
                setExclusiveOwnerThread(null);  //无任何占有线程
            }
            setState(c);
            return free;
        }

上述逻辑主要主要计算了释放状态后的值,如果为0则完全释放,返回true,反之仅是设置状态,返回false。

这里说Semaphore(共享锁,不可重入)与ReentrantLock的区别:

1)Semaphore的tryAcquireShared和tryReleaseShared内部都是for循环,且tryAcquireShared返回值是int类型以及两个方法通过CAS设置state(那么setState与compareAndSetState有什么区别呢?Java并发之AQS框架详解)

2)ReentrantLock的lock方法默认调用AQS.acquire(),它忽略中断;而Semaphore.acquire默认调用acquireSharedInterruptibly,它首先检查中断状态。

 

下面将主要的笔墨放在公平性和非公平性上,首先看一下二者测试的对比:
测试用例如下:

private static Lock fairLock = new ReentrantLock(true);
	private static Lock unfairLock = new ReentrantLock();

	@Test
	public void fair() {
		System.out.println("fair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(fairLock));
			thread.setName("" + i);
			thread.start();
		}

		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	@Test
	public void unfair() {
		System.out.println("unfair version");
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Job(unfairLock));
			thread.setName("" + i);
			thread.start();
		}

		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private static class Job implements Runnable {
		private Lock lock;
		public Job(Lock lock) {
			this.lock = lock;
		}

		@Override
		public void run() {
			for (int i = 0; i < 5; i++) {
				lock.lock();
				try {
					System.out.println("Lock by:"
							+ Thread.currentThread().getName());
				} finally {
					lock.unlock();
				}
			}
		}
	}

结果如图:

  

仔细观察返回的结果(其中每个数字代表一个线程),非公平的结果一个线程连续获取锁的情况非常多,而公平的结果连续获取的情况基本没有。那么在一个线程获取了锁的那一刻,究竟锁的公平性会导致锁有什么样的处理逻辑呢?

通过之前的同步器(AbstractQueuedSynchronizer)的介绍,在锁上是存在一个等待队列,sync队列,我们通过复写ReentrantLock.getQueuedThreads(该方法是protected,由于继承原则为“两同两小一大”,大即是方法权限越大,所以可以通过重写将protected方法改为public方法来测试,其他时候可能会破坏封装性)获取当前锁的sync队列,输出在ReentrantLock被获取时刻,当前的sync队列的状态。

修改测试如下:

public class ReentrantLockTestQueuedThread {
    private static Lock fairLock = new ReentrantLock2(true);
    private static Lock unfairLock = new ReentrantLock2();

    public void fair() {
        System.out.println("fair version");
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(fairLock)) {
                public String toString() {
                    return getName();
                }
            };
            thread.setName("" + i);
            thread.start();
        }
        // sleep 5000ms
    }


    public void unfair() {
        System.out.println("unfair version");
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(unfairLock)) {
                public String toString() {
                    return getName();
                }
            };
            thread.setName("" + i);
            thread.start();
        }
        // sleep 5000ms
    }

    private static class Job implements Runnable {
        private Lock lock;

        public Job(Lock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                lock.lock();
                try {
                    System.out.println("Lock by:"
                            + Thread.currentThread().getName() + " and "
                            + ((ReentrantLock2) lock).getQueuedThreads()
                            + " waits.");
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    private static class ReentrantLock2 extends ReentrantLock {
        // Constructor Override

        private static final long serialVersionUID = 1773716895097002072L;

        public ReentrantLock2() {
            super();
        }

        public ReentrantLock2(boolean fair) {
            super(fair);
        }

        public Collection<Thread> getQueuedThreads() {
            return super.getQueuedThreads();     //逆序输出,添加节点入队时通过移动tail,所以等待最长的节点是第一个,而getQueuedThreads也是从tail开始向前遍历,所以输出时逆序
        }
    }

    public static void main(String[] args) {
        ReentrantLockTestQueuedThread test = new ReentrantLockTestQueuedThread();
//        test.fair();
        test.unfair();
    }
}

上述逻辑主要是通过构造ReentrantLock2用来输出在sync队列中的线程内容,而且每个线程的toString方法被重写,这样当一个线程获取到锁时,sync队列里的内容也就可以得知了,运行结果如下: 

           

可以明显看出,在非公平获取的过程中,“插队”现象非常严重,后续获取锁的线程根本不顾及sync队列中等待的线程,而是能获取就获取。反观公平获取的过程,锁的获取就类似线性化的,每次都由sync队列中等待最长的线程(链表的第一个,sync队列是由尾部结点添加,当前输出的sync队列是逆序输出)获取锁。一个 hasQueuedPredecessors方法能够获得公平性的特性 

通过下图来深刻的认识公平性和AbstractQueuedSynchronizer的获取过程。
非公平的,或者说默认的获取方式如下图所示:

对于状态的获取,可以快速的通过tryAcquire的成功,也就是黄色的Fast路线,也可以由于tryAcquire的失败,构造节点,进入sync队列中排序后再次获取。因此可以理解为Fast就是一个快速通道,当例子中的线程释放锁之后,快速的通过Fast通道再次获取锁,就算当前sync队列中有排队等待的线程也会被忽略。这种模式,可以保证进入和退出锁的吞吐量,但是sync队列中过早排队的线程会一直处于阻塞状态,造成“饥饿”场景。
而公平性锁,就是在tryAcquire的调用中顾及当前sync队列中的等待节点(废弃了Fast通道),也就是任意请求都需要按照sync队列中既有的顺序进行,先到先得。这样很好的确保了公平性,但是可以从结果中看到,吞吐量就没有非公平的锁高了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值