lesson3:java的锁机制原理和分析

jdk1.5之前,我们对代码加锁(实际是对象加锁),都是采用Synchronized关键字来处理,jdk1.5及以后的版本中,并发编程大师Doug Lea在concurrrent包中提供了Lock机制。两种机制在性能上目前的jdk版本都差不多,Synchronized作为jvm的关键字,是在jvm层面实现的锁机制,而Lock机制是在java语言这个级别实现的锁机制,其实锁的核心原理都是为某个对象加锁。本文中将以Lock机制的源代码来分析锁机制的原理和实现,后面的demo代码也将按照Lock锁来展开。

demo源码:https://github.com/mantuliu/javaAdvance 中的类 Lesson3CoarseGrainedLock和Lesson3FinedGrainedLock

下面的斜体段落是java api中对Lock的描述:

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock 的读取锁。

synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用  “hand-over-hand” 或 “chain locking”:获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。

随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句: 

 Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); } 

锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。

Lock 实现提供了使用 synchronized 方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 (tryLock())、一个获取可中断锁的尝试 (lockInterruptibly()) 和一个获取超时失效锁的尝试 (tryLock(long, TimeUnit))。

Lock 类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。

注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock() 方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。

除非另有说明,否则为任何参数传递 null 值都将导致抛出 NullPointerException

内存同步

所有 Lock 实现都必须 实施与内置监视器锁提供的相同内存同步语义,如 The Java Language Specification, Third Edition (17.4 Memory Model) 中所描述的: 

  • 成功的 lock 操作与成功的 Lock 操作具有同样的内存同步效应。 
  • 成功的 unlock 操作与成功的 Unlock 操作具有同样的内存同步效应。  

不成功的锁定与取消锁定操作以及重入锁定/取消锁定操作都不需要任何内存同步效果。

实现注意事项

三种形式的锁获取(可中断、不可中断和定时)在其性能特征、排序保证或其他实现质量上可能会有所不同。而且,对于给定的 Lock 类,可能没有中断正在进行的 锁获取的能力。因此,并不要求实现为所有三种形式的锁获取定义相同的保证或语义,也不要求其支持中断正在进行的锁获取。实现必需清楚地对每个锁定方法所提供的语义和保证进行记录。还必须遵守此接口中定义的中断语义,以便为锁获取中断提供支持:完全支持中断,或仅在进入方法时支持中断。

由于中断通常意味着取消,而通常又很少进行中断检查,因此,相对于普通方法返回而言,实现可能更喜欢响应某个中断。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。

      Lock接口的实现类有:ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock;本文将以ReentrantLock的源码来分析内部实现原理:

a.lock()方法

    public void lock() {
        sync.lock();//调用sync对象的lock()方法
    }
下面看看sync对象的创建过程及它的lock()方法
    public ReentrantLock() {
        sync = new NonfairSync();//我们发现实现sync对象的类是NonfairSync
    }

    static final class NonfairSync extends Sync {

        private static final long serialVersionUID = 7316153563782823691L;

        /**

         * Performs lock.  Try immediate barge, backing up to normal

         * acquire on failure.

         */

        final void lock() {

        /*调用unsafe的compareAndSwapInt方法(原子操作),如果Lock对象存储的对象值是0(与第一个参数值相同),则将此值设置为1(与第二个参数值相同),反之,则返回失败*/

            if (compareAndSetState(0, 1))

                setExclusiveOwnerThread(Thread.currentThread());//设置当前线程为锁的持有者

            else

                acquire(1);//当前线程等待其它线程释放锁

        }

    }

 b.tryLock()方法:仅在调用时锁为空闲状态才获取该锁

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);//尝试获取锁
    }
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取到当前线程
            int c = getState();//获取锁的状态
            if (c == 0) {//0表示锁未被占用
                if (compareAndSetState(0, acquires)) {//调用unsafe的compareAndSwapInt方法(原子操作),标识此锁为已经占有状态
                    setExclusiveOwnerThread(current);//设置当前线程占有锁
                    return true;//标识获取锁成功
                }
            }
            else if (current == getExclusiveOwnerThread()) {//如果当前线程在之前已经持有过此锁,并且没有释放
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//标识同一个线程占用此锁几次
                return true;
            }
            return false;
        }

 c.tryLock(long time, TimeUnit unit)方法,介于lock()和tryLock()方法之间,如果锁已经被其它线程占用,则会等待一段时间

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||   //调用NonfairSynctryAcquire(方法),与上面的tryLock()方法调用的nonfairTryAcquire()一致
            doAcquireNanos(arg, nanosTimeout);
    }
    private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
        long lastTime = System.nanoTime();//按纳秒返回当前时间
        final Node node = addWaiter(Node.EXCLUSIVE);//创建一个节点并入队列来等待锁
        boolean failed = true;
        try {
            for (;;) {//一直循环
                final Node p = node.predecessor();//返回当前节点的前一个等待节点
                if (p == head && tryAcquire(arg)) {//如果前一个节点已经是head节点,则试图去获取锁,如果获取到了,则执行下面的代码
                    setHead(node);//设置当前节点为队列中的头节点
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                if (nanosTimeout <= 0)
                    return false;//超时返回失败
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                long now = System.nanoTime();
                nanosTimeout -= now - lastTime;
                lastTime = now;
                if (Thread.interrupted())//线程被中断了
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

d.lockInterruptibly()方法,可以相应线程中断的去获取锁,如果线程被中断了,则抛出异常

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//调用acquireInterruptibly
    }

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())//如果线程被中断了,则抛出异常
            throw new InterruptedException();
        if (!tryAcquire(arg))////调用NonfairSync的tryAcquire(方法),与上面的tryLock()方法调用的nonfairTryAcquire()一致
            doAcquireInterruptibly(arg);
    }
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) && //检测线程是否被中断
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 e.unlock()方法,释放锁,通常只有持有锁的线程才能释放锁成功

    public void unlock() {
        sync.release(1);
    }

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

 上面的代码分析了ReentrantLock的非公平锁的实现,在实现的使用过程中,锁和线程是息息相关的,如果我们把线程比作高速公路上的车道,那么行驶在高速公路上的车辆就是我们实际的代码(操作),因为路况或其它原因,各条车道需要并线,为防止意外发生,在一定时间内只能有一条车道或几条路上的车可以通过(代码上了锁),为保证通行时间,有两点需要注意:1是车辆通过的速度要快(线程占用锁的时间要少),2是在高速公路的车道数足够多的情况下,要尽可能的将车道分成多个小组,各个小组内部交替通行,小组与小组之间不影响。总结起来,在使用锁的情况下,要想对系统的性能影响足够小:1.锁的粒度要足够小,尽量减少同时使用同一把锁的线程数量;2.加锁的线程执行时间要足够快。下面的demo从粒度的角度展示了锁对于性能的影响:

先来看看粗粒度的锁,100个线程锁同一个资源,每个线程执行1秒钟的情况:

package com.mantu.advance;

import java.util.concurrent.locks.ReentrantLock;


/**
 * blog http://www.cnblogs.com/mantu/
 * github https://github.com/mantuliu/
 * @author mantu
 *
 */
public class Lesson3CoarseGrainedLock implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public static void main(String args[]){
        for(int i=0;i<100;i++){
            Thread thread =new Thread(new Lesson3CoarseGrainedLock());
            thread.start();
        }
    }

    @Override
    public void run() {
        try {
            lock.lock();//100个线程使用同一个锁,每个时刻只能有一个线程执行
            Thread.sleep(1000);
        } 
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally{
            lock.unlock();
            System.out.println("线程"+Thread.currentThread().getId()+"执行完毕");
        }
    }
}

 再来看看细粒度锁的demo,每10个线程分为一个组,组内的线程才会存在资源竞争的情况,执行完代码后,会发现效率比上一个demo提升了非常多

package com.mantu.advance;

import java.util.HashMap;
import java.util.concurrent.locks.ReentrantLock;


/**
 * blog http://www.cnblogs.com/mantu/
 * github https://github.com/mantuliu/
 * @author mantu
 *
 */
public class Lesson3FinedGrainedLock implements Runnable{
    public static HashMap<Integer,ReentrantLock> lockMap = new HashMap<Integer,ReentrantLock>();
    public static void main(String args[]){
        init();
        for(int i=0;i<100;i++){
            Thread thread =new Thread(new Lesson3FinedGrainedLock());
            thread.start();
        }
    }
    public static void init(){
        for(int i=0;i<10;i++){
            lockMap.put(new Integer(i),new ReentrantLock());
        }
    }
    @Override
    public void run() {
        ReentrantLock lock = Lesson3FinedGrainedLock.lockMap.get((int)(Thread.currentThread().getId())%10);
        try {
            lock.lock();//细粒度的锁,效率提高非常多
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally{
            lock.unlock();
            System.out.println("线程"+Thread.currentThread().getId()+"执行完毕");
        }       
    }
}

 

    原文作者:mantu
    原文地址: http://www.cnblogs.com/mantu/p/5761453.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞