Java并发之ReentrantLock

        java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件,但以更难用的语法为代价。 

        Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。 

        ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。 

        LockSupport 类提供了更低级别的阻塞和解除阻塞支持,这对那些实现自己的定制锁类的开发人员很有用。 

        一、锁的概念

        1.可重入锁

        可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。也就是说如果当前线程已经获得了某个监视器对象所持有的锁,那么该线程在该方法中调用另外一个同步方法也同样持有该锁。

        如果锁具备可重入性,则称作为可重入锁。像 synchronized和 ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个 synchronized方法时,比如说 methodA,而在 methodA中会调用另外一个synchronized方法 methodB,此时线程不必重新去申请锁,而是可以直接执行方法 methodB。

        如以下情况:

public synchronized void methodA() {
	// 調用相同监视器对象中的其他 synchronized方法
	this.methodB();
}

public synchronized void methodB() {
	// 其他代码
}

        因为进入 methodA时已经获得了该监视器对象持有的锁,当从 methodA跳转到 methodB时就不必再去获取锁了。

        所以使用以上代码修改后的示例为:

public class LockTest implements Runnable {

	public synchronized void methodA() {
		System.out.println("methodA:" + Thread.currentThread().getId());
		// 调用同线程内另一个 synchronized方法
		methodB();
	}

	public synchronized void methodB() {
		System.out.println("methodB:" + Thread.currentThread().getId());
	}

	public void run() {
		methodA();
	}

	public static void main(String[] args) {
		LockTest lt = new LockTest();
		new Thread(lt).start();
		new Thread(lt).start();
	}
}
//结果:
methodA:9
methodB:9
methodA:10
methodB:10

        假如 synchronized不具备可重入性,此时 methodA线程就需要重新申请锁。但是这就会造成一个问题,因为 methodA线程已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会造成 methodA线程一直等待永远不会获取到的锁。

        所以可重入锁最大的作用是避免死锁

       2.可中断锁

  顾名思义,就是在某些条件下可以相应中断的锁。

  在Java中,synchronized就不是可中断锁,而 Lock是可中断锁。

  如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

  上一篇文章已经介绍过 lockInterruptibly()方法的使用场景,所以 lockInterruptibly()的用法就体现了 Lock的可中断性。

        以下是中断锁的一个示例应用:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest implements Runnable {
	private Lock lock = new ReentrantLock();

	public void methodA() throws InterruptedException {

		lock.lockInterruptibly(); // 如果抛出InterruptedException异常说明已经被中断,需要在外层判断处理
		try {
			System.out.println(Thread.currentThread().getName() + " 获得锁");
			long startTime = System.currentTimeMillis();
			// 等待5秒
			for (;;) {
				if (System.currentTimeMillis() - startTime >= 5000)
					break;
			}
		} finally {
			lock.unlock();
			System.out.println(Thread.currentThread().getName() + " 释放锁");
		}

	}

	public void run() {
		try {
			methodA();
		} catch (InterruptedException e) {
			System.out.println(Thread.currentThread().getName() + " 被中断");
		}
	}

	public static void main(String[] args) {
		LockTest lt = new LockTest();
		Thread t1 = new Thread(lt);
		Thread t2 = new Thread(lt);
		t1.start();
		t2.start();
		t2.interrupt();
	}
}
//结果:
Thread-0 获得锁
Thread-1 被中断
Thread-0 释放锁

        当调用 lockInterruptibly()方法中断锁的获取时,会抛出 InterruptedException异常。这里不应该使用catch捕获异常,否则将继续执行 lockInterruptibly()方法之后的代码,从而报未获取锁的错误。应向外层抛出该异常以证明获取锁操作已经被中断,从而进行其他处理。

        3.公平锁

        公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

        而非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

        在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

        而对于 ReentrantLock和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是在初始化时可以设置为公平锁。

Lock lock=new ReentrantLock(true);
        如果参数为 true表示为公平锁,为 fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

        在 ReentrantLock中定义了2个静态内部类,一个是 NotFairSync,一个是 FairSync,分别用来实现非公平锁和公平锁。ReentrantLock中还有很多与这两种锁相关的方法,在下面的章节中会逐一介绍。

        4.读写锁

        读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

        正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

        ReadWriteLock就是读写锁接口,ReentrantReadWriteLock是这个接口的实现。

        可以通过 readLock()获取读锁,通过 writeLock()获取写锁。

        几种常用的锁类型已经了解,接下来就从具体实现来入手,深入学习它们的用法及原理。

        二、ReentrantLock

        1.简介

        java.util.concurrent.lock 中的 Lock 框架是锁的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)

        ReentrantLock是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。 

        可重入锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

        ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。

        2.构造方法

        ReentrantLock类的构造方法接受一个可选的公平(fair)参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。 

        以下是 ReentrantLock的两种构造方法:

/**
 * 创建 ReentrantLock实例,相当于使用 new ReentrantLock(false);
 */
public ReentrantLock() {
	sync = new NonfairSync();
}

/**
 * 根据指定的公平策略创建 ReentrantLock实例
 */
public ReentrantLock(boolean fair) {
	sync = (fair) ? new FairSync() : new NonfairSync();
}

        默认构造方法相当于构建了一个非公平策略的 ReentrantLock实例。

        3.ReentrantLock使用

        1)lock()方法

         之前已经有相关实例展现了 lock()方法的使用,使用 lock方法值得注意的是需要在finally块中主动释放锁,否则其他线程将阻塞。

public class LockThread {
	Lock lock = new ReentrantLock();

	public void lock() {
		// 获取锁
		try {
			lock.lock();
			System.out.println(Thread.currentThread().getName() + " get the lock");
		} finally {
			// 释放锁
			lock.unlock();
			System.out.println(Thread.currentThread().getName() + " release the lock");
		}
	}

	public static void main(String[] args) {
		final LockThread lt = new LockThread();
		new Thread(new Runnable() {
			public void run() {
				lt.lock();
			}
		}).start();
		new Thread(new Runnable() {
			public void run() {
				lt.lock();
			}
		}).start();
	}
}
//结果:
Thread-0 get the lock
Thread-0 release the lock
Thread-1 get the lock
Thread-1 release the lock

        2)unlock()方法

        unlock方法需要配合 lock()方法使用,unlock方法需要在 catch或 finally块中声明。当未获得锁时,使用unlock方法会抛出 IllegalMonitorStateException异常。

        注释以上代码中 lock方法,将产生以下结果:

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:127)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1175)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:431)
	at LockThread.lock(LockThread.java:14)
	at LockThread$1.run(LockThread.java:23)
	at java.lang.Thread.run(Thread.java:619)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:127)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1175)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:431)
	at LockThread.lock(LockThread.java:14)
	at LockThread$2.run(LockThread.java:28)
	at java.lang.Thread.run(Thread.java:619)

        3)tryLock()方法

        tryLock方法仅在调用时锁未被另一个线程保持的情况下,才获取该锁。

        如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS) ,它几乎是等效的(也检测中断)。 

        如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。 如果锁被另一个线程保持,则此方法将立即返回 false 值。 

        示例代码如下:

public class LockThread {
	Lock lock = new ReentrantLock();

	public void lock() {
		// 尝试获取锁
		if (lock.tryLock()) {
			try {
				System.out.println(Thread.currentThread().getName() + " get the lock");
				while (true) {
					//block here
				}
			} finally {
				// 释放锁
				lock.unlock();
				System.out.println(Thread.currentThread().getName() + " release the lock");
			}
		} else {
			System.out.println(Thread.currentThread().getName() + " get the lock fail");
		}
	}

	public static void main(String[] args) {
		final LockThread lt = new LockThread();
		new Thread(new Runnable() {
			public void run() {
				lt.lock();
			}
		}).start();
		new Thread(new Runnable() {
			public void run() {
				lt.lock();
			}
		}).start();
	}
}
//结果:
Thread-0 get the lock
Thread-1 get the lock fail

        其中利用 while循环产生阻塞,导致 Thread-0线程无法释放锁。当 Thread-1利用 tryLock方法尝试获取锁时,发现锁暂时无法被获取,tryLock方法返回 false,获取锁失败。

        4)tryLock(long timeout, TimeUnit unit)方法

        如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。 

        如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。如果为了使用公平的排序策略,已经设置此锁,并且其他线程都在等待该锁,则不会 获取一个可用的锁。这与 tryLock() 方法相反。如果想使用一个允许闯入公平锁的定时 tryLock,那么可以将定时形式和不定时形式组合在一起: 

if (lock.tryLock() || lock.tryLock(timeout, unit) ) {
 ... 
}

        如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。如果超出了指定的等待时间,则返回值为 false。如果该时间小于等于 0,则此方法根本不会等待。 

        将tryLock部分示例中的lock方法代码修改为:

public void lock() {
	try {
		// 尝试10秒内获取锁
		if (lock.tryLock() || lock.tryLock(10L, TimeUnit.SECONDS)) {
			try {
				System.out.println(Thread.currentThread().getName() + " get the lock");
			} finally {
				long startTime = System.currentTimeMillis();
				for (;;) {
					if (System.currentTimeMillis() - startTime >= 5000) {
						// 释放锁
						System.out.println(Thread.currentThread().getName() + " release the lock");
						lock.unlock();
						break;
					}
				}
			}
		} else {
			System.out.println(Thread.currentThread().getName() + " get the lock fail");
		}
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}
//结果:
Thread-0 get the lock
Thread-0 release the lock
Thread-1 get the lock
Thread-1 release the lock

        结果打印正确。如果将阻塞时间修改的比 tryLock方法时间要长,则结果为:

Thread-0 get the lock
Thread-1 get the lock fail
Thread-0 release the lock

        5)lockInterruptibly()方法

        如果当前线程未被中断,则获取锁。 

        如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。 

        如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。 

        如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:

        • 锁由当前线程获得;或者 

        • 其他某个线程中断当前线程。

        如果当前线程获得该锁,则将锁保持计数设置为 1。 

        如果当前线程: 

        • 在进入此方法时已经设置了该线程的中断状态;或者 

        • 在等待获取锁的同时被中断。 

        则抛出 InterruptedException,并且清除当前线程的已中断状态。 

        在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或重入获取。 

        lockInterruptibly方法的示例在本文刚开始时已经展现,这里就不再复述了。

        6)getHoldCount()方法

        查询当前线程保持此锁的次数。 

        对于与解除锁操作不匹配的每个锁操作,线程都会保持一个锁。 

        保持计数信息通常只用于测试和调试。例如,如果不应该使用已经保持的锁进入代码的某一部分,则可以声明如下: 

ReentrantLock lock = new ReentrantLock();
assert lock.getHoldCount() == 0;
lock.lock();
try {
	// ...
} finally {
	lock.unlock();
}

        其中 assert关键字用法如下:

        (1)assert <boolean表达式>

        如果<boolean表达式>为true,则程序继续执行。

        如果为false,则程序抛出AssertionError,并终止执行。

        (2)assert <boolean表达式> : <错误信息表达式>

        如果<boolean表达式>为true,则程序继续执行。

        如果为false,则程序抛出java.lang.AssertionError,并输入<错误信息表达式>。

        三、ReentrantLock内部类

        1.ReentrantLock.Sync类

       可重入锁内部实现的超类,主要实现了公平与非公平锁的共有方法,并提供了加锁操作的统一抽象:abstract void lock();,还有核心的释放锁的操作。Sync类是 ReentrantLock的内部类,继承自 AbstractQueuedSynchronizer类。可以看到,ReentrantLock都是把具体实现委托给内部类而不是直接继承自 AbstractQueuedSynchronizer,这样的好处是用户不会看到不需要的方法,也避免了用户错误地使用 AbstractQueuedSynchronizer的公开方法而导致错误。

        ReentrantLock的重入计数是使用 AbstractQueuedSynchronizer的state属性的,state大于0表示锁被占用、等于0表示空闲,小于0则是重入次数太多导致溢出了。

        Sync类是该锁的同步控制基础。Sync子类实现了公平与非公平两个版本。

        其中:

        • AbstractOwnableSynchronizer:保持和获取独占线程。

        • AbstractQueuedSynchronizer:以虚拟队列的方式管理线程的锁获取与锁释放,以及各种情况下的线程中断。提供了默认的同步实现,但是获取锁和释放锁的实现定义为抽象方法,由子类实现。目的是使开发人员可以自由定义获取锁以及释放锁的方式。

        • Sync:ReentrantLock的内部抽象类,实现了简单的获取锁和释放锁。

        • NonfairSync和 FairSync:分别表示“非公平锁”和“公平锁”,都继承于 Sync,并且都是 ReentrantLock的内部类。

        • ReentrantLock:实现了 Lock接口的 lock-unlock方法,根据 fair参数决定使用 NonfairSync还是FairSync。

        Sync类中比较重要的实现方法有:nonfairTryAcquire,tryRelease等。

        以下是 Sync的源代码:

static abstract class Sync extends AbstractQueuedSynchronizer {
	private static final long serialVersionUID = -5179523762034025860L;

	/**
	 * 执行Lock.lock()方法,留给子类根据其公平性实现 子类化的最主要原因是允许非公平的快速路径
	 */
	abstract void lock();

	/**
	 * 非公平获取实现
	 */
	final boolean nonfairTryAcquire(int acquires) {
		final Thread current = Thread.currentThread();
		int c = getState();
		if (c == 0) {
			// 如果锁是空闲的,进行加锁必须用CAS操作来确保即使有多个线程竞争锁也是安全的
			if (compareAndSetState(0, acquires)) {
				// 把当前线程设为锁的持有者,在获取前可用于判断是否是重入。
				setExclusiveOwnerThread(current);
				return true;
			}
		} else if (current == getExclusiveOwnerThread()) {
			// 如果锁被占用且当前线程是锁的持有者,说明是重入。
			int nextc = c + acquires;
			if (nextc < 0)
				// 溢出,加锁次数从0开始,加锁与释放操作是对称的,所以绝不会是小于0值,小于0只能是溢出。
				throw new Error("Maximum lock count exceeded");
			// 锁被持有的情况下,只有持有者才能更新锁保护的资源,所以这里不需要用CAS操作。
			setState(nextc);
			return true;
		}
		return false;
	}

	/**
	 * 尝试释放锁
	 */
	protected final boolean tryRelease(int releases) {
		// 先读取state是为了获得一个读屏障,owner不是volatile的。
		int c = getState() - releases;
		// 只有锁的持有者才能释放锁
		if (Thread.currentThread() != getExclusiveOwnerThread())
			throw new IllegalMonitorStateException();
		boolean free = false;
		if (c == 0) {// 锁重入计数减到0,需要真正释放锁了。
			free = true;
			setExclusiveOwnerThread(null);
		}
		// 如果c为0,写操作完成后,其他线程就会看到锁被释放了,所以 setExclusiveOwnerThread必须在这个写之前完成。
		setState(c);
		return free;
	}

	/**
	 * 判断当前线程是否为锁的持有者
	 */
	protected final boolean isHeldExclusively() {
		return getExclusiveOwnerThread() == Thread.currentThread();
	}

	/**
	 * 创建新的 Condition实例
	 */
	final ConditionObject newCondition() {
		return new ConditionObject();
	}

	/**
	 * 获取锁持有者
	 */
	final Thread getOwner() {
		return getState() == 0 ? null : getExclusiveOwnerThread();
	}

	/**
	 * 获取加锁次数
	 */
	final int getHoldCount() {
		// 以state属性作为加锁次数
		return isHeldExclusively() ? getState() : 0;
	}

	/**
	 * 是否获取锁
	 */
	final boolean isLocked() {
		// 加锁次数为0表示没有被拥有
		return getState() != 0;
	}

	/**
	 * 
	 * 从流中重新构建锁实例
	 */
	private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();
		setState(0); // 重置为未锁定状态
	}
}

        其中 compareAndSetState方法的作用是:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。

        setExclusiveOwnerThread方法的作用是:设置当前拥有独占访问的线程。null 参数表示没有线程拥有访问。

        其他方法的作用已经在注释中体现了。

        2.ReentrantLock.NonfairSync与 ReentrantLock.FairSync

        NonfairSync与 FairSync均继承于 Sync类,两个类主要的区别是lock() 方法与 tryAcquire(int)的具体实现。

        首先是 NonfairSync类:

/**
 * 非公平锁 Sync实现
 */
final static class NonfairSync extends Sync {
	private static final long serialVersionUID = 7316153563782823691L;

	/**
	 * 执行lock,尝试立即进入,失败就退回常规流程
	 */
	final void lock() {
		// 首先进行状态设置
		if (compareAndSetState(0, 1))
			// 如果状态设置成功,把当前线程设为锁持有者
			setExclusiveOwnerThread(Thread.currentThread());
		else
			acquire(1);
	}

	/**
	 * 调用非公平版本获取
	 */
	protected final boolean tryAcquire(int acquires) {
		return nonfairTryAcquire(acquires);
	}
}

        然后是 FairSync类,FairSync类提供公平性的锁实现。实现公平性的关键在于:如果锁被占用且当前线程不是持有者也不是等待队列的第一个,则进入等待队列。

/**
 * 公平锁 Sync实现
 */
final static class FairSync extends Sync {
	private static final long serialVersionUID = -3000897897090466540L;

	/**
	 * 获取锁
	 */
	final void lock() {
		// acquire方法会先调用 tryAcquire,所以公平策略的控制留给 tryAcquire
		acquire(1);
	}

	/**
	 * 公平版本 tryAcquire,除非是递归调用或没有等待者或者是第一个,否则不授予访问
	 */
	protected final boolean tryAcquire(int acquires) {
		final Thread current = Thread.currentThread();
		int c = getState();
		if (c == 0) {
			// 与非公平的不同就是要判断当前线程是否为首节点
			if (isFirst(current) && 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;
	}
}

        NonfairSync与 FairSync不同在于:

        lock方法中

        NonfairSync是:如果原状态为0,则设置为新值1,如果设置成功,直接得到锁;如果设置失败则执行与FairSync流程相同的操作。

        FairSync则是:先尝试去获取锁,如果得到了锁则设置状态值为1。

        重入锁方面两个方法表现一样。

        tryAcquire方法中

        FairSync会比 NonfairSync多判断一个 isFirst(current)条件。

 

        isFirst源代码为:

final boolean isFirst(Thread current) {
	Node h, s;
	return ((h = head) == null || ((s = h.next) != null && s.thread == current) || fullIsFirst(current));
}

        线程为首结点需要满足以下条件:

        1)等待队列为空。

        2)等待队列 head的 next结点的 thread为当前线程(head.next.thread = currentThread),即线程为等待队列除哑结点外的第一个结点。

        3)等待队列 head结点到某个结点(暂命名为结点s),之间的所有结点的thread变量为 null,且结点s的thread为当前线程。

        四、ReentrantLock 与 synchronized的选择

        1)比较 ReentrantLock 和 synchronized 的可伸缩性

        引用自网络的测试结果:


        两图总结了不同线程数量的结果。这个评测并不完美,而且只在两个系统上运行了(一个是双 Xeon 运行超线程 Linux,另一个是单处理器 Windows 系统),但是,应当足以表现 synchronized 与 ReentrantLock 相比所具有的伸缩性优势了。

        两图中的图表以每秒调用数为单位显示了吞吐率,把不同的实现调整到 1 线程 synchronized 的情况。每个实现都相对迅速地集中在某个稳定状态的吞吐率上,该状态通常要求处理器得到充分利用,把大多数的处理器时间都花在处理实际工作(计算机随机数)上,只有小部分时间花在了线程调度开支上。我们注意到,synchronized 版本在处理任何类型的争用时,表现都相当差,而 Lock 版本在调度的开支上花的时间相当少,从而为更高的吞吐率留下空间,实现了更有效的 CPU 利用。

        2)条件变量

        类 Object 包含某些特殊的方法,用来在线程的 wait() 、 notify() 和 notifyAll() 之间进行通信。这些是高级的并发性特性,许多开发人员从来没有用过它们 —— 这可能是件好事,因为它们相当微妙,很容易使用不当。幸运的是,随着 JDK 5.0 中引入 java.util.concurrent ,开发人员几乎更加没有什么地方需要使用这些方法了。

        通知与锁定之间有一个交互 —— 为了在对象上 wait 或 notify ,您必须持有该对象的锁。就像 Lock 是同步的概括一样, Lock 框架包含了对 wait 和 notify 的概括,这个概括叫作 条件(Condition) 。 Lock 对象则充当绑定到这个锁的条件变量的工厂对象,与标准的 wait 和 notify 方法不同,对于指定的 Lock ,可以有不止一个条件变量与它关联。这样就简化了许多并发算法的开发。例如, 条件(Condition) 的 Javadoc 显示了一个有界缓冲区实现的示例,该示例使用了两个条件变量,“not full”和“not empty”,它比每个 lock 只用一个 wait 设置的实现方式可读性要好一些(而且更有效)。 Condition 的方法与 wait 、 notify 和 notifyAll 方法类似,分别命名为 await 、 signal 和 signalAll ,因为它们不能覆盖 Object 上的对应方法。

        3)公平与非公平

        ReentrantLock 构造器的一个参数是 boolean fair值,它允许您选择想要一个 公平(fair)锁,还是一个 不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许直接获取锁,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。注意 synchronized 是非公平锁。

        为什么我们不让所有的锁都公平呢?毕竟,公平是好事,不公平是不好的,不是吗?(当孩子们想要一个决定时,总会叫嚷“这不公平”。我们认为公平非常重要,孩子们也知道。)在现实中,公平保证了锁是非常健壮的锁,有很大的性能成本。要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,应当把公平设置为 false ,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。

        那么同步又如何呢?内置的监控器锁是公平的吗?答案令许多人感到大吃一惊,它们是不公平的,而且永远都是不公平的。但是没有人抱怨过线程饥渴,因为 JVM 保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。所以,默认情况下 ReentrantLock 是“不公平”的,这一事实只是把同步中一直是事件的东西表面化而已。如果您在同步的时候并不介意这一点,那么在 ReentrantLock 时也不必为它担心。



 

        以上两图与之前两图数据相同,只是添加了一个数据集,用来进行随机数基准检测,这次检测使用了公平锁,而不是默认的协商锁。正如您能看到的,公平是有代价的。如果您需要公平,就必须付出代价,但是请不要把它作为您的默认选择。

        4)synchronized已无用?

        虽然 ReentrantLock 是个非常动人的实现,相对 synchronized 来说,它有一些重要的优势,但是我认为急于把 synchronized 视若敝屣,绝对是个严重的错误。 java.util.concurrent.lock 中的锁定类是用于高级用户和高级情况的工具 。一般来说,除非您对 Lock 的某个高级特性有明确的需要,或者有明确的证据(而不是仅仅是怀疑)表明在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用 synchronized。

        为什么我在一个显然“更好的”实现的使用上主张保守呢?因为对于 java.util.concurrent.lock 中的锁定类来说,synchronized 仍然有一些优势。比如,在使用 synchronized 的时候,不可能忘记释放锁;在退出 synchronized 块时,JVM 会为您做这件事。您很容易忘记用 finally 块释放锁,这对程序非常有害。您的程序能够通过测试,但会在实际工作中出现死锁,那时会很难指出原因(这也是为什么根本不让初级开发人员使用 Lock 的一个好理由。)

        另一个原因是因为,当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。而且,几乎每个开发人员都熟悉 synchronized,它可以在 JVM 的所有版本中工作。在 JDK 5.0 成为标准(从现在开始可能需要两年)之前,使用 Lock 类将意味着要利用的特性不是每个 JVM 都有的,而且不是每个开发人员都熟悉的。

        5)什么时候选择用 ReentrantLock 代替 synchronized

        既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。

猜你喜欢

转载自286.iteye.com/blog/2296191