java多线程总结(6)

来源:互联网 发布:二手索尼z3淘宝 编辑:程序博客网 时间:2024/06/05 08:05

转载自:并发编程网-ifeve.com


Java中的锁

锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字(译者注:这说的是Java 5之前的情况)。

自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了。但是你仍然需要去了解怎样使用这些锁,且了解这些实现背后的理论也是很有用处的。可以参考我对java.util.concurrent.locks.Lock的介绍,以了解更多关于锁的信息。

以下是本文所涵盖的主题:

  1. 一个简单的锁
  2. 锁的可重入性
  3. 锁的公平性
  4. 在finally语句中调用unlock()


一个简单的锁

让我们从java中的一个同步块开始:

public class Counter{private int count = 0;public int inc(){synchronized(this){return ++count;}}}

可以看到在inc()方法中有一个synchronized(this)代码块。该代码块可以保证在同一时间只有一个线程可以执行return ++count。虽然在synchronized的同步块中的代码可以更加复杂,但是++count这种简单的操作已经足以表达出线程同步的意思。

以下的Counter类用Lock代替synchronized达到了同样的目的:

public class Counter{private Lock lock = new Lock();private int count = 0;public int inc(){lock.lock();int newCount = ++count;lock.unlock();return newCount;}}

lock()方法会对Lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。

这里有一个Lock类的简单实现:

public class Counter{public class Lock{private boolean isLocked = false;public synchronized void lock()throws InterruptedException{while(isLocked){wait();}isLocked = true;}public synchronized void unlock(){isLocked = false;notify();}}

注意其中的while(isLocked)循环,它又被叫做“自旋锁”。自旋锁以及wait()和notify()方法在线程通信这篇文章中有更加详细的介绍。当isLocked为true时,调用lock()的线程在wait()调用上阻塞等待。为防止该线程没有收到notify()调用也从wait()中返回(也称作虚假唤醒),这个线程会重新去检查isLocked条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。

当线程完成了临界区(位于lock()和unlock()之间)中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且通知(唤醒)其中一个(若有的话)在lock()方法中调用了wait()函数而处于等待状态的线程。

锁的可重入性

Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个java代码块。下面是一个例子:

public class Reentrant{public synchronized outer(){inner();}public synchronized inner(){//do something}}

注意outer()和inner()都被声明为synchronized,这在Java中和synchronized(this)块等效。如果一个线程调用了outer(),在outer()里调用inner()就没有什么问题,因为这两个方法(代码块)都由同一个管程对象(”this”)所同步。如果一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。

前面给出的锁实现不是可重入的。如果我们像下面这样重写Reentrant类,当线程调用outer()时,会在inner()方法的lock.lock()处阻塞住。

public class Reentrant2{Lock lock = new Lock();public outer(){lock.lock();inner();lock.unlock();}public synchronized inner(){lock.lock();//do somethinglock.unlock();}}

调用outer()的线程首先会锁住Lock实例,然后继续调用inner()。inner()方法中该线程将再一次尝试锁住Lock实例,结果该动作会失败(也就是说该线程会被阻塞),因为这个Lock实例已经在outer()方法中被锁住了。

两次lock()之间没有调用unlock(),第二次调用lock就会阻塞,看过lock()实现后,会发现原因很明显:

public class Lock{boolean isLocked = false;public synchronized void lock()throws InterruptedException{while(isLocked){wait();}isLocked = true;}...}

一个线程是否被允许退出lock()方法是由while循环(自旋锁)中的条件决定的。当前的判断条件是只有当isLocked为false时lock操作才被允许,而没有考虑是哪个线程锁住了它。

为了让这个Lock类具有可重入性,我们需要对它做一点小的改动:

public class Lock{boolean isLocked = false;Thread  lockedBy = null;int lockedCount = 0;public synchronized void lock()throws InterruptedException{Thread callingThread =Thread.currentThread();while(isLocked && lockedBy != callingThread){wait();}isLocked = true;lockedCount++;lockedBy = callingThread;  }public synchronized void unlock(){if(Thread.curentThread() ==this.lockedBy){lockedCount--;if(lockedCount == 0){isLocked = false;notify();}}}...}

注意到现在的while循环(自旋锁)也考虑到了已锁住该Lock实例的线程。如果当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该Lock实例加了锁,那么while循环就不会被执行,调用lock()的线程就可以退出该方法(译者注:“被允许退出该方法”在当前语义下就是指不会调用wait()而导致阻塞)

除此之外,我们需要记录同一个线程重复对一个锁对象加锁的次数。否则,一次unblock()调用就会解除整个锁,即使当前锁已经被加锁过多次。在unlock()调用没有达到对应lock()调用的次数之前,我们不希望锁被解除。

现在这个Lock类就是可重入的了。

锁的公平性

Java的synchronized块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。本文所展现的锁在内部是用synchronized同步块实现的,因此它们也不保证公平性。饥饿和公平中有更多关于该内容的讨论。

在finally语句中调用unlock()

如果用Lock来保护临界区,并且临界区有可能会抛出异常,那么在finally语句中调用unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:

lock.lock();try{//do critical section code,//which may throw exception} finally {lock.unlock();}

这个简单的结构可以保证当临界区抛出异常时Lock对象可以被解锁。如果不是在finally语句中调用的unlock(),当临界区抛出异常时,Lock对象将永远停留在被锁住的状态,这会导致其它所有在该Lock对象上调用lock()的线程一直阻塞。

Java中的读/写锁

相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。

Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。

以下是本文的主题

  1. 读/写锁的Java实现(Read / Write Lock Java Implementation)
  2. 读/写锁的重入(Read / Write Lock Reentrance)
  3. 读锁重入(Read Reentrance)
  4. 写锁重入(Write Reentrance)
  5. 读锁升级到写锁(Read to Write Reentrance)
  6. 写锁降级到读锁(Write to Read Reentrance)
  7. 可重入的ReadWriteLock的完整实现(Fully Reentrant ReadWriteLock)
  8. 在finally中调用unlock() (Calling unlock() from a finally-clause)

读/写锁的Java实现

先让我们对读写访问资源的条件做个概述:

读取 没有线程正在做写操作,且没有线程在请求写操作。

写入 没有线程正在做读写操作。

如果某个线程想要读取资源,只要没有线程正在对该资源进行写操作且没有线程请求对该资源的写操作即可。我们假设对写操作的请求比对读操作的请求更重要,就要提升写请求的优先级。此外,如果读操作发生的比较频繁,我们又没有提升写操作的优先级,那么就会产生“饥饿”现象。请求写操作的线程会一直阻塞,直到所有的读线程都从ReadWriteLock上解锁了。如果一直保证新线程的读操作权限,那么等待写操作的线程就会一直阻塞下去,结果就是发生“饥饿”。因此,只有当没有线程正在锁住ReadWriteLock进行写操作,且没有线程请求该锁准备执行写操作时,才能保证读操作继续。

当其它线程没有对共享资源进行读操作或者写操作时,某个线程就有可能获得该共享资源的写锁,进而对共享资源进行写操作。有多少线程请求了写锁以及以何种顺序请求写锁并不重要,除非你想保证写锁请求的公平性。

按照上面的叙述,简单的实现出一个读/写锁,代码如下

public class ReadWriteLock{private int readers = 0;private int writers = 0;private int writeRequests = 0;public synchronized void lockRead() throws InterruptedException{while(writers > 0 || writeRequests > 0){wait();}readers++;}public synchronized void unlockRead(){readers--;notifyAll();}public synchronized void lockWrite() throws InterruptedException{writeRequests++;while(readers > 0 || writers > 0){wait();}writeRequests--;writers++;}public synchronized void unlockWrite() throws InterruptedException{writers--;notifyAll();}}

ReadWriteLock类中,读锁和写锁各有一个获取锁和释放锁的方法。

读锁的实现在lockRead()中,只要没有线程拥有写锁(writers==0),且没有线程在请求写锁(writeRequests ==0),所有想获得读锁的线程都能成功获取。

写锁的实现在lockWrite()中,当一个线程想获得写锁的时候,首先会把写锁请求数加1(writeRequests++),然后再去判断是否能够真能获得写锁,当没有线程持有读锁(readers==0 ),且没有线程持有写锁(writers==0)时就能获得写锁。有多少线程在请求写锁并无关系。

需要注意的是,在两个释放锁的方法(unlockRead,unlockWrite)中,都调用了notifyAll方法,而不是notify。要解释这个原因,我们可以想象下面一种情形:

如果有线程在等待获取读锁,同时又有线程在等待获取写锁。如果这时其中一个等待读锁的线程被notify方法唤醒,但因为此时仍有请求写锁的线程存在(writeRequests>0),所以被唤醒的线程会再次进入阻塞状态。然而,等待写锁的线程一个也没被唤醒,就像什么也没发生过一样(译者注:信号丢失现象)。如果用的是notifyAll方法,所有的线程都会被唤醒,然后判断能否获得其请求的锁。

用notifyAll还有一个好处。如果有多个读线程在等待读锁且没有线程在等待写锁时,调用unlockWrite()后,所有等待读锁的线程都能立马成功获取读锁 —— 而不是一次只允许一个。

读/写锁的重入

上面实现的读/写锁(ReadWriteLock) 是不可重入的,当一个已经持有写锁的线程再次请求写锁时,就会被阻塞。原因是已经有一个写线程了——就是它自己。此外,考虑下面的例子:

  1. Thread 1 获得了读锁
  2. Thread 2 请求写锁,但因为Thread 1 持有了读锁,所以写锁请求被阻塞。
  3. Thread 1 再想请求一次读锁,但因为Thread 2处于请求写锁的状态,所以想再次获取读锁也会被阻塞。

上面这种情形使用前面的ReadWriteLock就会被锁定——一种类似于死锁的情形。不会再有线程能够成功获取读锁或写锁了。

为了让ReadWriteLock可重入,需要对它做一些改进。下面会分别处理读锁的重入和写锁的重入。

读锁重入

为了让ReadWriteLock的读锁可重入,我们要先为读锁重入建立规则:

  • 要保证某个线程中的读锁可重入,要么满足获取读锁的条件(没有写或写请求),要么已经持有读锁(不管是否有写请求)。

要确定一个线程是否已经持有读锁,可以用一个map来存储已经持有读锁的线程以及对应线程获取读锁的次数,当需要判断某个线程能否获得读锁时,就利用map中存储的数据进行判断。下面是方法lockRead和unlockRead修改后的的代码:

public class ReadWriteLock{private Map<Thread, Integer> readingThreads =new HashMap<Thread, Integer>();private int writers = 0;private int writeRequests = 0;public synchronized void lockRead() throws InterruptedException{Thread callingThread = Thread.currentThread();while(! canGrantReadAccess(callingThread)){wait();                                                                   }readingThreads.put(callingThread,(getAccessCount(callingThread) + 1));}public synchronized void unlockRead(){Thread callingThread = Thread.currentThread();int accessCount = getAccessCount(callingThread);if(accessCount == 1) { readingThreads.remove(callingThread); } else {readingThreads.put(callingThread, (accessCount -1)); }notifyAll();}private boolean canGrantReadAccess(Thread callingThread){if(writers > 0) return false;if(isReader(callingThread) return true;if(writeRequests > 0) return false;return true;}private int getReadAccessCount(Thread callingThread){Integer accessCount = readingThreads.get(callingThread);if(accessCount == null) return 0;return accessCount.intValue();}private boolean isReader(Thread callingThread){return readingThreads.get(callingThread) != null;}}

代码中我们可以看到,只有在没有线程拥有写锁的情况下才允许读锁的重入。此外,重入的读锁比写锁优先级高。

写锁重入

仅当一个线程已经持有写锁,才允许写锁重入(再次获得写锁)。下面是方法lockWrite和unlockWrite修改后的的代码。

public class ReadWriteLock{private Map<Thread, Integer> readingThreads =new HashMap<Thread, Integer>();private int writeAccesses    = 0;private int writeRequests    = 0;private Thread writingThread = null;public synchronized void lockWrite() throws InterruptedException{writeRequests++;Thread callingThread = Thread.currentThread();while(!canGrantWriteAccess(callingThread)){wait();}writeRequests--;writeAccesses++;writingThread = callingThread;}public synchronized void unlockWrite() throws InterruptedException{writeAccesses--;if(writeAccesses == 0){writingThread = null;}notifyAll();}private boolean canGrantWriteAccess(Thread callingThread){if(hasReaders()) return false;if(writingThread == null)    return true;if(!isWriter(callingThread)) return false;return true;}private boolean hasReaders(){return readingThreads.size() > 0;}private boolean isWriter(Thread callingThread){return writingThread == callingThread;}}

注意在确定当前线程是否能够获取写锁的时候,是如何处理的。

读锁升级到写锁

有时,我们希望一个拥有读锁的线程,也能获得写锁。想要允许这样的操作,要求这个线程是唯一一个拥有读锁的线程。writeLock()需要做点改动来达到这个目的:

public class ReadWriteLock{private Map<Thread, Integer> readingThreads =new HashMap<Thread, Integer>();private int writeAccesses    = 0;private int writeRequests    = 0;private Thread writingThread = null;public synchronized void lockWrite() throws InterruptedException{writeRequests++;Thread callingThread = Thread.currentThread();while(!canGrantWriteAccess(callingThread)){wait();}writeRequests--;writeAccesses++;writingThread = callingThread;}public synchronized void unlockWrite() throws InterruptedException{writeAccesses--;if(writeAccesses == 0){writingThread = null;}notifyAll();}private boolean canGrantWriteAccess(Thread callingThread){if(isOnlyReader(callingThread)) return true;if(hasReaders()) return false;if(writingThread == null) return true;if(!isWriter(callingThread)) return false;return true;}private boolean hasReaders(){return readingThreads.size() > 0;}private boolean isWriter(Thread callingThread){return writingThread == callingThread;}private boolean isOnlyReader(Thread thread){return readers == 1 && readingThreads.get(callingThread) != null;}}

现在ReadWriteLock类就可以从读锁升级到写锁了。

写锁降级到读锁

有时拥有写锁的线程也希望得到读锁。如果一个线程拥有了写锁,那么自然其它线程是不可能拥有读锁或写锁了。所以对于一个拥有写锁的线程,再获得读锁,是不会有什么危险的。我们仅仅需要对上面canGrantReadAccess方法进行简单地修改:

public class ReadWriteLock{private boolean canGrantReadAccess(Thread callingThread){if(isWriter(callingThread)) return true;if(writingThread != null) return false;if(isReader(callingThread) return true;if(writeRequests > 0) return false;return true;}}

可重入的ReadWriteLock的完整实现

下面是完整的ReadWriteLock实现。为了便于代码的阅读与理解,简单对上面的代码做了重构。重构后的代码如下。

public class ReadWriteLock{private Map<Thread, Integer> readingThreads =new HashMap<Thread, Integer>();private int writeAccesses    = 0;private int writeRequests    = 0;private Thread writingThread = null;public synchronized void lockRead() throws InterruptedException{Thread callingThread = Thread.currentThread();while(! canGrantReadAccess(callingThread)){wait();}readingThreads.put(callingThread,(getReadAccessCount(callingThread) + 1));}private boolean canGrantReadAccess(Thread callingThread){if(isWriter(callingThread)) return true;if(hasWriter()) return false;if(isReader(callingThread)) return true;if(hasWriteRequests()) return false;return true;}public synchronized void unlockRead(){Thread callingThread = Thread.currentThread();if(!isReader(callingThread)){throw new IllegalMonitorStateException("Calling Thread does not" +" hold a read lock on this ReadWriteLock");}int accessCount = getReadAccessCount(callingThread);if(accessCount == 1){ readingThreads.remove(callingThread); } else { readingThreads.put(callingThread, (accessCount -1));}notifyAll();}public synchronized void lockWrite() throws InterruptedException{writeRequests++;Thread callingThread = Thread.currentThread();while(!canGrantWriteAccess(callingThread)){wait();}writeRequests--;writeAccesses++;writingThread = callingThread;}public synchronized void unlockWrite() throws InterruptedException{if(!isWriter(Thread.currentThread()){throw new IllegalMonitorStateException("Calling Thread does not" +" hold the write lock on this ReadWriteLock");}writeAccesses--;if(writeAccesses == 0){writingThread = null;}notifyAll();}private boolean canGrantWriteAccess(Thread callingThread){if(isOnlyReader(callingThread)) return true;if(hasReaders()) return false;if(writingThread == null) return true;if(!isWriter(callingThread)) return false;return true;}private int getReadAccessCount(Thread callingThread){Integer accessCount = readingThreads.get(callingThread);if(accessCount == null) return 0;return accessCount.intValue();}private boolean hasReaders(){return readingThreads.size() > 0;}private boolean isReader(Thread callingThread){return readingThreads.get(callingThread) != null;}private boolean isOnlyReader(Thread callingThread){return readingThreads.size() == 1 &&readingThreads.get(callingThread) != null;}private boolean hasWriter(){return writingThread != null;}private boolean isWriter(Thread callingThread){return writingThread == callingThread;}private boolean hasWriteRequests(){return this.writeRequests > 0;}}

在finally中调用unlock()

在利用ReadWriteLock来保护临界区时,如果临界区可能抛出异常,在finally块中调用readUnlock()和writeUnlock()就显得很重要了。这样做是为了保证ReadWriteLock能被成功解锁,然后其它线程可以请求到该锁。这里有个例子:

lock.lockWrite();try{//do critical section code, which may throw exception} finally {lock.unlockWrite();}

上面这样的代码结构能够保证临界区中抛出异常时ReadWriteLock也会被释放。如果unlockWrite方法不是在finally块中调用的,当临界区抛出了异常时,ReadWriteLock 会一直保持在写锁定状态,就会导致所有调用lockRead()或lockWrite()的线程一直阻塞。唯一能够重新解锁ReadWriteLock的因素可能就是ReadWriteLock是可重入的,当抛出异常时,这个线程后续还可以成功获取这把锁,然后执行临界区以及再次调用unlockWrite(),这就会再次释放ReadWriteLock。但是如果该线程后续不再获取这把锁了呢?所以,在finally中调用unlockWrite对写出健壮代码是很重要的。

重入锁死

重入锁死与死锁嵌套管程锁死非常相似。读写锁两篇文章中都有涉及到重入锁死的问题。

当一个线程重新获取读写锁或其他不可重入的同步器时,就可能发生重入锁死。可重入的意思是线程可以重复获得它已经持有的锁。Java的synchronized块是可重入的。因此下面的代码是没问题的:

(译者注:这里提到的锁都是指的不可重入的锁实现,并不是Java类库中的Lock与ReadWriteLock类)

public class Reentrant{public synchronized outer(){inner();}public synchronized inner(){//do something}}

注意outer()和inner()都声明为synchronized,这在Java中这相当于synchronized(this)块(译者注:这里两个方法是实例方法,synchronized的实例方法相当于在this上加锁,如果是static方法,则不然,更多阅读:哪个对象才是锁?)。如果某个线程调用了outer(),outer()中的inner()调用是没问题的,因为两个方法都是在同一个管程对象(即this)上同步的。如果一个线程持有某个管程对象上的锁,那么它就有权访问所有在该管程对象上同步的块。这就叫可重入。若线程已经持有锁,那么它就可以重复访问所有使用该锁的代码块。

下面这个锁的实现是不可重入的:

public class Lock{private boolean isLocked = false;public synchronized void lock()throws InterruptedException{while(isLocked){wait();}isLocked = true;}public synchronized void unlock(){isLocked = false;notify();}}

如果一个线程在两次调用lock()间没有调用unlock()方法,那么第二次调用lock()就会被阻塞,这就出现了重入锁死。

避免重入锁死有两个选择:

  1. 编写代码时避免再次获取已经持有的锁
  2. 使用可重入锁

至于哪个选择最适合你的项目,得视具体情况而定。可重入锁通常没有不可重入锁那么好的表现,而且实现起来复杂,但这些情况在你的项目中也许算不上什么问题。无论你的项目用锁来实现方便还是不用锁方便,可重入特性都需要根据具体问题具体分析。

原创粉丝点击