`
g21121
  • 浏览: 685604 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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 “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。

1
1
分享到:
评论

相关推荐

    Java并发之ReentrantLock类源码解析

    主要为大家详细介绍了Java并发系列之ReentrantLock源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

    深入java并发编程,使用ReentrantLock和 Synchronized加锁

    深入java并发编程,使用ReentrantLock和 Synchronized加锁

    java ReentrantLock详解.docx

    java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。这篇文章主要是从使用的角度来分析...

    Java并发系列之ReentrantLock源码分析

    主要为大家详细介绍了Java并发系列之ReentrantLock源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

    【Java并发】当年因为Reentrantlock被拒,今天把它源码里里外外给扒了

    【Java并发】当年因为Reentrantlock被拒,今天把它源码里里外外给扒了

    ReentrantLock与synchronized区别

    java语言 并发编程 ReentrantLock与synchronized区别 详解

    Java并发编程基础.pdf

    Java并发编程基础主要包括以下几个核心方面: 线程与线程状态:理解Java中线程的基本概念,包括线程的创建、启动、暂停、恢复和终止。熟悉线程的生命周期及其不同状态,如新建、就绪、运行、阻塞和死亡。 线程同步...

    Java并发编程学习笔记

    4、ReentrantLock底层实现和如何使用 5、Condition源码分析 6、ReentrantReadWriteLock底层实现原理 7、并发工具类CountDownLatch 、CyclicBarrier和Semaphore底层实现原理 8、线程池原理和如何使用线程池 9、...

    java并发编程面试题

    java并发编程 基础知识,守护线程与线程, 并行和并发有什么区别? 什么是上下文切换? 线程和进程区别 什么是线程和进程? 创建线程有哪几种方式?,如何避免线程死锁 线程的 run()和 start()有什么区别? 什么是 ...

    Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁

    主要介绍了Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁,本文讲解了ReentrantLock概况、Lock接口、Lock使用、轮询锁的和定时锁、公平性、可中断获锁获取操作等内容,需要的朋友可以参考下

    Java并发编程实战

    Java并发编程实战 本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及...

    Java并发编程相关技术使用案例

    1、本资源包含并发编程基础知识的使用案例,包括:线程创建、Synchronized和Reentrantlock锁的使用、线程安全问题演示、Condition的应用、CountDownLatch的应用、Cyclicbarrier的应用、Semaphore的应用、线程池的...

    java并发编程1-9

    java并发编程1-9,可解压,并发编程必看资料。 1 Java 并发编程实践基础 2 构建线程安全应用程序 3 使用JDK 并发包构建程序 4 使用开源软件 Amino 构建并发应用程序 5 数据冲突及诊断工具MTRAT 6 死锁 7 显示锁 ...

    java并发锁面试知识

    java中的乐观锁与悲观锁,synchronized与ReentrantLock重入锁的说明与比较

    详解java并发之重入锁-ReentrantLock

    主要介绍了java并发之重入锁-ReentrantLock,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

    java高并发相关知识点.docx

    锁:Java中的锁机制,包括synchronized关键字和ReentrantLock类。 线程池:Java中的线程池机制,包括线程池的创建、执行任务、关闭等操作。 并发集合:Java中的并发集合,包括ConcurrentHashMap、...

    Java 多线程与并发(11-26)-JUC锁- ReentrantLock详解.pdf

    Java 多线程与并发(11_26)-JUC锁_ ReentrantLock详解

    2023年最新Java高并发多线程面试题

    内容概要:最新2023年Java高并发多线程后端面试题整理, 包含线程池,并发集合,volatile,CountDownLatch,Semaphore,Phaser,AQS,ReentrantLock,ReentrantLock等等问题, 用简洁明了的语言,通俗易懂地阐述了高...

    java并发编程专题(五)----详解(JUC)ReentrantLock

    主要介绍了java(JUC)ReentrantLock的的相关资料,文中讲解非常详细,实例代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下

    Java并发教程.md

    [并发](#并发) * [synchronized](#synchronized) * [synchronized底层原理](#synchronized底层原理) * [synchronized 使用方法](#synchronized-使用方法) * [Synchronized和ReentrantLock的区别](#synchronized...

Global site tag (gtag.js) - Google Analytics