并发锁原理分析(并发进阶十五种锁)
并发锁原理分析(并发进阶十五种锁)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()) {
明天的你会感谢今天努力的你
举手之劳,加个关注
锁分类- 公平锁/非公平锁
- 可重入锁/不可重入锁
- 独享锁/共享锁
- 互斥锁/读写锁
- 乐观锁/悲观锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
- 自旋锁
公平锁是指多个线程按照申请的顺序获取锁
非公平锁非公平锁是指多线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能会造成优先级反转或者饥饿现象
对于Java ReentrantLock 来说,同构构造函数指定该锁是否是公平锁、默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized来说,也是一种非公平锁。由于其并不像ReentrantLock 是通过 AQS 来实现的线程调度,所以并没有任何办法使其变成公平锁。
可重入锁广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提是一个对象或者class) 这样的锁叫做可重入锁。ReentrantLock 和 synchronized 都是可重入锁。
ReentrantLock 可重入锁实现,AQS中维护了一个线程可见的 stat 来计算重入次数,避免频繁的持有和释放操作。
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;
}
不可重入锁
不可重入锁,与可重入锁相反, 不可递归调用, 递归调用就发生死锁,使用自旋锁来模拟一下不可重入锁
package com.test;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Descrintion:
* @Date : Created in 10:48 2019/5/14
* @
*/
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock(){
Thread current = Thread.currentThread();
for(;;){
System.out.println("start");
if(!owner.compareAndSet(null current)){
System.out.println("running");
return ;
}
}
}
public void unlock(){
Thread current = Thread.currentThread();
owner.compareAndSet(current null);
}
public static void main(String[] args) {
UnreentrantLock unreentrantLock = new UnreentrantLock();
unreentrantLock.lock();
//unreentrantLock.unlock();
System.out.println("=========");
unreentrantLock.lock();
}
}
未调用unlock 输出
调用unlock() 输出
使用原子引用来存放线程, 同一线程两次调用lock() 方法,如果不执行unlock() 释放锁的话,第二次调用自旋的时候就会产生死锁
把他变成一个可重入锁
package com.test;
import com.design.filter.Criteria;
import Java.util.concurrent.atomic.AtomicReference;
/**
* @Descrintion: 可重入锁
* @Date : Created in 11:10 2019/5/14
* @
*/
public class CreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
private int state = 0 ;
public void lock(){
Thread current = Thread.currentThread();
if(current == owner.get()){
state ;
System.out.println(" ");
return ;
}
for(;;){
System.out.println("start");
if(!owner.compareAndSet(null current)){
System.out.println("running");
return ;
}
}
}
public void unlock(){
Thread current = Thread.currentThread();
if(current == owner.get()){
if(state != 0){
state-- ;
}else{
owner.compareAndSet(current null);
}
}
}
public static void main(String[] args) {
CreentrantLock creentrantLock = new CreentrantLock();
creentrantLock.lock();
//creentrantLock.unlock();
creentrantLock.lock();
}
}
未调用 unlock
调用 unlock
每次执行操作之前,判断当前锁持有者是否是当前对象,采用state 计数,不用每次去释放锁
独享锁和共享锁在JUC的ReentrantLock 和 ReentrantReadwriteLock 它两一个是独享一个是共享
共享锁该锁可以被多个线程共有,典型的就是ReentrantReadWriteLock 里的读写锁,他的读锁是可以被共享的,但是他的写锁确每次只能被独占,另外读锁的共享可保证并发读是非常高效的,但是读写和写写、写读都是互斥的, 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
互斥锁在访问共享资源之前进行加锁,在访问完成之后进行解锁操作,加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进行解锁,如果解锁时有一个以上的线程阻塞,那么所有的该锁上的线程都被编成就绪状态,第一个变为就绪状态的线程有执行加锁操作,那么其他的线程又会进入等待,在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
读写锁读写锁既是互斥锁,又是共享锁, read 模式是共享, write 是互斥(排它锁)的
读写锁有三种状态:读加锁状态,写加锁状态和不加锁状态
读写锁在Java 中的具体实现就是 ReentrantLock
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。只有一个线程可以占有写状态的锁,但可以有多个线程同时占有写状态锁,这也是它可以实现高并发的原因,当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放; 如果处于读状态锁下,允许其他线程获得它的读状态锁,但是不允许获得它写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态的锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远大于写操作的情况
乐观锁顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。JUC 包下面的原子变量类都是适用了乐观锁的一种实现方式 CAS 实现
悲观锁顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
分段锁就是通过分段锁的形式来实现高效的并发操作。
并发容器类的加锁机制是基于粒度更小的分段锁,分段锁是提升多并发程序性能的重要手段之一。
并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会减低性能,在锁上发生竞争时将导致这两种情况问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式每次只能有一个线程访问它。所以对于可伸缩性来说最大的威胁就是独占锁。
我们一般有三种方式降低锁的竞争程度
1.减少锁的持有时间
2.降低锁的请求频率
3.使用带有协调机制的独占锁
在某些情况下,我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这称为分段锁。
偏向锁/轻量级锁/重量级锁锁状态
1. 无锁状态
2.偏向锁状态
3.轻量级锁状态
4.重量级锁状态
锁状态是通过对象监视器在对象头中的字段来表明的。四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。 这四种状态都不是Java 语言中的锁,而是JVM为了提高锁的获取与释放效率而做的优化(synchronzied)
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降级获取锁的代价。
轻量级锁
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能低下。
自旋锁
CAS 算法是乐观锁的一种实现方式,CAS 算法中有涉及到自旋锁。自旋锁(spinlock):它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名