【多线程】公平锁和非公平锁

邓敏 2022年03月27日 51次浏览

公平锁

公平锁其实很好理解,可以看作我们现实生活中的排队取餐,按照先来先得的规矩来依次取餐,公平锁也是这样,当有线程排队获取锁的同时,等待越久的线程则优先获取锁,这样就能保证每个线程获取锁的顺序和公平性。

场景

假设有4个线程来尝试获取锁,当线程1获取到锁之后,线程2、3、4就会在队列中等待,然后等到线程1执行完成之后,按照进入队列的顺序,依次执行最先存入队列的线程,也就是等到时间越久的线程,然后依次类推。
image.png

非公平锁

非公平锁在公平锁的基础上添加了插队的机制,并不是说每个阻塞的线程都在无时无刻的尝试获取锁,而是在线程排队的基础上,当线程去获取锁的同时,如果发现当前锁刚好释放了,那么这个尝试获取锁的线程就会直接获取到锁,而不是将锁给到排队的线程中去。这样的做法其实是为了提供线程的吞吐性和性能。因为在排队的情况下,如果当前线程锁释放了,如果交给队列中的线程,那么就需要先唤醒队列中的线程,然后才能让这个线程获取到这把锁,但是突然有个线程过来了想要直接尝试获取锁,那这里其实就是省去了唤醒线程这一步不小的开销,自然而然的就提高了线程的吞吐率和执行的性能了,但是这样也会有一个弊端,那就是可能会产生线程饥饿,线程饥饿就是指有些线程可能一直在排队的状态,导致很长时间得不到执行。

场景

假设线程1在解锁的时候,突然有个线程5来获取线程1的这把锁,那么根据我们的非公平策略,线程5是可以拿到这把锁的,尽管线程5还没有进入到等待队列,而且线程2、3、4等待的时间都比线程5要长,但是根据整体的效率来看,此时的这把锁会交给5持有。
image.png

代码示例

/**
 * 描述:演示公平锁,分别展示公平和不公平的情况,非公平锁会让现在持有锁的线程优先再次获取到锁。代码借鉴自Java并发编程实战手册2.7。
 */

public class FairAndUnfair {

    public static void main(String args[]) {

        PrintQueue printQueue = new PrintQueue();

        Thread thread[] = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue), "Thread " + i);
        }

        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class Job implements Runnable {

    private PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    @Override
    public void run() {
        System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
        printQueue.printJob(new Object());
        System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
    }
}


class PrintQueue {
    private final Lock queueLock = new ReentrantLock(false);
    public void printJob(Object document) {
        queueLock.lock();
        try {
            Long duration = (long) (Math.random() * 10000);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
        queueLock.lock();
        try {
            Long duration = (long) (Math.random() * 10000);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
            }
    }
}

我们可以通过new ReentrantLock(false)中的参数来设置公平/非公平锁,在ReentrantLock中是默认使用非公平锁的,包括synchronized也是一样。

以上的代码在公平锁的情况下的输出:

Thread 0: Going to print a job
Thread 0: PrintQueue: Printing a Job during 5 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 1: PrintQueue: Printing a Job during 3 seconds
Thread 2: PrintQueue: Printing a Job during 4 seconds
Thread 3: PrintQueue: Printing a Job during 3 seconds
Thread 4: PrintQueue: Printing a Job during 9 seconds
Thread 5: PrintQueue: Printing a Job during 5 seconds
Thread 6: PrintQueue: Printing a Job during 7 seconds
Thread 7: PrintQueue: Printing a Job during 3 seconds
Thread 8: PrintQueue: Printing a Job during 9 seconds
Thread 9: PrintQueue: Printing a Job during 5 seconds
Thread 0: PrintQueue: Printing a Job during 8 seconds
Thread 0: The document has been printed
Thread 1: PrintQueue: Printing a Job during 1 seconds
Thread 1: The document has been printed
Thread 2: PrintQueue: Printing a Job during 8 seconds
Thread 2: The document has been printed
Thread 3: PrintQueue: Printing a Job during 2 seconds
Thread 3: The document has been printed
Thread 4: PrintQueue: Printing a Job during 0 seconds
Thread 4: The document has been printed
Thread 5: PrintQueue: Printing a Job during 7 seconds
Thread 5: The document has been printed
Thread 6: PrintQueue: Printing a Job during 3 seconds
Thread 6: The document has been printed
Thread 7: PrintQueue: Printing a Job during 9 seconds
Thread 7: The document has been printed
Thread 8: PrintQueue: Printing a Job during 5 seconds
Thread 8: The document has been printed
Thread 9: PrintQueue: Printing a Job during 9 seconds
Thread 9: The document has been printed

可以看出,线程获取锁的顺序都是依次执行的。

我们接下来看一下同样的代码在设置了非公平锁下的输出

Thread 0: Going to print a job
Thread 0: PrintQueue: Printing a Job during 6 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 0: PrintQueue: Printing a Job during 8 seconds
Thread 0: The document has been printed
Thread 1: PrintQueue: Printing a Job during 9 seconds
Thread 1: PrintQueue: Printing a Job during 8 seconds
Thread 1: The document has been printed
Thread 2: PrintQueue: Printing a Job during 6 seconds
Thread 2: PrintQueue: Printing a Job during 4 seconds
Thread 2: The document has been printed
Thread 3: PrintQueue: Printing a Job during 9 seconds
Thread 3: PrintQueue: Printing a Job during 8 seconds
Thread 3: The document has been printed
Thread 4: PrintQueue: Printing a Job during 4 seconds
Thread 4: PrintQueue: Printing a Job during 2 seconds
Thread 4: The document has been printed
Thread 5: PrintQueue: Printing a Job during 2 seconds
Thread 5: PrintQueue: Printing a Job during 5 seconds
Thread 5: The document has been printed
Thread 6: PrintQueue: Printing a Job during 2 seconds
Thread 6: PrintQueue: Printing a Job during 6 seconds
Thread 6: The document has been printed
Thread 7: PrintQueue: Printing a Job during 6 seconds
Thread 7: PrintQueue: Printing a Job during 4 seconds
Thread 7: The document has been printed
Thread 8: PrintQueue: Printing a Job during 3 seconds
Thread 8: PrintQueue: Printing a Job during 6 seconds
Thread 8: The document has been printed
Thread 9: PrintQueue: Printing a Job during 3 seconds
Thread 9: PrintQueue: Printing a Job during 5 seconds
Thread 9: The document has been printed

在输出的内容中我们不难发现,存在了抢锁“插队”的现象,比如Thrand0在释放锁之后又能优先的获取到锁,如果按照公平锁的策略应该是会直接给在排队的Thread1~9了。

总结

公平锁和非公平锁的优缺点

image.png