Home  >  Article  >  Java  >  Java multithreading-detailed explanation of locks and sample code

Java multithreading-detailed explanation of locks and sample code

高洛峰
高洛峰Original
2017-01-05 15:30:301199browse

Since Java 5, the java.util.concurrent.locks package contains some lock implementations, so you don't have to implement your own locks. But you still need to understand how to use these locks.

A simple lock

Let us start with a synchronized block in java:

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

You can see that there is a synchronized(this) code in the inc() method piece. This code block guarantees that only one thread can execute return ++count at the same time. Although the code in the synchronized synchronization block can be more complex, a simple operation like ++count is enough to express the meaning of thread synchronization.

The following Counter class uses Lock instead of synchronized to achieve the same purpose:

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

The lock() method will lock the Lock instance object, so all calls to the lock() method on the object All threads will be blocked until the unlock() method of the Lock object is called.

Here is a simple implementation of the Lock class:

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

Pay attention to the while(isLocked) loop, which is also called "spin lock". When isLocked is true, the thread calling lock() blocks waiting on the wait() call. In order to prevent the thread from returning from wait() without receiving the notify() call (also called a false wake-up), the thread will re-check the isLocked condition to decide whether it is safe to continue execution or whether it needs to wait again, and It is not considered that the thread can safely continue execution after it is awakened. If isLocked is false, the current thread will exit the while(isLocked) loop and set isLocked back to true, allowing other threads that are calling the lock() method to lock the Lock instance.

When the thread completes the code in the critical section (between lock() and unlock()), unlock() will be called. Executing unlock() will reset isLocked to false and notify (wake up) one of the threads (if any) that was in a waiting state after calling the wait() function in the lock() method.

Reentrancy of locks

The synchronized synchronized block in Java is reentrant. This means that if a Java thread enters a synchronized synchronized block in the code and thus obtains the lock on the monitor corresponding to the synchronization object used by the synchronized block, then this thread can enter another synchronized by the same monitor object. A block of java code. Here is an example:

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

Note that both outer() and inner() are declared synchronized, which is equivalent to the synchronized(this) block in Java. If a thread calls outer(), there is no problem in calling inner() within outer(), because both methods (blocks of code) are synchronized by the same monitor object ("this"). If a thread already owns a lock on a monitor object, it has access to all code blocks synchronized by the monitor object. This is reentrancy. A thread can enter any block of code synchronized by a lock it already owns.

The lock implementation given earlier is not reentrant. If we rewrite the Reentrant class as follows, when the thread calls outer(), it will be blocked at lock.lock() of the inner() method.

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

The thread calling outer() will first lock the Lock instance, and then continue to call inner(). In the inner() method, the thread will try to lock the Lock instance again, but the action will fail (that is, the thread will be blocked) because the Lock instance has already been locked in the outer() method.

If unlock() is not called between two lock() calls, the second call to lock will block. After looking at the implementation of lock(), you will find that the reason is obvious:

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

Whether the thread is allowed to exit the lock() method is determined by the conditions in the while loop (spin lock). The current judgment condition is that the lock operation is only allowed when isLocked is false, regardless of which thread locked it.

In order to make this Lock class reentrant, we need to make a small change to it:

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 that the current while loop (spin lock) also takes into account the lock The thread holding the Lock instance. If the current lock object is not locked (isLocked = false), or the current calling thread has already locked the Lock instance, then the while loop will not be executed, and the thread calling lock() can exit the method (translation Note: "Being allowed to exit this method" under the current semantics means that wait() will not be called and cause blocking).

In addition, we need to record the number of times the same thread repeatedly locks a lock object. Otherwise, a single unblock() call will unlock the entire lock, even if the current lock has been locked multiple times. We don't want the lock to be released before the unlock() call has reached the corresponding number of lock() calls.

Now this Lock class is reentrant.

Lock fairness

Java's synchronized blocks do not guarantee the order of threads trying to enter them. Therefore, if multiple threads constantly compete for access to the same synchronized block, there is a risk that one or more of them will never get access - that is, access will always be assigned to other threads. This situation is called thread starvation. To avoid this problem, locks need to be fair. The locks presented in this article are implemented internally using synchronized blocks, so they do not guarantee fairness.

在 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 多线程-锁详解及示例代码相关文章请关注PHP中文网!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn