概述
公平锁和非公平锁
在多线程的学习中,发现通过显式锁对线程上锁解决了线程安全问题后,还存在会有线程插队的问题。可能刚出来的线程又马上抢到锁再次插队进入,导致后边的线程一直轮不到,最后线程饿死了的情况发生。
问题代码示例:
public class DemoReentrantLock {
public static void main(String[] args) {
//创建一个DemoR对象
Runnable r = new DemoR();
//创建三个线程,并命名
new Thread(r,"线程1").start();
new Thread(r,"线程2").start();
new Thread(r,"线程3").start();
}
}
//测试类
class DemoR implements Runnable{
private int r = 10;
//显式锁加锁方式
//通过ReentrantLock无参构造器,默认创建一个非公平锁
Lock l = new ReentrantLock();
@Override
public
void run() {
while (true){
//上锁
l.lock();
if (r>0){
//休眠
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
r--;
System.out.println(Thread.currentThread().getName()+":,当前:"+r);
}else {
//解锁
l.unlock();
break;
}
//解锁
l.unlock();
}
}
}
发生了如下运行结果:
线程1:,当前:9
线程2:,当前:8
线程2:,当前:7
线程2:,当前:6
线程2:,当前:5
线程2:,当前:4
线程2:,当前:3
线程2:,当前:2
线程2:,当前:1
线程2:,当前:0
Process finished with exit code 0
可以根据运行结果看出,线程2进入线程再出来后一直插队,导致了线程3一直没能执行,线程3就被饿死了。
为什么会有这种情况发生?
其实是因为,第一个线程出来会再次尝试和其他线程一起获取锁,如果插队成功,就会导致后面的线程还要继续等待,后面一直未能执行的线程可能最后就饿死了。
为了得让后面的线程不饿死,那就得让他们排队,这时候就得运用公平锁了,
公平锁就是多个线程按照申请锁的顺序来排队获取锁,线程直接进入队列中排队,必须是队列的第一个线程才可以获取锁。这样后边等待的线程就不会饿死了。
公平锁上锁代码示例:
class DemoR implements Runnable{
private int r = 10;
//通过ReentrantLock构造器,创建一个公平锁
Lock l = new ReentrantLock(true);
@Override
public
void run() {
while (true){
//上锁
l.lock();
if (r>0){
//休眠
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
r--;
System.out.println(Thread.currentThread().getName()+":,当前:"+r);
}else {
//解锁
l.unlock();
break;
}
//解锁
l.unlock();
}
}
}
线程1:,当前:9
线程2:,当前:8
线程3:,当前:7
线程1:,当前:6
线程2:,当前:5
线程3:,当前:4
线程1:,当前:3
线程2:,当前:2
线程3:,当前:1
线程1:,当前:0
Process finished with exit code 0
可以看出,现在是线程按照排队顺序依次执行了。公平锁出来一个线程就得到后面去排队。
公平锁的优点:等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁的优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
源码剖析
根据源码看看是怎样实现公平锁的公平。
ReentrantLock
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer{...}
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。
它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
继续公平锁与非公平锁的加锁方法的源码:
公平锁
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && !hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
非公平锁
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。
再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
public final boolean hasQueuedPredecessors() {
Thread first = null; Node h, s;
if ((h = head) != null && ((s = h.next) == null ||
(first = s.waiter) == null ||
s.prev == null))
first = getFirstQueuedThread(); // retry via getFirstQueuedThread
return first != null && first != Thread.currentThread();
}
公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。
非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。
各自优缺点:
公平锁的优点:等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁的优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
实际使用可以根据特性需求使用这两种锁。
最后
以上就是甜甜小鸭子为你收集整理的公平锁和非公平锁使用和解析的全部内容,希望文章能够帮你解决公平锁和非公平锁使用和解析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复