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 “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
相关推荐
Java并发之ReentrantLock类源码解析 ReentrantLock是Java并发包中的一种同步工具,它可以实现可重入锁的功能。ReentrantLock类的源码分析对理解Java并发机制非常重要。本文将对ReentrantLock类的源码进行详细分析,...
Java 并发之 ReentrantLock ReentrantLock 是 Java 并发包中的一个技术,在并发编程中非常常用。它是一个实现 Lock 接口的类,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞...
《Java并发编程实战》是Java并发编程领域的一本经典著作,它深入浅出地介绍了如何在Java平台上进行高效的多线程编程。这本书的源码提供了丰富的示例,可以帮助读者更好地理解书中的理论知识并将其应用到实际项目中。...
在Java并发编程中,多线程是核心概念之一。多线程允许程序同时执行多个任务,从而充分利用系统资源,提高程序性能。然而,多线程编程也带来了同步和竞态条件等问题,这需要开发者具备良好的线程管理和同步机制的知识...
Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发可以极大地提升程序的性能和响应速度。以下是对标题和描述中所提及的几个知识点的详细解释: 1. **线程与并发** - **线程*...
根据提供的信息,“Java 并发编程实战.pdf”这本书聚焦于Java并发编程的实践与应用,旨在帮助读者深入了解并掌握Java中的多线程技术及其在实际项目中的应用技巧。虽然部分内容未能提供具体章节或实例,但从标题及...
锁机制是Java并发编程中的另一大主题,包括内置锁(互斥锁)和显式锁(如`ReentrantLock`)。内置锁是`synchronized`关键字提供的,而显式锁提供了更细粒度的控制和更丰富的功能。书中可能还会讨论读写锁(`...
### Java并发编程实战知识点概述 #### 一、Java并发特性详解 在《Java并发编程实战》这本书中,作者深入浅出地介绍了Java 5.0和Java 6中新增的并发特性。这些特性旨在帮助开发者更高效、安全地编写多线程程序。书中...
《Java 并发编程实战》是一本专注于Java并发编程的权威指南,对于任何希望深入了解Java多线程和并发控制机制的开发者来说,都是不可或缺的参考资料。这本书深入浅出地介绍了如何在Java环境中有效地管理和控制并发...
Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发可以极大地提升程序的性能和响应速度。这份“java并发编程内部分享PPT”显然是一个深入探讨这一主题的资料,旨在帮助开发者...
《Java并发编程实践》是一本深入探讨Java多线程编程的经典著作,由Brian Goetz、Tim Peierls、Joshua Bloch、Joseph Bowles和David Holmes等专家共同编写。这本书全面介绍了Java平台上的并发编程技术,是Java开发...
《Java并发编程的艺术》这本书是Java开发者深入理解并发编程的重要参考书籍。这本书全面地介绍了Java平台上的并发和多线程编程技术,旨在帮助开发者解决在实际工作中遇到的并发问题,提高程序的性能和可伸缩性。 ...
第四部分深入探讨了Java并发编程的高级主题,包括显式锁(如ReentrantLock)、原子变量(Atomic类)、非阻塞算法以及自定义同步组件的开发。这些高级主题帮助开发者解决复杂并发场景下的问题,实现更高层次的并发...
《JAVA并发编程艺术》是Java开发者深入理解和掌握并发编程的一本重要著作,它涵盖了Java并发领域的核心概念和技术。这本书详细阐述了如何在多线程环境下有效地编写高效、可靠的代码,对于提升Java程序员的技能水平...
Java并发编程实践是Java开发中不可或缺的一个领域,它涉及到如何高效、正确地处理多线程环境中的任务。这本书的读书笔记涵盖了多个关键知识点,旨在帮助读者深入理解Java并发编程的核心概念。 1. **线程和进程的...
#### 一、Java并发概述 自Java诞生之初,其设计者就赋予了该语言强大的并发处理能力。Java语言内置了对线程和锁的支持,这使得开发者能够轻松地编写多线程应用程序。本文旨在帮助Java开发者深入理解并发的核心概念...
"Java并发编程与实践"文档深入剖析了这一主题,旨在帮助开发者理解和掌握如何在Java环境中有效地实现并发。 并发是指在单个执行单元(如CPU)中同时执行两个或更多任务的能力。在Java中,这主要通过线程来实现,...
- **Executor框架**:是Java并发工具包中的核心组件之一,提供了强大的任务管理和执行功能,如定时任务、周期性任务等。 #### 二、Java线程安全问题及解决方案 ##### 2.1 线程安全问题分析 在多线程环境下,如果不...
Java并发编程是Java开发者必须掌握的关键技能之一,它涉及到如何在多线程环境中高效、安全地执行程序。并发编程能够充分利用多核处理器的计算能力,提高应用程序的响应速度和整体性能。《Java编程并发实战》这本书是...