Lock是一个相似同步代码块(synchronized block)的线程同步机制。同步代码块而言,Lock能够作到更细粒度的控制。 Lock(或者其余高级同步机制)也是基于同步代码块(synchronized block),因此还不能彻底摒弃
synchronized
关键字。html
从Java 5开始,
java.util.concurrent.locks
包提供了几个Lock的实现类。你能够直接使用,前提是你知道如何使用它们,并且只有了解它们的内部机制,才能更好的使用它们。 更多的内容能够参考原做者的java.util.concurrent.locks.Lock相关文章,以及JDK API。java
先来看看同步块的代码:安全
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
注意
inc()
方法中的synchronized(this)
块。 这个代码块,能够确保同一时间,只有一个线程能够执行return ++count
。 固然,同步块还有不少高级的用法,这里只是简单的用于保障++count
的安全。并发
对于上面的
Counter
类,能够用Lock
进行改写:this
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()
会让没有获得锁的线程进入等待,直到unlock()
方法被调用。.net
下面看看一个简单的Lock实现:线程
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)
循环,起到了一个“自旋锁(spin lock)”的效果。 有关自旋锁以及wait()
和notify()
的信息,能够去看看《线程间的信号处理》code
当
isLocked
为true时,若是有线程调用lock()
方法,那就会进入wait()
等待。 此时,线程可能会被意外唤醒,从而退出wait()
方法,因此,须要用循环来再次检查isLocked
的条件。(这就是:伪唤醒)htm
若是,
isLocked
为false,则线程不会进入while(isLocked)
等待,而是会直接修改isLocked
为true,从而达到加锁的效果。对象
当线程完成了临界区的代码,而且调用了
unlock()
。接着将isLocked
条件置回false。而且经过notify()
方法,唤醒等待线程,从而达到释放锁的效果。
Java中的同步块是能够重入的(Reentrance)。 这就意味着,Java线程进入一个同步代码块,就能够得到对象上的监控器锁(monitor),当这个线程去访问同一个监控器锁保护的同步代码块时,就能够直接进入了。
来看看这个例子:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
注意,
outer()
和inner()
都声明为synchronized
,这在Java中,就等价于synchronized(this)
。 若是一个线程在outer()
方法中调用了inner()
方法。那么,因为这两个方法,实际都是由同一个监控器(monitor)管理的(this),因此,能够直接进入inner()
方法。
若是一个线程已经持有了某个监控器,那么它就能够访问这个监控器保护的全部同步代码块。而这种特性就称之为“重入”,线程能够从新进入已经得到的锁所保护的代码块。
以前,展现的Lock实现,是一个不可重入锁。 而若是,咱们重写一个像下面这样的
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 something lock.unlock(); } }
当某个线程调用
outer()
方法,会首先锁住Lock实例。 接着,调用inner()
方法。而inner()
会再次对Lock实例进行加锁。 因为Lock实例已经在out()
方法中加锁了,因此这里就会失败,并且会致使线程一直阻塞下去。
线程第二次调用
lock()
方法期间没有调用unlock()
从而致使上述问题。 因此,咱们再来看看lock()
方法的实现:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... }
内部循环的条件,决定着是否容许退出
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(); } } } ... }
如今,自旋循环的条件能够直接放行已经持有锁的线程了。 若是锁是自由状态(
isLocked = false
),或者执行线程就是持有锁的线程,循环都不会执行,线程能够顺利的退出lock()
方法。
另外,咱们须要记录同一个线程加锁的次数。 由于,在
unlock()
释放锁时,咱们须要知道多少次后才能真正释放锁。 而这也将决定,unlock()
须要执行与lock()
对应的次数,才能释放锁。
如今,Lock就是一个可重入锁了。
Java的
synchronized
是不保障线程进入的顺序的。 所以,若是有多个线程不断的竞争同一个同步块,那就有可能某些线程永远也没法获得访问权(比较悲催的线程每次都没有被唤醒)。 这就造成了饥饿。 为了不这个问题,就须要把Lock实现为公平性的。 因为这里的Lock都是基于synchronized
的,因此就没法保障公平性。 更多有关公平性的问题,能够参见:《并发中的饥饿问题以及公平性》
使用Lock来保护临界区时,可能会因为异常,致使没有机会执行
unlock()
方法。 因此,须要经过finally
来释放锁,这样才能确保安全。
lock.lock(); try{ //do critical section code, which may throw exception } finally { lock.unlock(); }
这样一个范式,能够确保Lock能够获得有效释放。
不论对于什么类型的Lock,都须要考虑几点: