锁

引言
这些分类并不全指锁的状态,有的指锁的特性,有的指锁的设计
公平锁
? 多个线程按照申请锁的顺序获取锁
非公平锁
? 多个线程不按照申请锁的顺序获取锁
? 有可能后申请的线程比先申请的优先获得锁。有可能造成优先级反转h或饥饿现象
? 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
? 对于Synchronized而言,也是一种非公平锁。其并不像ReentranLock是通过AQS来实现线程调度,因而并没有任何办法使其变成公平锁。
可重入锁
? 广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,内层仍然可以使用,并且不发生死锁(前提是同一个对象或者class),这样的锁叫做可重入锁。
? ReentrantLock或synchronized都是可重入锁
不可重入锁
? 不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。
? 独享锁和共享锁在C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就会发现,他俩一个是独享锁一个是共享锁。
独享锁
? 该锁每一次只能被一个线程持有
共享锁
? 可被多个线程共同持有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以共享的,但写锁每次只能被独占
读锁的共享可保证并发读是非常高效的,但附带写的操作都是互斥的,例如读写,谢谢,写读。共享锁和独占锁通过AQS来实现,通过实现不同的方法,来实现独享或共享。对于Synchronized而言,当然是独享锁

互斥锁
? 在访问共享资源之前对其进行加锁,在访问完成之后进行解锁操作。加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前线程解锁。
? 解锁时有一个以上的线程在阻塞,那么该锁上的所有线程都被编程为就绪状态,第一个变为就绪状态的线程又执行加锁操作,那么其他线程又会进入等待。此方式下,只有一个线程能够访问被互斥锁保护的资源
读写锁
? 读写锁既是互斥锁,也是共享锁。read模式是共享锁,write模式是互斥锁(排他锁)
? 读写锁的三种状态:读写锁,写加锁和不加锁
? 在Java中的具体实现:ReadWriteLock
? 只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态的锁,这也是它实现高并发的原因。当其处在写状态下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其他线程获取其读状态锁,但不允许获取它的写状态锁直到所有线程的读都释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读状态锁感知到有线程想要获得写状态锁时,会阻塞其后想要获取读状态锁的线程,所以读写锁非常适合在 读 远多于 写操作的情况下使用
悲观锁
? 总是假设最坏的情况,每次去拿数据都会认为别人会修改,所以每次拿数据的时候都会上锁。这样别人拿这个数据就会阻塞直到它拿到共享锁(共享锁每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程)。传统的关系型数据库里边就用到了很多类似的锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。Java中Synchronized和ReentrantLock等独占锁j就是悲观锁思想的实现。
乐观锁
? 总假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但更新的时候会判断一下在此期间别人有没有更新这个数据,可以使用版本号机制和CAS算法实现。
? 乐观锁适用于多读的应用类型,可以提高吞吐量,就像数据库提供的类似write_condition机制,其实都是提供乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的

分段锁
? 分段锁其实是一种锁的设计,并非具体实现。对于concurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效并发操作
? 并发容器类的加锁机制是基于更小粒度的分段锁,分段锁也是提升多并发程序性能的重要手段之一。
? 在并发程序中,串行操作会降低可伸缩性,并且上下文的切换也会降低性能。在锁上发生竞争时,将同时导致这两种问题,使用独占锁时保护受限资源,基本采用串行方式,每次只能有一个线程访问它。所以对于可伸缩性来说最大的威胁就是独占锁
一般有三种降低锁的竞争程度的方式:
1,减少锁的持有时间;2,降低锁的请求频率;3,使用带协调机制的独占锁,这些机制允许更高的并发性。
在某些情况下,我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这称为分段锁。
简单讲解:
? 容器内有多把锁,每把锁用于锁容器中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是concurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
举例:
? 在concurrentHashMap中使用一个包含16个锁的数组,每个锁保护散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合适的散列算法使关键字均匀分布,那么这大约能使对锁的请求减少1/16,正是这项技术使concurrentHashMap支持的并发写入达到16个

锁的状态:
1,无锁状态
2,偏向锁状态
3,轻量级锁状态
4,重量级锁状态
锁的状态会通过对象监视器在对象头部中的字段表明。
四种状态会随着竞争的升级而升级,且不可逆,不可降级
四种锁并非Java语言中的锁,而是jvm为了提高锁的获取与释放效率而做的优化(用Synchronized时)
偏向锁
? 指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
轻量级锁
? 当锁是偏向锁时,被另一个线程访问,偏向锁会升级为轻量级锁,其他线程会通过自选形式尝试获取,不会阻塞,提高性能
重量级锁
? 指当前锁为轻量级锁时,另一个线程虽然自旋,但不会一直持续下去,当达到一定次数还没得到,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低

自旋锁
CAS算法是乐观锁的一种实现,CAS算法中涉及到自旋锁,这里简述一下
CAS算法说明
? CAS的单词Compare And Swap(比较并交换),一种知名的无锁算法。无锁编程,即不使用锁实现多线程间的变量同步,也是在线程没有被阻塞时实现变量同步,也叫非阻塞同步(Non-blocking synchronization)
CAS算法涉及三个操作数:
1,要读写的内存值V
2,进行比较的值A
3,要写入的新值B
? 更新变量的时候,只有当变量的预期值A和内存V中的实际值相等时(证明之前没有被修改),才将内存中的V值修改为新值B,否则不会执行任何操作
自旋锁说明
? 指当某线程在获取锁的时候,若锁已经被其他线程获取,则该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
? 该锁是为了实现保护共享资源而提出的一种锁。自旋锁与互斥锁比较类似,都是为了解决对某项资源的互斥使用。这两种锁在任意时刻最多只能有一个保持者,也就是说,在任何时刻最多只有一个执行单元获得锁。
两者在调度机制上有所不同:
? 对于互斥锁,如果资源已经被占用,资源的申请者只能进入睡眠状态。
? 对于自旋锁,它不会引起调度者睡眠,如果自旋所已经被别的执行单元保持,调用者就在那里一直旋转查看保持着是否释放
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);
}
}

自旋锁存在的问题
? 1,自旋锁不会让现成状态发生改变,一直处于用户态。即多线程一直都是active;不会让线程阻塞,减少了不必要的上下文切换,执行速度快
? 2,非旋转锁在拿不到锁的情况下会进入阻塞状态,进而进入内核态。当拿到锁的时候需要从内核态恢复,需要线程上下文切换。
? (进程的执行模式划分为用户模式和内核模式,如果当前运行的是用户程序、应用程序或者内核之外的系统程序,那么对应进程就在用户模式下运行;如果在用户程序执行过程中出现了系统调用或者发生中断时间就要运行操作系统程序,即核心,进程模式就会变成内核模式。在内核模式下运行的进程可以执行机器的特权指令并且不受用户的干扰即使是root用户。用户进程即可以在用户模式下运行也可以在内核模式下运行)
可重入的自旋锁和不可重入的自旋锁
? 上面写过的自旋锁代码,仔细看他是不支持重入的。既当一个线程第1次已经拿到了锁,在该所释放之前又一次重新获得锁住,第2次就不可能成功拿到。由于不满足CAS,所以第2次获取的时候就会进入循环等待状态,如果是可重入锁,第2次应该也能够成功获取到。
public class ReentrantSpinLock { private AtomicReferencecas = 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); } } } }
总结
1,线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待直到获取到锁
2,自旋锁所等待期间线程的状态不会改变,线程一直都是用户态并且是活动的
3,自旋锁如果持有锁的时间太长,会导致其他等待获取锁的线程耗尽CPU
4,自旋锁本身无法保证公平性这也无法保证可重入性
5,基于自旋锁,可以实现具备公平性和可重入性质的锁
参考文章
你知道Java里有多少种锁吗?(15种锁最全总结)