跳至主要內容

9、Java中的各种锁

KindBrave大约 5 分钟

总结于文章1open in new window 文章2open in new window

0.Java中的两种锁

  • synchronized,关键字,在jvm中实现
  • Lock,一个接口

0.1 synchronized

synchronized是Java中的一个关键字

它是一个悲观锁,它涉及了一些锁升级的概念。

  • 在最开始时,synchronized是偏向锁,即它更加偏向于第一个获取到这个锁的线程。如果说下一次执行到同步语句,发现还是这个线程,那么synchronized就不需要重新加锁了,这样只有一个线程的话,就不需要频繁加锁,性能开销基本没有。
  • 如果某时刻又来了一个线程,那么此时的偏向锁就会升级为轻量锁。轻量级锁是一个自旋锁,如果现在有两个线程A和B,那么A在执行的时候,B此时会自旋等待,即不会暂停执行。这个自旋等待次数是有上限的,一旦到达了某个上限,就会升级为重量级锁,此时没有争夺到锁的线程就会暂停执行,让出CPU,直到通知它再次执行。

0.2 ReentrantLock

ReentrantLock是可重入锁,默认是非公平锁。

这里主要说一下它是如何可重入的。

ReentrantLock里面有个内部类Sync和它的两个子类FairSync和NonFairSync,对应着公平锁和非公平锁的实现。

如何实现可重入的,我们看Sync#tryLock():

final boolean tryLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    //如果state值为0,表示这个锁没有被任何线程占用,通过CAS将state改为1
    //(因为可能有多个线程同时争夺)
    if (c == 0) {
        if (compareAndSetState(0, 1)) {
	    //如果争夺成功,那么把这个锁的拥有者设置为当前线程
	    //这样可以保证下次重入
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果发现state不为1,且拥有者是本线程,表示重入 
    else if (getExclusiveOwnerThread() == current) {
        if (++c < 0) // overflow
	    throw new Error("Maximum lock count exceeded");
        setState(c);
        return true;
    }
    return false;
}

可以看到重入的关键就是AQS中维护了state变量和所有者线程值。每重入一层,state就会加1

1711526206795

0.3 synchronized和ReentrantLock区别

  • s可以修饰代码块、方法,R只可修饰方法
  • s是JVM层面实现的,R是实现了Lock接口,在Java层面实现的
  • s是非公平锁,R是默认非公平锁,可选公平锁
  • s是通过JVM中监视器来实现加锁解锁,R是通过AQS

1.乐观锁和悲观锁

对于乐观锁来说,它会认为每次读取都不会有其他线程去修改,就直接读就可以了。对于写的时候,它会比较当前值是否是期待值,如果是,则表明可能没有其他线程修改,就进行修改,否则则循环等待下次修改。

对于悲观锁来说,它认为每次读取都会有人修改,所以就在读取之前都加锁。这样在竞争激烈的情况下可能导致性能下降。

乐观锁更加适合读多写少的情况,而悲观锁更适合写多的情况

1.1 乐观锁有什么问题?

乐观锁一般使用CAS来实现。

使用CAS时,可能会有三个问题:

  • 可能会导致ABA问题,即其他线程把数据从A修改到B,又从B修改到A,这时我们的线程是发现不了数据变化的
  • 在写多的场景下,CAS会频繁失败然后循环等待,这时也是比较耗时的。
  • CAS只能保证单个共享变量的原子性,没办法保证多个共享变量的原子性

1.2 Java中哪些实现是悲观锁,哪些是乐观锁

Java中,无论是synchronized还是Lock接口,都是悲观锁。

Java中乐观锁可以使用Unsafe的CAS,还有一些原子类都是乐观锁

2.自旋锁

自旋锁就是一个线程在CPU执行时,发现同步资源是上锁的,此时不必让出CPU,而是在CPU中自旋等待,等待到同步资源解锁直接开始执行就可以了。

因为自旋锁不会让出CPU,等待线程会在CPU中无用的自旋操作,所以会有一定的上限,如果自旋次数超过某个数量,就不再自旋了。

此外,后面还引入了自适应自旋锁,即自旋次数不再固定,而是根据历史的自旋次数等因素来动态决定。

3.公平锁和非公平锁

  • 公平锁:按照线程队列的顺序来获取锁,只有队头的线程才可以获得锁
  • 非公平锁:队列中的线程都可以竞争锁,会出现后来的线程却先得到锁的情况,也可以不进队列直接执行。非公平锁可以提高性能,因为有时候线程不必经历暂停-恢复的过程,但是可能出现线程饿死的情况。

公平锁就是线程直接进入阻塞队列,直到成为队列的第一个,才可以获得锁。非公平锁就是多个线程加锁时直接尝试上锁,上锁成功直接执行,失败进入阻塞队列。

ReentrantLock默认的实现就是非公平锁。它其中有一个Sync类,继承了AQS,它有两个子类FairSync和NonfairSync,对应着公平锁和非公平锁。

4.可重入锁,非可重入锁

可重入锁又叫递归锁,就是如果一个线程在外部方法已经获取了锁,那么在进入内部方法时,会自动获得锁,而不会被阻塞

可重入锁可以防止死锁。

synchronized和ReentrantLock都是可重入锁。

5.共享锁和排他锁

共享锁就是多个线程可同时持有一把锁,排他锁就是同时只可有一个线程获得一把锁。

某个同步资源加了共享锁,那么其他线程也只能加共享锁,不能加排他锁,而且共享锁只能读数据,不能写数据。