深入理解Java里的各种锁(下)

悲观锁,乐观锁,自旋锁,偏向锁,轻量级锁,重量级锁在上篇:深入理解Java里的各种锁(上)

今天再来聊聊其他的锁:
深入理解Java里的各种锁(下)

4、公平锁 VS 非公平锁

锁的公平性是相对于获取锁的顺序而言的。

公平锁:公平锁获取锁的顺序符合请求的绝对时间顺序,没有获取到锁的线程会被安排到一个阻塞队列中去,也就是FIFO,缺点是吞吐量很低,因为除了等待队列中的第一个线程,其他的线程全部会被阻塞,而 CPU 唤醒 阻塞线程也是需要很多开销的。

非公平锁:非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

看看 ReentrantLock 是如何实现公平锁 和 非公平锁的:

深入理解Java里的各种锁(下)

对比源码可以发现 公平锁比非公平锁多了一个方法判断:
hasQueuedPredecessors()

 public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

hasQueuedPredecessors() 做的事情是判断同步队列中当前节点是否有前驱节点,如果返回true,这表明有其他线程更早的获取到了锁,因此需要等待前一个节点释放锁它才有机会继续获取锁。

综上所述,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

5. 可重入锁 VS 非可重入锁

可重入锁:同一个对象被线程在方法外获取到锁时,再进入该方法内层的方方法会自动获取到锁,不会因为外层或内层方法未释放锁而阻塞,Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。一段简单的代码来讲解一下:

扫描二维码关注公众号,回复: 12289310 查看本文章
public class Man {

    public synchronized void watch(){
        say();
    }

    public synchronized void say(){
        System.out.println("happy new year");
    }
}

Man 这个类 两个方法都使用了 synchronized 来修饰,watch ( ) 方法中调用了 eat( ) 方法,因为 synchronized 锁是可重入的,所以同一个线程调用 watch () 方法还可以继续调用 say ( ) 方法。
如果 synchronized 是不可重入锁,那同一个线程在调用完 watch () 方法时还没释放锁(释放 synchronized 带来的 monitor),再调用 say () 方法时就会被阻塞住,实际上当前线程已经持有该对象的锁了,这无论怎么说都是不合理的~ 这就是可重入锁。

简单分析一下 可重入锁 ReentrantLock 不可重入锁 NonReentrantLock
是如何实现可重入锁的:

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

6、排他锁 VS 共享锁

排它锁:前文锁提到的 synchronized 、 ReentrantLock 这些都是排他锁,也就是这些锁在同一时间只允许一个线程进行访问。

共享锁:ReentrantReadWriteLock ,也就是常说的读写锁;在同一时间可以允许多个线程访问,但是写线程访问时所有的读线程和其他的写线程都会被阻塞。读写锁维护了一对锁,一个是读锁,一个是写锁,一般情况下,读写锁的性能都会被排他锁要好,因为大多数场景是读多余写,在读多写少的情况下,读写锁比排它锁能够有更好的并发性能和吞吐量。

读写锁的实现原理后续会再更新一篇文章来分析 ReentrantReadWriteLock 的源码。

回家过年了,起飞~

深入理解Java里的各种锁(下)

猜你喜欢

转载自blog.51cto.com/15075523/2606414