内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized 关键字 ),同步代码块包含两部分:一个作为锁的对象的引用,一个作为由这个锁保护的代码块。
synchronized {
//代码块
}
每个Java对象都可以用做一个实现同步的锁,这些锁被秒为内置锁(Intrinsic Lock)或监视锁(Monitor Lock),线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时怎释放锁,而且无论是通过正常路径退出锁还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
内置锁具有可重入性,当某个线程请求一个由其它线程持有的内置锁时,发出请求的线程就会被阻塞,如果某个线程试图获得一个由它自己持有的锁时,那么这个请求就会成功,这就是内置锁的可重入性,“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方法是为每个锁关联一个获取计数值和一个所有者线程,当计数为0时,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并将获取计数加1,如果同一个线程再次获取这个锁时,计数值将递增,当线程退出同步代码块时,计数值会递减。
下面代码中,如果没有锁的可重入性,那么将产生死锁:
public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething();//若内置锁是不可重入的,则发生死锁 } }
显示锁
在JDK5之前,可使用的同步机制只有synchronized 关键字和volative变量,JDK5增加了一种新的锁机制:ReentrantLock, ReentrantLock并不是替代内置锁的方法,而是当内置锁机制不适用时作为一种可选择的高级功能。
JDK5提供的锁工具类都在java.util.concurrent.locks包下,有Condition、Lock、ReadWriteLock等接口:
Lock
: 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock
。
ReadWriteLock
: 读写锁接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock
,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
Condition
: 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。
在大多数情况下,显示锁都能很好的工作,但有些局限性,如无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限的等待下去。Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方式都是显示的。下面代码给出了Lock接口使用的标准形式,必须在finally中释放锁:
Lock lock = new ReentrantLock(); ... lock.lock();//显示加锁 try{ ... }finally{ //显示释放锁 lock.unlock(); }
boolean tryLock()接口实现可定时与可轮询获取锁的实现,与无条件获取锁的模式相比,它具有更完善的错误恢复机制,在内置锁中,死锁是一种严重的问题,恢复程序的唯一方法就是重启应用,而防止死锁的方法就是在构造程序时避免出现不一致的锁顺序。可定时与可轮询提供了另一种方式:避免死锁的发生。下面代码实现了可轮询获取锁,如果不能同时获得两个锁,那么就退回重试:
public boolean transferMoney(Account fromAcct, Account toAcct) { while (true) { if (fromAcct.lock.tryLock()) { try { if (toAcct.lock.tryLock()) { try { ... } finally { toAcct.lock.unlock(); } } } finally { fromAcct.lock.unlock(); } } } }
在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限,如果操作不能在指定的时间内给出结果,那么就会使程序提前结束,下面代码演示了试图在Lock保护的共享通信上发一条消息,如果不能在指定时间内完成,代码就会失败,当时的tryLock能够在这种带有时间限制的操作中实现独占的加锁行为:
public boolean trySendOnSharedLine(String message, long timeout, TimeUnit unit) { if (!lock.tryLock(timeout, NANOSECONDS)) { return false; } try { return sendOnSharedLine(message); } finally { lock.unlock(); } }
ReadWriteLock
读写锁 :分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。
获取锁顺序
此类不会将读取者优先或写入者优先强加给锁访问的排序。但是,它确实支持可选的公平 策略。
非公平模式(默认) : 当非公平地(默认)构造时,未指定进入读写锁的顺序,受到 reentrancy 约束的限制。连续竞争的非公平锁可能无限期地推迟一个或多个 reader 或 writer 线程,但吞吐量通常要高于公平锁。
公平模式 : 当公平地构造线程时,线程利用一个近似到达顺序的策略来争夺进入。当释放当前保持的锁时,可以为等待时间最长的单个 writer 线程分配写入锁,如果有一组等待时间大于所有正在等待的 writer 线程 的 reader 线程,将为该组分配写入锁。如果保持写入锁,或者有一个等待的 writer 线程,则试图获得公平读取锁(非重入地)的线程将会阻塞。直到当前最旧的等待 writer 线程已获得并释放了写入锁之后,该线程才会获得读取锁。当然,如果等待 writer 放弃其等待,而保留一个或更多 reader 线程为队列中带有写入锁自由的时间最长的 waiter,则将为那些 reader 分配读取锁。
试图获得公平写入锁的(非重入地)的线程将会阻塞,除非读取锁和写入锁都自由(这意味着没有等待线程)。(注意,非阻塞 ReentrantReadWriteLock.ReadLock.tryLock() 和 ReentrantReadWriteLock.WriteLock.tryLock() 方法不会遵守此公平设置,并将获得锁(如果可能),不考虑等待线程)。
重入
此锁允许 reader 和 writer 按照 ReentrantLock 的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入 reader 使用它们。 此外,writer 可以获取读取锁,但反过来则不成立。在其他应用程序中,当在调用或回调那些在读取锁状态下执行读取操作的方法期间保持写入锁时,重入很有用。如果 reader 试图获取写入锁,那么将永远不会获得成功。
锁降级
重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。
锁获取的中断
读取锁和写入锁都支持锁获取期间的中断。
下面的代码展示了如何利用重入来执行升级缓存后的锁降级(为简单起见,省略了异常处理):
class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } }
下面为用读-写锁来包装的Map类:
/** * Huisou.com Inc. * Copyright (c) 2011-2012 All Rights Reserved. */ package thread; import com.sun.org.apache.bcel.internal.generic.NEW; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @description * * @author chenzehe * @email [email protected] * @create 2013-1-9 下午09:05:58 */ public class ReadWriteMap<K, V> { private final Map<K, V> map; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock r = lock.readLock(); private final Lock w = lock.writeLock(); public ReadWriteMap(Map<K, V> map) { this.map = map; } public V put(K key, V value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } public V get(Object key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } }
Condition
条件(也称为条件队列 或条件变量 )为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait
做的那样。
Condition
实例实质上被绑定到一个锁上。要为特定 Lock
实例获得 Condition
实例,请使用其 newCondition()
方法。如下代码假定有一个绑定的缓冲区,它支持 put
和 take
方法。如果试图在空的缓冲区上执行 take
操作,则在items有可用数据之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put
操作,则在有空间变得可用之前,线程将一直阻塞,如下:
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
双向双端链表LinkedBlockingDeque也是通过实现一个ReentrantLock对象和两个Condition对象来实现“阻塞”和“同步”的:
/** Main lock guarding all access */ private final ReentrantLock lock = new ReentrantLock(); /** Condition for waiting takes */ private final Condition notEmpty = lock.newCondition(); /** Condition for waiting puts */ private final Condition notFull = lock.newCondition(); ...... /** * @throws NullPointerException {@inheritDoc} * @throws InterruptedException {@inheritDoc} */ public void putLast(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); lock.lock(); try { while (!linkLast(e)) notFull.await(); } finally { lock.unlock(); } } ...... /** * Links e as last element, or returns false if full. */ private boolean linkLast(E e) { if (count >= capacity) return false; ++count; Node<E> l = last; Node<E> x = new Node<E>(e, l, null); last = x; if (first == null) first = x; else l.next = x; notEmpty.signal(); return true; }