概要

本文介绍Lock接口的实现类,其中一个常用类ReetrantLock与synchronized的对比。

Lock锁

JAVA5中引入了新的锁机制—java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作。Lock接口有三个实现类:ReetrantLock、ReetrantReadWriteLock.ReadLock、ReetrantReadWriteLock.WriteLock,即重入锁、读锁和写锁。Lock必须被显式创建、锁定和释放。为了保证锁最终一定会被释放,要把互斥区放在try语句块内,并在finally语句块中释放锁,尤其当有return语句时,return语句必须放在try子句中,以确保unlock()不会过早发生。

Lock与synchronized区别

  1. 原始构成
    synchronized是关键字属于JVM层面
    monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步块或者方法中才能掉wait/notify等方法)。
    当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试获得锁,如果获得锁那么锁计数+1(为什么会加一呢,因为它是一个可重入锁,所以需要用这个锁计数判断锁的情况),如果没有获得锁,那么阻塞。
    当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁。synchronized锁释放有两种机制,一种就是执行完释放;另外一种就是发送异常,虚拟机释放。

    Lock 是具体类(java.util.concurrent.locks.Lock)是API

  2. 使用方法
    synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用。
    ReentrantLock则需要用户去手动释放锁,若没有主动释放锁就有可能导致线程死锁现象,需要lock()和unlock()方法配合try/finally语句块来完成

  3. 等待是否可中断
    synchronized不可中断,除非抛异常或者正产运行完成。
    ReentrantLock可以中断,1.设置超时方法tryLock(long timout,TimUnit unit) ; 2.lockInterruptbly()放代码块中,调用interrupt()方法可中断。
  4. 加锁是否公平
    synchronized非公平锁
    ReentrantLock默认非公平锁,传入true为公平锁。
  5. 绑定多个条件Condition
    synchronized没有
    ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个,要么唤醒全部线程。
  6. 底层锁的类型
    synchronized 底层使用的是悲观锁,Lock底层是CAS乐观锁
  7. synchronized JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,增加了从偏向锁到轻量级锁再到重量级锁的过度。

ReetrantLock与synchronized比较

性能比较

在JDK1.5中,synchronized是重量级操作是性能低效的,它对性能最大的影响是挂起线程和恢复线程的操作都要转入内核态中完成,这些操作给系统带来了很大的压力。相比之下使用Lock对象性能更高一些。到了JDK1.6以后synchronized加入了很多优化措施,有自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等等,性能提升了很多。

下面浅析两种锁机制的底层实现策略。
synchronized采用的
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因而这种同步又称为阻塞同步,它属于一种悲观的并发策略,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁上下文切换导致效率很低。
ReetrantLock采用的
ReetrantLock使用了基于冲突检测的乐观并发策略,通俗讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就在井陉其他的补偿措施(最常见的补偿措施就是不断地重拾,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因为这种同步被称为非阻塞同步。
在乐观的并发策略中,需要操作和冲突检测这两个步骤具备原子性,它靠硬件指令来保证,这里用到的是CAS操作。研究ReentrantLock源码发现其中一个比较重要的获得锁的一个方法时compareAndSetState,这里其实就是调用了CPU提供的特殊指令。现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet()就用这些替代了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或者挂起。

用途对比

基本语法上,ReentrantLock与synchronized很相似,他们都具备一样的线程冲入特性,只是代码写法上有些区别而已,一个表现在API层面上的互斥锁(Lock),一个表现为原生语法层面的互斥锁(synchronized)。ReentrantLock相对synchronized而言还是增加了一些高级功能,主要有以下三项:

  • 等待可中断:ReentrantLock当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间上的同步块很有帮助。而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的。
  • 可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这一点,在释放锁时,任何一个等待锁的线程都有机会获得锁。ReentrantLock默认情况是非公平锁,但是可以通过构造方法ReentrantLock(true)来要求使用公平锁。而synchronized只有非公平锁。
  • 锁可以绑定多个条件:ReentrantLock对象可以同时绑定多个condition对象(又称为条件变量或者条件队列),而在synchronized中,锁对象的wait()和notify()和notifyAll()方法可以实现一个隐含条件,但如果要和多于一个条件关联的时候,就不得不额外的添加一个锁。而ReentrantLock只需要多次调用newCondition()方法即可。而且还可以通过绑定condition 对象来判断当前哪个线程通知的是哪些线程。

    锁的种类

    ReentrantLock有两种锁:忽略中断锁和相应中断锁。忽略中断锁与synchronized实现的互斥锁一样,不能相应中断,而响应中断锁可以响应中断。
    使用方法:
  1. ReentrantLock lock = new ReentrantLock();
  2. lock.lockInterruptibly();//获取响应中断锁
  3. try{
  4. //更新对象的状态
  5. //捕获异常
  6. //如果有return语句放在这里
  7. }finally{
  8. lock.unclock();//锁必须放在finally块中释放
  9. }

条件变量实现线程间协作(实现生产者消费者模型)

synchronized可以配合使用wait()和notify()或者notifyAll()方法来实现线程间的协作。
ReentrantLock锁配合Condition对象上的await()和signal()或者signalAll()方法来实现线程间协作。在ReentrantLock对象上newCondition()可以得到一个Condition对象,可以通过在Condition上调用await()方法来挂起一个任务或者线程,通过Condition上调用signal()方法来通知任务,从而唤醒一个任务。
如果使用了公平锁,则Condition关联的所有任务将以FIFO队列显式获取挂起的任务。相比notifyAll()来说signalAll()是更安全的方式。

读写锁

synchronized获取的互斥锁不仅互斥读写操作、写写操作,还互斥读读操作。Lock接口提供了读写锁类,可以保证读写操作不互斥。

  1. ReedWriteLock rwl = new ReentrantReedWriteLock();
  2. rwl.writeLock().lock();//获取写锁
  3. rwl.readLock().lock();//获取读锁