在多线程的编程中,我们经常会涉及到锁的使用。今天来聊一聊Java
中的锁。
一、悲观锁
1.1 含义
坏事一定会发生,所以不管进行任何操作前,先上锁。
1.2 常见实现:
数据库中的行锁,表锁,读锁,写锁,
以及Java
中的Synchornized
关键字都是悲观锁的实现。
二、乐观锁
2.1 含义
坏事未必会发生,如果发生了再做处理。
自旋锁(CAS)
是一种常见的乐观锁实现。CAS
全称 CompareAndSwrap
。
2.2 说明:
比如:对变量i
进行++
操作,写入数据库之前会重新获取i
值,如果值发生了改变,则重新将新值进行++
;入库之前再去判断值有没有发生改变,若发生改变,重复上述操作。这样一次次循环,直到值未发生改变,写入数据库。
Java
中的AtomicInteger
底层实现的就是CAS
。
2.3 ABA问题
上述例子中对变量i
进行入库前检查时,可能会有一个问题,i
可能经过了由m
到n
又变为m
的情况,这个时候数据会写入成功,但不代表数据是没有问题的。
ABA问题的常见解决方案:
版本号 Boolean
版本号: 在每一次对i
的操作,都进行版本号的修改,最终以版本号是否改变来判断是否入库。
Boolean: 添加一个Bollean类型的标识,比如默认为false
,,如果发生了改变,修改为true
。
以上两种方式都可以解决ABA
问题,至于怎么选择:如果不在乎值改变的次数,可使用Boolean
方式,否则使用版本号
方式。
三、排他锁、共享锁
3.1 排他锁
也成为独享锁、独占锁。
锁在同一时刻只能有一个线程使用,同一时刻不能被多个线程一同占用,一个线程占用后其它线程只能等待。
ReentrantLock
、synchronized
、ReentrantReadWriteLock
的写锁等都是排他锁的实现。
3.2 共享锁
锁在同一时刻可以被多个线程共享使用,一个线程对资源加了共享锁后其它线程对资源也只能加共享锁。共享锁有着很好的读性能。
ReentrantReadWriteLock
的读锁就是一种共享锁的实现。
获取排他锁的线程可以读或写数据;
获取共享锁的线程只能读取数据,不能修改数据。(在共享锁的代码块中修改数据,可能会导致其他获取共享锁的线程对数据不可见!)
六、统一锁、分段锁
统一锁: 大粒度的锁 分段锁: 分成一段一段的小粒度的锁
6.1 举例:
统一锁: 一个线程锁定A
等待B
,一个线程锁定B
等待A
,就会容易造成死锁
。这个时候就可以将A+B
统一称为大锁。
分段锁: 比如有一个特别长的链表,几万甚至几十万的数据。当多线程对这个链表插入元素的时候,每次插入,都要锁定整个链表,效率会非常低。如果想要提高效率,可以将此链表分为一段一段,每次添加元素,只需要锁定某一段即可。
七、公平锁、非公平锁
7.1 原理
公平锁: 多线程中保障了各线程获取锁的顺序,先到的线程优先获取锁;
非公平锁: 多线程中可能后来的线程先获取到锁。
以下是Java
并发包下获取公平锁和非公平锁的源码:
// 公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键代码 hasQueuedPredecessors() 判断是否有队列 或 是否是队列的第一个
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 非公平锁
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;
}
公平锁在获取锁之前,需要判断等待队列
是否为空,或者自己是否是队列的第一个,满足此条件,才可以获取到锁。
对于非公平锁,则不需要判断队列
,当线程来获取锁时,如果持有锁的线程刚巧释放锁,这个时候排在队列中的第一个线程还没有被唤醒(因为线程的上下文切换是需要不少开销的),非公平锁的线程就可以成功抢占到锁;否则,就要像公平锁一样排队等待。
上面说的线程切换的开销,其实也正是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率。
ReentrantLock
、ReadWriteLock
默认都是非公平锁模式。
7.2 ReentrantLock 的使用
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
try {
if (fairLock.tryLock()) {
log.info("--------获取到公平锁--------");
}
} finally {
fairLock.unlock();
}
// 非公平锁
ReentrantLock nonFairLock = new ReentrantLock(false);
try {
if (nonFairLock.tryLock()) {
log.info("--------获取到非公平锁--------");
}
} finally {
nonFairLock.unlock();
}
ReentrantLock
内部类Sync
继承自AbstractQueuedSynchronizer
类(AQS
),实现了锁的基本功能。
并使用FairSync
内部类实现公平锁,使用NonfairSync
实现非公平锁,这两个内部类都继承自Sync
。
7.3 饥饿效应
正是因为非公平锁获取锁时是不公平的,因此可能导致排队的线程迟迟获取不到锁,进而形成饥饿效应。
虽然非公平锁有饥饿效应,但它相对于公平锁在获取锁的性能上更优,不会像公平锁一样每次都需要通知队列中的等待者去获取锁。
如何解决饥饿效应?
饥饿效应产生的根本原因是:线程在排队等待获取锁的过程中,非公平锁使用插队的方式来减少CPU
的开销,而导致后边的线程一直在等待。
解决的方法可以是,让等待时间过长的线程有重新获取锁的机会。可以给每一个等待的线程设置一个超时时间,超时后可以重新获取一次锁。
代码示例:
ReentrantLock nonfair = new ReentrantLock(false);
try {
while (nonfair.tryLock(1, TimeUnit.SECONDS)) {
if (nonfair.isLocked()) {
System.out.println("获取公平锁成功!");
}
}
} catch (InterruptedException e) {
nonfair.unlock();
} finally {
nonfair.unlock();
}
原文始发于微信公众号(连帆起航):Java中的锁你了解多少?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/239262.html