锁分类介绍


本博客系列是学习并发编程过程当中的记录总结。因为文章比较多,写的时间也比较散,因此我整理了个目录贴(传送门),方便查阅。html

并发编程系列博客传送门java


乐观锁和悲观锁

锁从宏观上来分类,能够分为悲观锁与乐观锁。注意,这里说的的锁能够是数据库中的锁,也能够是Java等开发语言中的锁技术。悲观锁和乐观锁其实只是一类概念(对某类具体锁的总称),不是某种语言或是某个技术独有的锁技术。数据库

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采起在写时先读出当前版本号,而后加锁操做(比较跟上一次的版本号,若是同样则更新),若是失败则要重复读-比较-写的操做。java中的乐观锁基本都是经过CAS操做实现的,CAS是一种更新的原子操做,比较当前值跟传入值是否同样,同样则更新,不然失败。数据库中的共享锁也是一种乐观锁。编程

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,因此每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中典型的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如ReentrantLock。数据库中的排他锁也是一种悲观锁。多线程

公平锁与非公平锁

根据线程获取锁的抢占机制,锁能够分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间迟早来决定的,也就是最先请求锁的线程将最先获取到锁。而非公平锁则在运行时闯入,也就是先来不必定先得。并发

ReentrantLock提供了公平和非公平锁的实现。框架

  • 公平锁:ReentrantLock pairLock = new ReentrantLock(true)。
  • 非公平锁:ReentrantLock pairLock = new ReentrantLock(false)。若是构造函数不传递参数,则默认是非公平锁。

例如,假设线程A已经持有了锁,这时候线程B请求该锁其将会被挂起。当线程A释放锁后,假如当前有线程C也须要获取该锁,若是采用非公平锁方式,则根据线程调度策略,线程B和线程C二者之一可能获取锁,这时候不须要任何其余干涉,而若是使用公平锁则须要把C挂起,让B获取当前锁。函数

在没有公平性需求的前提下尽可能使用非公平锁,由于公平锁会带来性能开销。性能

独占锁和共享锁

根据锁只能被单个线程持有仍是能被多个线程共同持有,锁能够分为独占锁和共享锁。学习

独占锁保证任什么时候候都只有一个线程能获得锁,ReentrantLock就是以独占方式实现的。共享锁则能够同时由多个线程持有,例如ReadWriteLock读写锁,它容许一个资源能够被多线程同时进行读操做。

独占锁是一种悲观锁,因为每次访问资源都先加上互斥锁,这限制了并发性,由于读操做并不会影响数据的一致性,而独占锁只容许在同一时间由一个线程读取数据,其余线程必须等待当前线程释放锁才能进行读取。

共享锁则是一种乐观锁,它放宽了加锁的条件,容许多个线程同时进行读操做。

可重入锁

当一个线程要获取一个被其余线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它本身已经获取的锁时是否会被阻塞呢?若是不被阻塞,那么咱们说该锁是可重入的,也就是只要该线程获取了该锁,那么能够无限次数(严格来讲是有限次数)地进入被该锁锁住的代码。

public synchronized void helloA(){
...
}

public synchronized void helloB(){
    ...
    helloA();
}

在如上代码中,调用helloB方法前会先获取内置锁,而后打印输出。以后调用helloA方法,在调用前会先去获取内置锁,若是内置锁不是可重入的,那么调用线程将会一直被阻塞。

实际上,synchronized内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪一个线程占用,而后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其余线程再来获取该锁时会发现锁的全部者不是本身而被阻塞挂起。

可是当获取了该锁的线程再次获取锁时发现锁拥有者是本身,就会把计数器值加+1,当释放锁后计数器值-1。当计数器值为0时,锁里面的线程标示被重置为null,这时候被阻塞的线程会被唤醒来竞争获取该锁。

自旋锁

因为Java中的线程是与操做系统中的线程一一对应的,因此当一个线程在获取锁(好比独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又须要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在必定程度上会影响并发性能。自旋锁则是,当前线程在获取锁时,若是发现锁已经被其余线程占有,它不立刻阻塞本身,在不放弃CPU使用权的状况下,屡次尝试获取(默认次数是10,可使用-XX:PreBlockSpinsh参数设置该值),颇有可能在后面几回尝试中其余线程已经释放了锁。若是尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,可是颇有可能这些CPU时间白白浪费了。

参考

  • 《Java并发编程之美》
相关文章
相关标签/搜索