概述
Java提供了两种加锁方式,一种是基于监视器的Monitor Lock,也叫内置锁Intrinsic Lock;另一种是显示加锁。
Monitor Lock使用synchronized关键字使用,获取对象自身的监视器,实现互斥访问;
显示锁在java中有三种类型,java.util.concurrent.locks.Lock,java.util.concurrent.locks.ReentrantLock和java.util.concurrent.locks.Condition三种。
在JDK1.5中,内置锁比显示锁慢,但这一现象在JDK1.6中得到了修复。
内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized 关键字 ),同步代码块包含两部分:一个作为锁的对象的引用,一个作为由这个锁保护的代码块。
synchronized {
//代码块
}
每个Java对象都可以用做一个实现同步的内置锁(当然除非是显示锁自身实现,不建议显示锁做内置锁使用),线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁;获取锁失败则挂起。
内置锁具有可重入性,当某个线程请求一个由其它线程持有的内置锁时,发出请求的线程就会被阻塞;但如果某个线程试图获得一个由它自己持有的锁时,那么这个请求就会成功,这就是内置锁的可重入性。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。
重入的一种实现方法是为每个锁关联一个获取计数值和一个所有者线程,当计数为0时,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并将获取计数加1,如果同一个线程再次获取这个锁时,计数值将递增,当线程退出同步代码块时,计数值会递减。
下面代码中,如果没有锁的可重入性,那么将产生死锁:
public static void main(String[] args) {
test();
}
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();//若内置锁是不可重入的,则发生死锁
}
}
有一点需要注意, 锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。比如线程A将临界区的一个共享变量改变后,线程B读取这个变量,会去内存中读取。但是,volatile其实并不能真正实现同步,synchronized才是严格的同步。
显示锁
在JDK5之前,可使用的同步机制只有synchronized 关键字和volative变量,JDK5增加了一种新的锁机制:ReentrantLock, ReentrantLock并不是替代内置锁的方法,而是作为一种可选择的高级功能。
锁工具类都在java.util.concurrent.locks包下,有Condition、Lock、ReadWriteLock三个接口:
1.Lock : 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock 。
2.ReadWriteLock : 读写锁接口,以类似方式定义了一些读取者可以共享而写入者独占的锁。实现 ReentrantReadWriteLock ,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
3.Condition : 接口描述了与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。
Lock
在大多数情况下,显示锁都能很好的工作。下面代码给出了Lock接口使用的标准形式,必须在finally中释放锁:
Lock lock = new ReentrantLock();
...
lock.lock();//显示加锁
try{
...
}finally{
//显示释放锁
lock.unlock();
}
上面方式有局限性:如无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限的等待下去。Lock还提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作, boolean tryLock()接口实现可定时与可轮询获取锁的实现,与无条件获取锁的模式相比,它具有更完善的错误恢复机制。
在内置锁中,死锁是一种严重的问题,恢复程序的唯一方法就是重启应用,而防止死锁的方法就是在构造程序时避免出现不一致的锁顺序。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保护的共享通信上发一条消息,如果不能在指定时间内完成,代码就会失败.
public boolean trySendOnSharedLine(String message, long timeout, TimeUnit unit) {
if (!lock.tryLock(timeout, NANOSECONDS)) {
return false;
}
try {
return sendOnSharedLine(message);
}
finally {
lock.unlock();
}
}
在
ReentrantLock中,可以设置公平获取锁,下面举例:
1.首先说明不用显示锁无法实现按序处理
1.1定义Monitor作为锁。
public class Myonitor {
}
1.2定义线程,重写run方法,内部定义synchronized块。
这里,每个线程会sleep一会儿(1s),保证其他线程开始竞锁monitor。
public class MyThread implements Runnable {
private MyMonitor monitor;
public MyThread(MyMonitor monitor) {
super();
this.monitor = monitor;
}
@Override
public void run() {
printMessage("Entered run method...trying to lock monitor object");
synchronized (monitor) {
printMessage("Locked monitor object");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
printMessage("Releasing lock");
monitor.notifyAll();
}
printMessage("End of run method");
}
private void printMessage(String msg) {
System.out.println(Thread.currentThread().getName()+" : "+msg);
}
}
1.3测试案例
public class IntrinsicTest {
public static void main(String[] args) {
System.out.println("=========Intrinsic Test=======");
String[] myThreads = {"Therad ONE","Thread TWO","Thread THREE","Thread FOUR"};
MyMonitor monitor = new MyMonitor();
for(String threadName:myThreads) {
new Thread(new MyThread(monitor),threadName).start();
}
}
}
1.4输出结果
Therad ONE : Entered run method...trying to lock monitor object Therad ONE : Locked monitor object Thread THREE : Entered run method...trying to lock monitor object Thread TWO : Entered run method...trying to lock monitor object Thread FOUR : Entered run method...trying to lock monitor object Therad ONE : Releasing lock Therad ONE : End of run method Thread FOUR : Locked monitor object Thread FOUR : Releasing lock Thread FOUR : End of run method Thread TWO : Locked monitor object Thread TWO : Releasing lock Thread TWO : End of run method Thread THREE : Locked monitor object Thread THREE : Releasing lock Thread THREE : End of run method显然,上面结果显示,在Two, Three, Four线程竞争monitor锁的时候,并没有按照竞争的先后顺序获得锁;用显示锁可以实现公平竞争。
2.1重新定义线程,内部用显示锁。
public class FairThread implements Runnable {
private Lock lock;
public FairThread(Lock lock) {
super();
this.lock = lock;
}
@Override
public void run() {
printMessage("Entered run method...trying to lock monitor object");
lock.lock();
try {
printMessage("Locked monitor object");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
printMessage("Realising lock");
lock.unlock();
}
printMessage("End of run method");
}
private void printMessage(String msg) {
System.out.println(Thread.currentThread().getName() + " : " + msg);
}
}
2.2测试
public class ExplicitLockTest {
public static void main(String[] args) {
System.out.println("=========Explicit Test=======");
String[] myThreads = { "Therad ONE", "Thread TWO", "Thread THREE", "Thread FOUR" };
Lock lock = new ReentrantLock(true);
for (String threadName : myThreads) {
new Thread(new FairThread(lock), threadName).start();
}
}
}
2.3输出结果
Therad ONE : Entered run method...trying to lock monitor object Therad ONE : Locked monitor object Thread TWO : Entered run method...trying to lock monitor object Thread THREE : Entered run method...trying to lock monitor object Thread FOUR : Entered run method...trying to lock monitor object Therad ONE : Realising lock Therad ONE : End of run method Thread TWO : Locked monitor object Thread TWO : Realising lock Thread TWO : End of run method Thread THREE : Locked monitor object Thread THREE : Realising lock Thread THREE : End of run method Thread FOUR : Locked monitor object Thread FOUR : Realising lock Thread FOUR : End of run method显然,适用在 ReentrantLock(true),就可以实现公平竞争了。
ReadWriteLock
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类:
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
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
条件(也称为条件队列或条件变量 )为当前线程提供了一种方式:在需要情况下,让另一个线程一直挂起该线程(即让其“等待”),直到自己通知那个线程。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式释放相关的锁,并挂起当前线程,就像 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;
}
Reference :