zoukankan      html  css  js  c++  java
  • 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()+"执行完毕");
            }       
        }
    }
    
  • 相关阅读:
    iOS: ARC和非ARC下使用Block属性的问题
    HTTP POST GET 本质区别详解
    php输出json,需要嵌套数组和对象问题
    正则验证在线生成代码
    笔记
    按钮删除
    sudo dpkg-reconfigure phpmyadmin命令,重新配置一遍phpmyadmin
    Ubuntu 16.04 安装 Apache, MySQL, PHP7
    ubuntu18.04安装chrome浏览器
    初三知识点
  • 原文地址:https://www.cnblogs.com/mantu/p/5761453.html
Copyright © 2011-2022 走看看