JavaGuide之各种锁

背景

多线程的本质其实就是各种线程对静态资源的合理使用问题,怎么能最大限度发挥CPU性能执行任务。锁这种概念就是解决多个线程抢占资源问题的办法,当A线程使用资源时起时不希望B线程也去使用,因为如果同时操作静态资源会导致读写不一致等问题。

各种锁的分类

以下的分类都是站在不同角度对锁这种概念的分类,相互之间并没有必然的关系。

乐观锁和悲观锁

这种分类是针对线程要不要锁住同步资源来划分的。

  • 悲观锁:悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,取保数据不会被别的线程修改。Java中synchronized和Lock都是悲观锁,在操作资源前加锁,在释放资源后解锁。

  • 乐观锁:乐观锁则认为自己在使用数据的时候不会有其它线程去修改数据,所以不会添加锁,只是在要更新数据的时候去判断之前又没有别的线程更新了这个数据。如果这个数据没有被更新则当前线程将自己修改的数据成功写入,如果数据已经被其它线程更新,则根据不同的实现方式执行不同操作(报错或者自动重试)。Java中乐观锁是通过使用无锁编程来实现,最常见的是CAS算法,原子类就是典型的乐观锁。

    CAS算法其实就是CompareAndSet()方法,是一种解决线程之间抢占资源导致线程不安全的方法。它是一种无锁化(不是显示的加锁,例如sycn,lock等)来实现多线程之间变量的同步保障线程安全的算法。后面会单独讲CAS。

自旋锁和适应性自旋锁

首先介绍自旋锁的概念

阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间,如果同步代码块中的内容过于简单,状态转换耗费的时间又可能比用户代码执行的时间还要长。在许多场景中,同步资源被某个线程锁定的时间很短,为了这一小段时间去切换线程,线程挂起和恢复线程的花费可能会让性能下降很多。如果物理及其有多个处理器,能够让两个或者两个以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU执行时间(这里会涉及线程的一些方法,例如sleep,wait,yield等),看看这个资源是不是很快就会被释放。而为了让请求锁的线程“等一等”,我们需要让当前线程自旋,如果在自旋完成后,持有同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞或者重新被唤醒之类的,而是直接获取资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间短,自旋等待的效果就很好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源。所以,自旋等待的时间必须要有限制,如果自旋超过了限定的次数(默认是10次,可以更改)没有成功获取到锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

自适应自旋锁其实就是自旋的时间(次数,循环次数)不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者来决定(怎么决定)。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋邓艾持续相对更长的时间,如果对于某个锁,自旋很少成功获得过,那么以后再尝试获取这个锁时将可能不自旋,直接阻塞线程,避免浪费处理器资源。

在自旋锁中,另有三种常见的锁形式:TicketLock,CLHlock和MSlock。(TODO查查资料)

无锁/偏向锁/轻量级锁/重量级锁

这四种锁是指锁的状态,专门针对synchronized的。(synchronized在JDK 1.6之后有被优化)