A lock is a thread synchronization mechanism like synchronized blocks except locks can be more sophisticated than Java's synchronized blocks. Locks (and other more advanced synchronization mechanisms) are created using synchronized blocks, so it is not like we can get totally rid of thesynchronizedkeyword.

A Simple Lock

Let's start out by looking at a synchronized block of Java code:

public class Counter{

  private int count = 0;

  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}

Notice thesynchronized(this)block in theinc()method. This block makes sure that only one thread can execute thereturn ++countat a time. The code in the synchronized block could have been more advanced, but the simple++countsuffices to get the point across.

TheCounterclass could have been written like this instead, using aLockinstead of a synchronized block:

public class Counter{

  private Lock lock = new Lock();
  private int count = 0;

  public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
  }
}

Thelock()method locks theLockinstance so that all threads callinglock()are blocked untilunlock()is executed.

Here is a simpleLockimplementation:

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();
  }
}

Notice thewhile(isLocked)loop, which is also called a "spin lock". Spin locks and the methodswait()andnotify()are covered in more detail in the textThread Signaling. WhileisLockedis true, the thread callinglock()is parked waiting in thewait()call. In case the thread should return unexpectedly from the wait() call without having received anotify()call (AKA aSpurious Wakeup) the thread re-checks theisLockedcondition to see if it is safe to proceed or not, rather than just assume that being awakened means it is safe to proceed. IfisLockedis false, the thread exits thewhile(isLocked)loop, and setsisLockedback to true, to lock theLockinstance for other threads callinglock().

When the thread is done with the code in thecritical section(the code betweenlock()andunlock()), the thread callsunlock(). Executingunlock()setsisLockedback to false, and notifies (awakens) one of the threads waiting in thewait()call in thelock()method, if any.

但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

Lock Reentrance

Synchronized blocks in Java are reentrant. This means, that if a Java thread enters a synchronized block of code, and thereby take the lock on the monitor object the block is synchronized on, the thread can enter other Java code blocks synchronized on the same monitor object. Here is an example:

public class Reentrant{

  public synchronized outer(){
    inner();
  }

  public synchronized inner(){
    //do something
  }
}

Notice how bothouter()andinner()are declared synchronized, which in Java is equivalent to asynchronized(this)block. If a thread callsouter()there is no problem calling inner() from insideouter(), since both methods (or blocks) are synchronized on the same monitor object ("this"). If a thread already holds the lock on a monitor object, it has access to all blocks synchronized on the same monitor object. This is called reentrance. The thread can reenter any block of code for which it already holds the lock.

The lock implementation shown earlier is not reentrant. If we rewrite theReentrantclass like below, the thread callingouter()will be blocked inside thelock.lock()in theinner()method.

public class Reentrant2{

  Lock lock = new Lock();

  public outer(){
    lock.lock();
    inner();
    lock.unlock();
  }

  public synchronized inner(){
    lock.lock();
    //do something
    lock.unlock();
  }
}

A thread callingouter()will first lock theLockinstance. Then it will callinner(). Inside theinner()method the thread will again try to lock theLockinstance. This will fail (meaning the thread will be blocked), since theLockinstance was locked already in theouter()method.

The reason the thread will be blocked the second time it callslock()without having calledunlock()in between, is apparent when we look at thelock()implementation:

public class Lock{

  boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}

It is the condition inside the while loop (spin lock) that determines if a thread is allowed to exit thelock()method or not. Currently the condition is thatisLockedmust befalsefor this to be allowed, regardless of what thread locked it.

To make theLockclass reentrant we need to make a small change:

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();
      }
    }
  }

  ...
}

Notice how the while loop (spin lock) now also takes the thread that locked theLockinstance into consideration. If either the lock is unlocked (isLocked= false) or the calling thread is the thread that locked theLockinstance, the while loop will not execute, and the thread callinglock()will be allowed to exit the method.

Additionally, we need to count the number of times the lock has been locked by the same thread. Otherwise, a single call tounlock()will unlock the lock, even if the lock has been locked multiple times. We don't want the lock to be unlocked until the thread that locked it, has executed the same amount ofunlock()calls aslock()calls.

TheLockclass is now reentrant.

Lock Fairness

Java's synchronized blocks makes no guarantees about the sequence in which threads trying to enter them are granted access. Therefore, if many threads are constantly competing for access to the same synchronized block, there is a risk that one or more of the threads are never granted access - that access is always granted to other threads. This is called starvation. To avoid this aLockshould be fair. Since theLockimplementations shown in this text uses synchronized blocks internally, they do not guarantee fairness. Starvation and fairness are discussed in more detail in the textStarvation and Fairness.

Calling unlock() From a finally-clause

When guarding a critical section with aLock, and the critical section may throw exceptions, it is important to call theunlock()method from inside afinally-clause. Doing so makes sure that theLockis unlocked so other threads can lock it. Here is an example:

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

This little construct makes sure that theLockis unlocked in case an exception is thrown from the code in the critical section. Ifunlock()was not called from inside afinally-clause, and an exception was thrown from the critical section, theLockwould remain locked forever, causing all threads callinglock()on thatLockinstance to halt indefinately.

results matching ""

    No results matching ""