Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

https://segmentfault.com/a/1190000017766364java


Java 中15种锁的介绍
在读不少并发文章中,会说起各类各样锁如公平锁,乐观锁等等,这篇文章介绍各类锁的分类。介绍的内容以下:web

公平锁 / 非公平锁
可重入锁 / 不可重入锁
独享锁 / 共享锁
互斥锁 / 读写锁
乐观锁 / 悲观锁
分段锁
偏向锁 / 轻量级锁 / 重量级锁
自旋锁
上面是不少锁的名词,这些分类并非全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每一个锁的名词进行必定的解释。算法

公平锁 / 非公平锁
公平锁数据库

公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁编程

非公平锁是指多个线程获取锁的顺序并非按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会形成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,经过构造函数指定该锁是不是公平锁,默认是非公平锁。非公平锁的优势在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。因为其并不像ReentrantLock是经过AQS的来实现线程调度,因此并无任何办法使其变成公平锁。segmentfault

可重入锁 / 不可重入锁
可重入锁数组

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁以后,在内层仍然可使用,而且不发生死锁(前提得是同一个对象或者class),这样的锁就叫作可重入锁。ReentrantLock和synchronized都是可重入锁多线程

synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}并发

synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特色,若是不是可重入锁的话,setB可能不会被当前线程执行,可能形成死锁。svg

不可重入锁

不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,代码以下

import java.util.concurrent.atomic.AtomicReference;

public class UnreentrantLock {

private AtomicReference<Thread> owner = new AtomicReference<Thread>();

public void lock() {
    Thread current = Thread.currentThread();
    //这句是很经典的“自旋”语法,AtomicInteger中也有
    for (;;) {
        if (!owner.compareAndSet(null, current)) {
            return;
        }
    }
}

public void unlock() {
    Thread current = Thread.currentThread();
    owner.compareAndSet(current, null);
}

}
代码也比较简单,使用原子引用来存放线程,同一线程两次调用lock()方法,若是不执行unlock()释放锁的话,第二次调用自旋的时候就会产生死锁,这个锁就不是可重入的,而实际上同一个线程没必要每次都去释放锁再来获取锁,这样的调度切换是很耗资源的。

把它变成一个可重入锁:

import java.util.concurrent.atomic.AtomicReference;

public class UnreentrantLock {

private AtomicReference<Thread> owner = new AtomicReference<Thread>();
private int state = 0;

public void lock() {
    Thread current = Thread.currentThread();
    if (current == owner.get()) {
        state++;
        return;
    }
    //这句是很经典的“自旋”式语法,AtomicInteger中也有
    for (;;) {
        if (!owner.compareAndSet(null, current)) {
            return;
        }
    }
}

public void unlock() {
    Thread current = Thread.currentThread();
    if (current == owner.get()) {
        if (state != 0) {
            state--;
        } else {
            owner.compareAndSet(current, null);
        }
    }
}

}
在执行每次操做以前,判断当前锁持有者是不是当前对象,采用state计数,不用每次去释放锁。

ReentrantLock中可重入锁实现

这里看非公平锁的锁获取方法:

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
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;
}
在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操做,这样既提高了效率,又避免了死锁。

独享锁 / 共享锁
独享锁和共享锁在你去读C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就会发现,它俩一个是独享一个是共享锁。

独享锁:该锁每一次只能被一个线程所持有。
共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是能够被共享的,可是它的写锁确每次只能被独占。
另外读锁的共享可保证并发读是很是高效的,可是读写和写写,写读都是互斥的。

独享锁与共享锁也是经过AQS来实现的,经过实现不一样的方法,来实现独享或者共享。
对于Synchronized而言,固然是独享锁。

互斥锁 / 读写锁
互斥锁

在访问共享资源以前对进行加锁操做,在访问完成以后进行解锁操做。 加锁后,任何其余试图再次加锁的线程会被阻塞,直到当前进程解锁。

若是解锁时有一个以上的线程阻塞,那么全部该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操做,那么其余的线程又会进入等待。 在这种方式下,只有一个线程可以访问被互斥锁保护的资源

读写锁

读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。

读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态

读写锁在Java中的具体实现就是ReadWriteLock

一次只有一个线程能够占有写模式的读写锁,可是多个线程能够同时占有读模式的读写锁。
只有一个线程能够占有写状态的锁,但能够有多个线程同时占有读状态锁,这也是它能够实现高并发的缘由。当其处于写状态锁下,任何想要尝试得到锁的线程都会被阻塞,直到写状态锁被释放;若是是处于读状态锁下,容许其它线程得到它的读状态锁,可是不容许得到它的写状态锁,直到全部线程的读状态锁被释放;为了不想要尝试写操做的线程一直得不到写状态锁,当读写锁感知到有线程想要得到写状态锁时,便会阻塞其后全部想要得到读状态锁的线程。因此读写锁很是适合资源的读操做远多于写操做的状况。

乐观锁 / 悲观锁
悲观锁

老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库提供的相似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

分段锁
分段锁实际上是一种锁的设计,并非具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是经过分段锁的形式来实现高效的并发操做。

并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提高多并发程序性能的重要手段之一。

在并发程序中,串行操做是会下降可伸缩性,而且上下文切换也会减低性能。在锁上发生竞争时将通水致使这两种问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式—-每次只能有一个线程能访问它。因此对于可伸缩性来讲最大的威胁就是独占锁。

咱们通常有三种方式下降锁的竞争程度:
一、减小锁的持有时间
二、下降锁的请求频率
三、使用带有协调机制的独占锁,这些机制容许更高的并发性。

在某些状况下咱们能够将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。

其实说的简单一点就是:

容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不一样数据段的数据时,线程间就不会存在锁竞争,从而能够有效的提升并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分红一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问。

好比:在ConcurrentHashMap中使用了一个包含16个锁的数组,每一个锁保护全部散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字可以均匀的分部,那么这大约能使对锁的请求减小到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。

偏向锁 / 轻量级锁 / 重量级锁
锁的状态:

无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态
锁的状态是经过对象监视器在对象头中的字段来代表的。
四种状态会随着竞争的状况逐渐升级,并且是不可逆的过程,即不可降级。
这四种状态都不是Java语言中的锁,而是Jvm为了提升锁的获取与释放效率而作的优化(使用synchronized时)。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。下降获取锁的代价。
轻量级

轻量级锁是指当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,提升性能。
重量级锁

重量级锁是指当锁为轻量级锁的时候,另外一个线程虽然是自旋,但自旋不会一直持续下去,当自旋必定次数的时候,尚未获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能下降。
自旋锁
咱们知道CAS算法是乐观锁的一种实现方式,CAS算法中又涉及到自旋锁,因此这里给你们讲一下什么是自旋锁。

简单回顾一下CAS算法
CAS是英文单词Compare and Swap(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的状况下实现多线程之间的变量同步,也就是在没有线程被阻塞的状况下实现变量的同步,因此也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操做数

须要读写的内存值 V
进行比较的值 A
拟写入的新值 B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B,不然不会执行任何操做。通常状况下是一个自旋操做,即不断的重试。

什么是自旋锁?
自旋锁(spinlock):是指当一个线程在获取锁的时候,若是锁已经被其它线程获取,那么该线程将循环等待,而后不断的判断锁是否可以被成功获取,直到获取到锁才会退出循环。

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较相似,它们都是为了解决对某项资源的互斥使用。不管是互斥锁,仍是自旋锁,在任什么时候刻,最多只能有一个保持者,也就说,在任什么时候刻最多只能有一个执行单元得到锁。可是二者在调度机制上略有不一样。对于互斥锁,若是资源已经被占用,资源申请者只能进入睡眠状态。可是自旋锁不会引发调用者睡眠,若是自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是所以而得名。

Java如何实现自旋锁?
下面是个简单的例子:

public class SpinLock {
private AtomicReference cas = new AtomicReference();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
lock()方法利用的CAS,当第一个线程A获取锁的时候,可以成功获取到,不会进入while循环,若是此时线程A没有释放锁,另外一个线程B又来获取锁,此时因为不知足CAS,因此就会进入while循环,不断判断是否知足CAS,直到A线程调用unlock方法释放了该锁。

自旋锁存在的问题
一、若是某个线程持有锁的时间过长,就会致使其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会形成CPU使用率极高。
二、上面Java实现的自旋锁不是公平的,即没法知足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优势
一、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减小了没必要要的上下文切换,执行速度快
二、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候须要从内核态恢复,须要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会致使系统在用户态与内核态之间来回切换,严重影响锁的性能)

可重入的自旋锁和不可重入的自旋锁
文章开始的时候的那段代码,仔细分析一下就能够看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放以前又一次从新获取该锁,第二次就不能成功获取到。因为不知足CAS,因此第二次获取会进入while循环等待,而若是是可重入锁,第二次也是应该可以成功获取到的。

并且,即便第二次可以成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

为了实现可重入锁,咱们须要引入一个计数器,用来记录获取锁的线程数。

public class ReentrantSpinLock { private AtomicReference cas = new AtomicReference(); private int count; public void lock() { Thread current = Thread.currentThread(); if (current == cas.get()) { // 若是当前线程已经获取到了锁,线程数增长一,而后返回 count++; return; } // 若是没获取到锁,则经过CAS自旋 while (!cas.compareAndSet(null, current)) { // DO nothing } } public void unlock() { Thread cur = Thread.currentThread(); if (cur == cas.get()) { if (count > 0) {// 若是大于0,表示当前线程屡次获取了该锁,释放锁经过count减一来模拟 count–; } else {// 若是count==0,能够将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。 cas.compareAndSet(cur, null); } } } } 自旋锁与互斥锁 自旋锁与互斥锁都是为了实现保护资源共享的机制。 不管是自旋锁仍是互斥锁,在任意时刻,都最多只能有一个保持者。 获取互斥锁的线程,若是锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。 自旋锁总结 自旋锁:线程获取锁的时候,若是锁被其余线程持有,则当前线程将循环等待,直到获取到锁。 自旋锁等待期间,线程的状态不会改变,线程一直是用户态而且是活动的(active)。 自旋锁若是持有锁的时间太长,则会致使其它等待获取锁的线程耗尽CPU。 自旋锁自己没法保证公平性,同时也没法保证可重入性。 基于自旋锁,能够实现具有公平性和可重入性质的锁。