java锁和同步

Java 语言设计中的一大创新就是:第一个把跨平台线程模型和锁模型应用到语言中去,Java 语言包括了跨线程的关键字synchronized 和 volatile,使用关键字和java类库就能够简单的实现线程间的同步。在简化与平台无关的并发程序开发时,它没有使并发程序的编写工作变得繁琐,反而使它变得更容易了。

在这一章,我们详细介绍锁的技术和概念,java中提供了两种锁,一个是使用关键字的锁,还有一种类库提供的锁。

synchronized关键字锁

synchronized关键字能够作为函数的修饰符,也可作为函数内的语句,这就是常用的同步方法和同步语句块。如果仔细进行分类,synchronized可作用于instance函数、instance变量、static函数和class (类 变量)身上。 

在使用synchronised关键字时,我们需注意下面几点:

1.无论synchronized关键字加在方法上还是对象上,它的锁都是针对的java对象,而不是把一段代码或函数。

2.每个对象只有一个锁(lock)与之相关联。如果要实现多个锁,需要对应多个方法

3.实现同步需要的要系统开销非常大,甚至可能造成死锁。需要特别注意。

在方法声明中加入 synchronized关键字来声明 synchronized 方法,如:

    public synchronized void accessVal(int newVal);

synchronized 方法控制对类成员变量的访问:每个实例对象对应一把锁,synchronized 方法都必须获得该实例对象锁后才能执行,否则所属线程将被阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才由虚拟机将锁释放,此后被该实例对象的锁阻塞的线程方能获得该锁。这种机制确保了同一时刻,其所有声明为 synchronized 的成员函数中至多只有一个能够执行(因为至多只有一个线程能够获得该实例对象的锁),从而有效地避免了成员变量的访问冲突。

在 Java 中,每一个类(类对象)也对应一把锁,这样我们也可将类静态成员函数声明为 synchronized ,以控制对类的静态成员变量的访问。

synchronized 方法的缺陷:若将一个执行时间较长的方法声明为synchronized 将会大大影响性能,因此其他任何 synchronized 方法的调用都需要等待。当然我们可以将访问类成员变量的代码放到专门的方法中,并将其声明为 synchronized,并在其他方法中调用它来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 语句块。

在一个函数里,将synchronized关键字修饰一个对象,如:

    public void method(SomeObject so) { 
        synchronized(so) { 

           //….. 

        } 

    } 

这时锁就是so对象,谁拿到这个锁就有权运行它所控制的那段代码。当有一个明确的对象作为锁时,就能够使用上面的方法加锁,但当没有明确的对象作为锁,我们需要创建一个特别的实例变量来充当锁。

当让,类对象也能充当synchronized 块的锁,如MYClass.class,语义跟实例对象相同。

对象同步

wait、notify和notifyAll是java基本对象Object的线程同步方法,这些方法被声明为final native,这些方法不能被子类重写,下面是他们的定义:

  public final void wait() throws InterruptedException

当线程调用此方法时,它就会被操作系统挂起而进入等待状态,直到被其他线程通过notify()或者notifyAll()唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。

  public final native void wait(long timeout) throws InterruptedException  当线程调用此方法时,它也会像wait()函数一样被操作系统挂起挂起而进入等待状态,但是它有两种方法被唤醒:(一)其他线程通过notify()或者notifyAll()唤醒;(二)经过指定的timeout时间。它也只能在同步方法中调用,否则该方法抛出一个IllegalMonitorStateException异常。

  wait()和wait(long timeout)也可以在一种特殊情况下被唤醒:其他线程调用等待线程的interrupt()函数,wait()和wait(long timeout)在抛出异常的情况下被唤醒。

  public final native void notify()  随机选择一个在该对象上调用wait方法的线程,将其唤醒。该方法也只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。

  public final native void notifyAll()  唤醒所有在该对象上调用wait方法的线程。该方法也只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。

请看这些函数实现信号量的例子:

通常把一个非负整数称为信号量(Semaphore),表示为:S,可理解为可用的资源数量.,通常假定S >=0,信号量实现同步机制表示为PV原语操作:

P(S): S减一(--S),若S<0,线程进入等待队列;否则继续执行

V(S):S加一(++S),若S<=0唤醒处于等待中的一个线程;

public abstract class Semaphore {

    private int value = 0;

    public Semaphore() {

    }

    public Semaphore(int initial) {

       if (initial >= 0)

           value = initial;

       else

           throw new IllegalArgumentException("initial < 0");

    }

    public final synchronized void P() throws InterruptedException {

       value--;

       if(value < 0)

           wait();     

    }

    public final synchronized void V() {

       value++;

       if(value <=0)

         notify();

    }

}

有一个问题:wait()函数会不会释放synchronized对象锁,答案是肯定的。我们拿上面信号量解释这个问题,假如wait()函数不释放锁时,我们假定S=1,当执行P操作时,运行的线程拿到当前信号量的锁,在减一操作后,调用wait操作,然后当前线程被挂起,此时当另外一个线程执行V操作时,也需要获得synchronized对象锁,因为锁被挂起的线程拥有,执行V操作不能执行notify函数,被挂起的线程也不能被唤醒,这就造成了死锁。实际上,这种PV操作时能够正确执行,原因是wait函数释放了线程拥有的synchronized对象锁。Wait函数主要做了下面几件事:

  1. 将当前线程加入等待队列
  2. 释放线程占有的锁,然后挂起自己
  3. 线程唤醒后,将自己移出等待队列
  4. 重新获得对象锁,然后返回

再有一个问题:在什么样的情况下,线程会释放持有的锁?大约有如下几种情况:

  1. 执行完同步代码块
  2. 在执行同步代码块的过程中,遇到异常而导致线程终止。
  3. 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁。

在下面几种情况下,线程是不会释放持有的锁:

  1. 在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁
  2. 在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁。
  3. 在执行同步代码块的过程中,其他线程执行了suspend()方法,当前线程被暂停,但不会释放锁。但Thread类的suspend()方法已经被废弃。

再讲完java最基本的锁和同步技术后,我们来介绍java类库提供的锁和同步技术

Lock接口

前面的介绍,可以看出synchronized实现同步是一种相当不错的方法,那么为什么JAVA团队还要花许多时间来开发 java.util.concurrent.lock 框架呢?答案是synchronized关键字提供的同步比较简单,功能有一些限制: 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。synchronized同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行。

java.util.concurrent.lock中的Lock框架是锁的一个抽象,它允许把锁的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与synchronized 相同的并发性和语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)

java.util.concurrent.lock接口中定义了下面几个方法:

  void lock(); 典型的加锁操作,不同的锁有不同的语义,当线程调用ReentrantLock.lock方法时,锁计数加一,它在两种情况下才可获得锁:(一)当前线程是锁的持有者;(二)锁没有被其它线程持有。

  void lockInterruptibly() throws InterruptedException;  可中断加锁操作,与lock操作只有一点不同:允许在等待时可由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不会获取锁,而会抛出一个InterruptedException,而lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。

  boolean tryLock();  试图执行加锁操作,如果当前锁可以执行加锁操作,则对其加锁并且返回true,否则仅仅返回false

  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  类似于tryLock方法,也是一种试图加锁操作,唯一的一点区别:当锁不可达时,线程可以等待指定的time时间,经过time后,仍旧不能获得锁,则返回false,否则执行加锁并且返回true。当然,线程也可以被其它线程执行Thread.interrupt方法中断。

  void unlock();   典型的释放锁操作,不同的锁有不同的语义,当线程调用ReentrantLock.unlock方法时,锁计数减一,它必须满足两种情况才能释放锁:(一)当前线程是锁的持有者;(二)ReentrantLock锁的计算为0

  Condition newCondition();  创建一个条件变量,此条件可量类似于Object的wait, notify操作,可能实现线程的同步。我们将在后面的条件变量部分,详细介绍条件变量的操作。

 

条件变量

 Object 提供了 wait()notify()notifyAll()些特殊的方法,用来在线程之间进行通信。这些都是些高级的并发性特性,许多开发人员从来没有用过它们,这可能是件好事,因为很难合适地使用它们。就像 Lock 一样, Lock 框架包含对 wait 和 notify 的定义,被叫作 条件变量(Condition)。 与标准的 wait 和notify 方法不同,对于指定的 Lock ,可以有不止一个条件变量与它关联,这样就简化了许多并发算法的开发。例如Javadoc中的条件变量(Condition)显示了一个有界缓冲区实现的示例,该示例使用了两个条件变量“not full”和“not empty”,它比每个 lock 只用一个 wait 设置的实现方式可读性要好一些。 Condition 的方法与 wait 、 notify 和 notifyAll 方法类似,分别命名为 await 、 signal 和signalAll ,因为它们不能必须实现 Object 上的对应方法。

请看Condition包含的主要方法:

  void await() throws InterruptedException;  功能相似于Object的wait方法,线程执行此方法于,会被操作系统挂起而进入等待状态,直到被signal()和signalAll()唤醒,它是一中可中断的操作,等待线程被它行程调用Thread.interrupt后,抛出InterruptedException异常。在调用前,线程也必须加锁,如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。

  void awaitUninterruptibly();  不可中断的等待操作,类似于await,它不能被其它线程中断唤醒。

long awaitNanos(long nanosTimeout) throws InterruptedException;  类似于await,多了一个纳秒级别的时间参数,经过nanosTimeout时间后,线程也会唤醒,有一个大的区别:函数会返回所剩微秒数的一个估计值,如果超时,则返回一个小于等于 0 的值。可以用此值来确定在等待返回但某一等待条件仍不具备的情况下,是否要再次等待,以及再次等待的时间。

  boolean await(long time, TimeUnit unit) throws InterruptedException;  类似于awaitNanos操作,都可以经过指定的时间后被唤醒,只是时间的方式有所变量,由TimeUnit指定,可以是纳秒,微妙,毫秒,秒,分,等待。此方法在行为上等效于: awaitNanos(unit.toNanos(time)) > 0

  boolean awaitUntil(Date deadline) throws InterruptedException;  类似于awaitNanos(long nanosTimeout)和await(long time, TimeUnit unit),调用此线程会造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态

  void signal();

唤醒一个等待线程,如果所有的线程都在等待此条件,则选择其中的一个唤醒

  void signalAll();  唤醒所有等待线程,如果所有的线程都在等待此条件,则唤醒所有线程。

通过下面两个例子,可以帮助我们理解条件变量的概念

先简单介绍一下ReentrantLock,它是一个实现Lock接口的类,它的语义跟关键字synchronized一样,是一个可重入式锁。它的功能比关键字synchronized强一点。后面会详细介绍ReentrantLock

实现一个缓冲池的代码

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

     }

   }

 }

 

  实现信号量的代码

public abstract class Semaphore {

    private int value = 0;

    private final Lock lock = new ReentrantLock();

    private Condition signal = lock.newCondition();

    public Semaphore() {

    }

 

    public Semaphore(int initial) {

       if (initial >= 0)

           value = initial;

       else

           throw new IllegalArgumentException("initial < 0");

    }

 

    public final synchronized void P() throws InterruptedException {

       lock.lock(); 

        try { 

        value--;

        if(value < 0)

            signal.wait();

        } finally { 

            lock.unlock(); 

        } 

    }

 

    public final synchronized void V() {

       lock.lock();

        try {

           value--;

           if (value <= 0)

              signal.signal();

       } finally {

           lock.unlock();

       }

    }

}

 

ReentrantLock

ReentrantLock 锁意味着什么呢?简单来说,它是一个实现上述Lock接口的类型,它还有一个与锁相关的计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这跟 synchronized 的语义是一样的,如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个 synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized 块时,才释放锁。

下面这段代码说明了ReentrantLock的用法

           Lock lock = new ReentrantLock(); 

           lock.lock(); 

           try {  

             // code

           } 

           finally { 

             lock.unlock();  

           }

 

从上面的代码示例时,可以看到 Lock 和 synchronized 有一点明显的区别:lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个巨大的问题,当问题出现时,您要花费很大力气才有找到源头。然而使用synchronized同步,JVM 会主动锁释放锁。

 

ReentrantLock公平锁(fair)和非公平锁(unfair

ReentrantLock提供了两个构造器,一个带参数和另外一个不带参数:参数是 boolean 值,它表明用户是选用一个公平(fair)锁,还是一个不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则不一定,在不公平种情况下,线程有时可以比先请求锁的其他线程先得到锁。

public ReentrantLock(boolean fair)

public ReentrantLock()

为什么我们不让所有的锁都公平呢?从竞争关系的角度看,公平是好事。从性能上看,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,ReentrantLock选择讲公平参数设置为false而是用非公平锁。 那么同步又如何呢?内置的监控器锁是公平的吗?答案依旧是是不公平的,而且永远都是不公平的。但是没有人抱怨过线程饥渴,因为 JVM 保证了所有线程最终都会得到它们所等候的锁。因此默认情况下ReentrantLock是“不公平”的,下面两组图清晰的展示了性能:公平是有代价的。如果您需要公平,就必须付出代价,请不要把它作为您的默认选择。

那么如何选择ReentrantLock synchronized?  实验证明表现ReentrantLock 无论在哪方面都比 synchronized 好:所有 synchronized 能做的,它都能做,它拥有与 synchronized 相同的内存和并发性语义,还拥有synchronized所没有的特性,在高负荷下还拥有更好的性能。那么,我们是不是应当忘记synchronized,不再使用它呢,或者甚至用ReentrantLock重写我们现有的synchronized代码?实际上,好几本介绍 Java 编程方面的书籍在多线程的章节中就采用了这种方法,完全用Lock来做示例,只把 synchronized 当作历史。但我觉得这是把好事做得太过了。

虽然 ReentrantLock 是个非常好的实现,相对 synchronized 来说,它有一些重要的优势,但是把完全无视synchronized将绝对是个严重的错误。一般来说,除非用户对 Lock 的某个高级特性有明确的需要,或者能够表明synchronized同步已经成为性能瓶颈,否则还是应当继续使用 synchronized。对于java.util.concurrent.lock中的锁来说,synchronized 仍然有一些优势。比如在使用 synchronized 的时候,不能忘记释放锁;在退出 synchronized 块时,JVM 会为您做这件事。您很容易忘记用 finally 块释放锁,这对程序非常有害。另一个原因是因为,当 JVM synchronized 管理锁请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。而且几乎每个开发人员都熟悉 synchronized,它可以运行在 JVM 的所有版本中。在 JDK 5.0 成为标准之前,使用 Lock 类将意味着特性不是每个 JVM 都有的,而且不是每个开发人员熟悉的。

既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单:在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎没有出现过高争用,所以可以把高度争用放在一边。建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设使用 ReentrantLock “性能会更好,他们是高级用户使用的工具。

ReentrantLock加锁与解锁分析

下面我们稍微分析一下ReentrantLock是怎么实现的

经过观察ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer

static abstract class Sync extends AbstractQueuedSynchronizer 

Sync又有两个子类:

final static class NonfairSync extends Sync 

final static class FairSync extends Sync 

 

ReentrantLock默认是一种非公平锁,通过下面的构造函数和lock函数,可以看出锁的真真实现是NonfairSync

    public ReentrantLock() {

        sync = new NonfairSync();

    }

 

    public void lock() {

        sync.lock();

    }

NonfairSync.lock主要做两件事:(1)原子比较并设置计算,如果成功设置,说明锁是空闲的,当前线程获得锁,并把当前线程设置为锁拥有者;(2)否则,调用acquire方法

        final void lock() {

            if (compareAndSetState(0, 1))

                setExclusiveOwnerThread(Thread.currentThread());

            else

                acquire(1);

        }

NonfairSync. Acquire主要做下面几件事:尝试以独占的方式获得锁,如果失败,就把当前线程封装为一个Node,加入到等待队列中;如果加入队列成功,接下来检查当前线程的节点是否应该等待(挂起),如果当前线程所处节点的前一节点的等待状态小于0,则通过LockSupport挂起当前线程;无论线程是否被挂起,或者挂起后被激活,都应该返回当前线程的中断状态,如果处于中断状态,需要中断当前线程

    public final void acquire(int arg) {

        if (!tryAcquire(arg) &&

            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

            selfInterrupt();

    }

 

        protected final boolean tryAcquire(int acquires) {

            return nonfairTryAcquire(acquires);

        }

nonfairTryAcquire代码比较容易理解,尝试进行加锁:(1)如果锁状态空闲(state=0),且通过原子的比较并设置操作,则当前线程获得锁,并把当前线程设置为锁拥有者; (2)如果锁状态空闲,且原子的比较并设置操作失败,那么返回false,说明尝试获得锁失败; (3)否则,检查当前线程与锁拥有者线程是否相等(可重入锁),如果相等,增加锁状态计数,并返回true; (4)如果不是以上情况,说明锁已经被其他的线程持有,直接返回false;

addWaiter操作,主要进行节点操作:创建一个新的节点,并将节点加入到等待队列中。

acquireQueued操作设计的相当完美,从逻辑结构上看,采用无条件的循环语句,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,原因在于parkAndCheckInterrupt会把 当前线程挂起,从而阻塞住线程的调用栈

    final boolean acquireQueued(final Node node, int arg) {

        try {

            boolean interrupted = false;

            for (;;) {

                final Node p = node.predecessor();

                if (p == head && tryAcquire(arg)) {

                    setHead(node);

                    p.next = null; // help GC

                    return interrupted;

                }

                if (shouldParkAfterFailedAcquire(p, node) &&

                    parkAndCheckInterrupt())

                    interrupted = true;

            }

        } catch (RuntimeException ex) {

            cancelAcquire(node);

            throw ex;

        }

    }

private final boolean parkAndCheckInterrupt() {

        LockSupport.park(this);

        return Thread.interrupted();

    }

LockSupport.park最终把线程交给系统内核进行阻塞。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程 的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:通过对前一个节点等待状态分析,来决定是否需要阻塞。

NonfairSync.unlock是一个解锁的过程,调用release函数进行解锁,release函数执行:(1)先判断是否可以解锁,如果tryRelease返回false,则不解锁;(2)如果可以解锁,对头节点的状态进行判断,是否可以唤醒一个线程。

tryRelease语义很明确:如果线程多次锁定,则进行多次释放,直至status==0则真正释放锁,所谓释放锁即设置status为0。

解锁代码相对简单,主要体现在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中:

public final boolean release(int arg) {

        if (tryRelease(arg)) {

            Node h = head;

            if (h != null && h.waitStatus != 0)

                unparkSuccessor(h);

            return true;

        }

        return false;

    }

        protected final boolean tryRelease(int releases) {

            int c = getState() - releases;

            if (Thread.currentThread() != getExclusiveOwnerThread())

                throw new IllegalMonitorStateException();

            boolean free = false;

            if (c == 0) {

                free = true;

                setExclusiveOwnerThread(null);

            }

            setState(c);

            return free;

        }

在这里介绍LockSupport提供的几个操作系统阻塞很唤醒线程函数,ReentrantLock的实现依赖这些函数:

  public static void park()  使当前线程处于等待状态,直到被其它系统调用unpark唤醒。

  public static void parkNanos(long nanos)  使当前线程处于等待状态,直到被其它系统调用unpark唤醒或者超过nanos指定的时间。

  public static void parkUntil(long deadline)  使当前线程处于等待状态,直到被其它系统调用unpark唤醒或者超过nanos指定的时间。

  public static void park(Object blocker)

  public static void parkNanos(Object blocker, long nanos)

  public static void parkUntil(Object blocker, long deadline)

这三个函数的含义跟上面的park函数意思一样,只是多了一个参数blocker,blcoker会记录在Thread的一个parkBlocker属性中,通过jstack命令可以非常方便的监控具体的阻塞对象。

public static void unpark(Thread thread)  唤醒thread指定的线程

 这些lock都可以被其他线程中断唤醒。

ReentrantReadWriteLock

ReentrantLock也实现了Lock接口,它是一种可读写锁。在通常情况下规定任何“读/读”,“写/读”,“写/写”操作都不能同时发生。但是我们知道一个概念:锁是有一定的开销,当并发比较大的时候,锁的开销就非常明显了,所以如果可能的话就尽量少使用锁。

在现实应用中,一种资源往往被很多线程读,而仅仅少数线程写,hbase中很多资源就是这种情况,采用synchronized或者ReentrantLock尽管能够保证资源读写的正确性,但是在多线程环境中,性能已经成为瓶颈,我们需要采用一种新的机制能够在保证资源被正确读写的同时,性能也没有受到大的影响。ReadWriteLock就是这样一种锁,它通常描述的场景是:一个资源能够被多个读线程访问,或者被一个或少数写线程访问,但是不能同时存在读写线程。也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作。

ReentrantReadWriteLock实现了ReadWriteLock接口,使用两把锁来解决问题,一个读锁,一个写锁,线程进入读锁的前提条件:

  1. 没有其他线程的写锁,

  2. 没有写请求

  3. 有写请求,但调用线程和持有锁的线程是同一个(锁的降级:允许从写锁定降级为读锁定,ReentrantReadWriteLock允许先获取写锁,然后获取读锁,最后释放写锁。但是,从读锁升级到写锁是不可能的)

线程进入写锁的前提条件:

  1. 没有其他线程的读锁

  2. 没有其他线程的写锁

下面列出ReadWriteLock接口

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();

}

ReadWriteLock并不是Lock的子接口,而是获取锁的接口:readLock获取读锁;writeLock获取写锁。获得锁后,就可以使用Lock提供的API,使用的方法类似于ReentrantLock:

           lock.lock(); 

           try {  

             // code

           } 

           finally { 

             lock.unlock();  

下面的代码展示了一个简单的缓存系统:

public class Cache {

    private Map<String, Object> cache = new HashMap<String, Object>();

    private ReadWriteLock rwl = new ReentrantReadWriteLock();

   

    public Object getData(String key) {

       rwl.readLock().lock(); // 添加读锁

       Object value = null;

       try {

           value = cache.get(key);        

       } finally {

           rwl.readLock().unlock(); //释放读锁

       }

       return value;

    }

   

    public void putData(String key, Object value){

       rwl.writeLock().lock(); // 添加写锁

       try{

           cache.put(key, value);

       }finally{

           rwl.writeLock().unlock(); //释放写锁

       }

    }

}

在这里就不解释ReentranReadWriteLock内部实现,有兴趣的读者可以阅读它的源代码。我们对ReentrantReadWriteLock做一个总结,它与ReentrantLock一样都是单独的实现,彼此之间没有继承或实现的关系。ReentrantReadWriteLock有如下锁机制的特性了:

  1. 在重入方面,其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock不可能的。WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能。

  2. ReadLock可以被多个线程持有并且排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。

  3. 不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。

  4. WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。

 

posted on 2014-05-13 21:02  cloudkiller  阅读(2994)  评论(0编辑  收藏  举报

导航