可重入锁 / 不可重入锁
可重入锁:一个线程,调用获取临界资源的方法A,如果在方法内部同时又调用了另一个获取临界资源的方法B,此时方法A和方法B其实是在争抢同一个临界资源。此时方法A和方法B就需要使用同一把锁来加锁。如果此时过程当中不会出现死锁的问题,则该锁就是可重复锁。
广义上的可重入锁指的是可以重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不会发生死锁(前提是同一个对象或class)。这样的锁就叫做可重入锁。ReentrantLock
和 synchronized
都是可重入锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
此代码就是一个可重入锁的特点。如果不是可重入锁的话,setB可能不会被当前线程执行,可能会造成死锁。
不可重复锁:同样是一个线程,调用获取临界资源的方法A,同时在方法A内部调用获取临界资源的方法B。此时方法A和方法B是需要同一把锁来加锁的。但是如果是不可重入锁的话,方法B就要等待方法A释放锁之后才能加锁去访问内部资源,但是方法A又是执行完方法B才会去释放锁,此时就会造成死锁。
不可重复,不可递归调用。递归调用就会发生死锁。
import java.util.concurrent.atomic.AtomicReference;
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
//这句是很经典的“自旋”语法,AtomicInteger中也有
// 如果同一个线程两次调用lock方法,如果不执行unlock方法释放锁的话,
// 第二次调用lock方法就会产生死锁
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
对上述代码修改成可重入锁:
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);
}
}
}
}
公平锁 / 非公平锁
公平锁:多个线程抢占资源都会加锁。如果每个线程都能按照申请锁的顺序去获得到锁的话,就是公平锁。
非公平锁:多个线程抢占资源都会加锁。但是每个线程获取锁的顺序并不是按照申请锁的顺序去拿的,有可能后面申请锁的线程会比前面申请锁的线程先拿到锁去访问资源。有可能造成优先级反转或饥饿现象。ReentrantLock
默认是非公平锁,但是可以指定为公平锁。synchronized
是非公平锁。因为其并不像 ReentrantLock
是通过 AQS
来实现线程调度的,所以没有办法使其变成公平锁。
非公平锁的有点在于吞吐量比公平锁大。
为什么??
独占锁 / 共享锁
独占锁:该锁每次都只能被一个线程所持有。
共享锁:该锁可以被多个线程共有,典型的就是ReentrantReadWrite里的读锁,它的读锁是可以被共享的,但是它的写锁却是每次只能被独占。
互斥锁 / 读写锁
互斥锁
互斥锁:在访问共享资源之前进行加锁操作,在访问完成之后进行解锁操作。加锁后,任何其他试图再次加锁的线程都被阻塞,直到当前线程解锁。
读写锁
读写锁:既是独占锁,也是共享锁。read模式是共享,write是独占(排它)锁。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态被释放。如果处于读状态锁下,允许其他线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放。
为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后想要获得读状态锁的线程。
读写锁非常适合资源的读操作远多于写操作的场景。
读写锁在Java中的实现: ReadWriteLock
乐观锁 / 悲观锁
悲观锁
总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。共享资源每次只给一个线程使用,其他线程阻塞,用完之后再把资源转让给其它线程。
关系型数据库的 行锁,表锁,读锁,写锁都是悲观锁。
Java中的 synchronized
和 ReentrantLock
等独占锁都是悲观锁的实现。
乐观锁
总是假设最好的情况,每次那数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。可以使用版本号机制和CAS算法实现。
乐观锁适用于多读的应用类型,可以提高吞吐量。
Java中的juc包下的atomic包下的原子变量操作类就是使用了乐观锁的一种实现方式CAS实现的。
分段锁
分段锁是一种锁的设计,并不是特指一种锁。java中分段锁的具体实现是 ConcurentHashMap
,通过使用分段锁,实现高效的并发操作。ConcurrentHashMap
是针对每一个子元素都会有一个锁。如果程序修改的数据是不同的子元素,其实是不会出现争抢锁的情况。只有当出现多个线程同时操作 ConcurrentHashMap
中的同一个子元素,才会进行锁的申请和使用,以及线程的阻塞等情况。
如果 ConcurrentHashMap
容量为16,其内部就会有16个锁。
在并发程序中,串行操作 synchronized
是会降低可伸缩性,同时,线程的上下文切换也会减低性能。在锁上发生竞争通常会导致以上两种问题。使用独占锁 synchronized
和 ReentrantLock
保护受限资源的时候,基本上都是采用串行方式——每次只能有一个线程访问受限资源。所以对于伸缩性来说最大的威胁就是独占锁。
如何降低锁的竞争程度
- 减少锁的持有时间
- 降低锁的请求频率
- 使用带有协调机制的独占锁,这些机制允许更高的并发性。
其实说简单点,
容器里有很多把锁,每一把锁用于锁容器中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。
这就是 ConcurrentHashMap
所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段的数据也能被其他线程访问。
偏向锁 / 轻量级锁 / 重量级锁
锁的状态
1.无锁状态
2.偏向锁状态
3.轻量级锁状态
4.重量级锁状态
锁的状态是通过对象监视器在对象头中的字段来表明的。
四种状态会随着竞争的情况逐渐升级,而且是不可逆的,不可降级。
这四种状态都不是java语言中的锁,而是JVM为了提高锁的获取与释放效率而做的优化。(使用synchronized时)。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程就会自动获取锁,降低获取锁的代价。
轻量级锁
轻量级锁是指锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
重量级锁时指当前锁为轻量级锁的时候,另外一个线程虽然是自旋,但是自旋不会一直持续下去,当自旋达到一定次数还没有获得到锁,就会进入阻塞,该锁就会膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁
是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够成功获取,知道获取锁才会退出循环。
自旋锁是为实现保护共享资源而提出的一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,在任何时刻最多只能有一个执行单元获得锁。
但是两者在调度机制上略有不同。互斥锁,如果资源被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。(换句话说,就是一直持有CPU资源,不会进入阻塞状态)。
java当中的CAS机制,就涉及到了自旋锁。
// 不可重入自旋锁
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
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);
}
}
// 可重入自旋锁
public class ReentrantSpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
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);
}
}
}
}
自旋锁存在的问题
- 如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率特别高。
- 如果实现的自旋锁是非公平锁,可能会导致出现“线程饥饿”问题。
自旋锁的优点
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
- 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态回复,需要进行线程上下文切换。(线程被阻塞后边进入内核(linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)。
自旋锁与互斥锁
- 自旋锁与互斥锁都是为了实现保护资源共享的机制。
- 无论是自旋锁还是互斥锁,在任意时刻,都最多只有一个保持者。
- 获取互斥锁的线程,如果锁已经被占用,则该线程将进入阻塞状态。获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。
自旋锁总结
- 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将进入循环等待,直到获取锁。
- 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
- 自旋锁本身无法保证公平性,同时也无法保证可重入性。
- 基于自旋锁,可以实现具备公平性和可重入性质的锁。