Java中的锁总结

  Seves

    一、前言
      Java中锁的是保证线程安全的重要手段,也是java并发编程的基础,本文是笔者对《java并发编程的艺术》一书中Java锁相关的重点内容的总结和分析。
    二、synchronized
      关键字synchronized可以修饰方法或者以同步块的形式来使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,由JVM具体实现。
      任意线程对Object(Object受synchronized的保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当Object的前驱(获得锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
    三、Lock接口
      在Lock接口出现之前,Java是靠synchronized关键字实现锁功能的,在JavaSE5之后,并发包中新增了Lock接口(以及相关的实现类)用来实现锁功能,它提供了与synchronzied关键字类似的同步功能,只是需要在使用时显示的释放和获取锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取和释放的可操作性、可中断的获取锁以及超时获取锁等多种Synchronized关键字所不具备的同步特性。
      Lock接口提供的synchronized关键字不具备的主要特性:
      1.尝试非阻塞地获取锁。当前线程尝试获取锁,如果这一时刻没有被其他线程获取到,则成功获取并持有锁。
      2.能够中断地获取锁。与synchronized不同,获取到锁的线程能够响应中断。当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。
      3.超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取到锁,则返回。
      API:
    1c053fbfdc6248d6a69c2b18e7459498-lock1.png

    四、队列同步器
      队列同步器(AbstractQueuredSynchronizer),是用来构建锁或者其他同步组件的基础框架,他使用了一个int成员变量(volatile)表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
      同步器的设计是基于模版方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,岁以后将同步器组合在自定义同步组件实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
      重写时通过基于CAS的getState(),setState(int newState),compareAndSetState(int expect,int update)来访问或者修改同步状态。
      可重写的方法如下:
      1.protected boolean tryAcquire(int arg)。独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
      2.protected boolean tryRelease(int arg)。独占是释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
      3.protected int tryAcquireShared(int arg)。共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败。
      4.protected boolean tryReleaseShared(int arg)。共享式释放同步状态。
      5.protected boolean isHeldExclusively()。当前同步器是否在独占模式下被线程占用,一般该方法便是是否被当前线程所独占。
      同步器提供的模板方法:
    e62750844f974c3aa75041b7e3b01eb4-lock2.png
      同步器提供的模板方法分为三类:独占式获取和释放同步状态、共享式获取和释放同步状态和查询同步队列中等待线程情况。
      同步器依赖内部的同步队列(一个FIFO的双向队列)来完成同步状态的管理,当前线程或者同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
    五、重入锁
      重入锁ReentrantLock,表示支持重进入的锁,他表示该锁能够支持一个线程对资源的反复加锁。除此之外,该锁还支持获取锁时的公平和非公平性选择。
      关于重入可以拿synchronized来分析,用synchronized修饰的递归方法在执行线程获取锁之后仍然能连续多次的获得该锁。关于ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取带锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
      关于公平性问题,如果在绝对的时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。非公平性锁可能使线程饥饿,但是它被设定为默认实现,因为它极少的线程切换,保证了其更大的吞吐量。
    六、读写锁
      Mutex(基于AQS实现的独占锁)、ReentrantLock、包括Synchronized关键字都是排它锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读和其他写线程均被阻塞。读写锁维护了一对锁(一个读锁和一个写锁),通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大提升,java并发包提供读写锁的实现是ReentrantReadWriteLock。
      它具有如下特性:
      1.公平性选择。支持非公平性(默认)和公平锁获取方式,吞吐量还是非公平由于公平。
      2.重进入。
      3.锁降级。遵循获取写锁,获取读锁再释放写锁的次序,写锁能降级为读锁。
      读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。ReentrantLock中自定义同步器的实现的同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁的关键。读写锁将变量切分成了两个部分,高16位表示都,低16位表示写,读写锁通过位运算确定各自的状态。写状态等于S&0x0000FFFF(将高16位抹去),读状态等于S>>>16。当写状态增加时,等于S+1,当读状态增加1时,等于S+(1«16),也就是S+0x00010000。("»“表示按照二进制把数字右移制定和数位,高位如符号位为正补零,符号位负补一,地位直接移除。”>>>"表示按照二级制把数字右移指定数位,高位直接补零,低位移除。)
      写锁的获取与释放:
      写锁是一个支持重进入的排它锁。如果当前线程以及获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁以及被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
      写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已经被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
    读锁的获取与释放:
      读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他线程访问(或者写状态为0)时,该锁总会被成功的获取,而如果当前线程在获取读锁时写锁已经被其他线程获取,则进入等待状态。
      读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
      锁降级:
      锁降级是指把持住当前的写锁,在获取到读锁,然后释放写锁的过程。
      锁降级中的读锁是否有必要?答案是必要的。为了保护数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记做线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
    七、LockSupport工具
      LockSupport类定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而  LockSupport也成了构建同步组件的基础工具。
      LockSupport提供的阻塞和唤醒方法如下:
      1.void park()。阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回。
      2.void parkNanos(long nanos)。阻塞当前线程,最长不超过nanos纳秒。返回条件上在park()的基础上增加了超时返回。
      3.void parkUntil(long deadline)。阻塞当前线程,直到deadline时间(从1970年开始到deadline时间的毫秒数)。
      4.void unpark(Thread thread)。唤醒处于阻塞状态的线程thread。
      5.park(object blocker)。
      6.parkNanos(Object blocker,long nanos)。
      7.parkUntile(Object blocker,long deadline)。
      后面的这三个方法增加了一个Object参数blocker,这个参数是为了标识当前线程在等待的对象,该对象主要用户问题排查和系统监控(在dump线程的时候会传递给开发人员更多的信息)。
    八、Condition接口
      任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是两者在使用方式以及功能特性上还是有差别的。
    2c3c06c92f744c2cb777872c5d313953-lock3.png
      Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的。
    在调用condition对象的await()方法后,当前线程会释放锁并在此等待,而其他线程调用condition对象的signal()方法通知当前线程后,当前线程才从await()方法返回,并且在返回前以及获取了锁。
      等待队列:
      等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
      等待:
      调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。相当于同步队列的首节点(获取了锁的节点)移动到了Condition的等待队列中。
      通知:
      调用Condition的signal()方法,将会唤醒在等待队列中等待最长的节点(首节点),在唤醒之前,会将节点移到同步队列中,然后再使用LockSupport唤醒该节点的进程,被唤醒后的线程,将从await()方法中退出,进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。

    CSDN地址:https://blog.csdn.net/qq_36236890

    微信订阅号:
    2bf252d0283b491fb0b54d3a4d20302d-weixin.png

    手在键盘敲很轻,我写的代码很小心。
    1,336