不是朱锁锁的“锁”

如果学习 Java 的中“锁” 能跟看朱锁锁一样快乐就好了

1 锁的分类

1.1 偏向锁/轻量级锁/重量级锁

(1)偏向锁(做标记,没有竞争)

一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。

(2)轻量级锁(CAS,短时间的竞争)

JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

(3)重量级锁(互斥锁,长时间的竞争)

当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

锁升级

针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

1.2 可重入锁/非可重入锁

(1)可重入锁

可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。ReentrantLock 是典型的可重入锁,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。

(2)不可重入锁

不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。

1.3 共享锁/独占锁

共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

1.4 公平锁/非公平锁

公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。

1.5 悲观锁/乐观锁

(1)悲观锁

悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。

应用

ReentrantLock 类中的 lock() 等方法就是执行加锁,而 unlock() 方法是执行解锁。处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想。

MySQL 选择 select for update 语句,就是悲观锁,在提交之前不允许第三方来修改该数据,这当然会造成一定的性能损耗,在高并发的情况下是不可取的。

使用场景

悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。

(2)乐观锁

而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。

应用

可以利用一个版本 version 字段在数据库中实现乐观锁。在获取及修改数据时都不需要加锁,但是更新数据时,会检查版本号和获取数据时的版本号是否一致,如果一致就直接更新,如果不一致,说明计算期间已经有其他线程修改过这个数据了,可以选择重新获取数据,重新计算,然后再次尝试更新数据。

UPDATE student
    SET 
        name = ‘小文’,
        version= 2
    WHERE   id= 100
        AND version= 1

使用场景

乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。

1.6 自旋锁/非自旋锁

自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。

1.7 可中断锁/不可中断锁

synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。

2 synchronized 背后的 " monitor " 锁

最简单的同步方式就是利用 synchronized 关键字来修饰代码块或一个方法,这部分被保护的代码,在同一时刻只能有一个线程可以运行。

public synchronized void method() {
    method body
}

等价的伪代码

public void method() {
    this.intrinsicLock.lock();
    try{
        method body
    }
    finally {
        this.intrinsicLock.unlock();
    }
}

用 javap 命令查看反汇编的结果

public void method();
...
monitorenter // 加锁
...
monitorexit  // 释放锁 

synchronized 代码块实际上多了 monitorenter 和 monitorexit 指令。每个对象维护着一个记录着被锁次数的计数器,未被锁定的对象的该计数器为 0。

monitorenter

  • 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。
  • 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
  • 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。

monitorexit

monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止,代表这个 monitor 已经被释放了,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。

3 synchronized 和 Lock

3.1 相同点

(1)都可以用来保护资源的安全性

(2)都可以保证可见性

(3)都拥有可重入的特点

3.2 不同点

(1)用法不同

synchronized 关键字可以加在方法上,不需要指定锁对象(此时的锁对象为 this),也可以新建一个同步代码块并且自定义 monitor 锁对象;而 Lock 接口必须显示用 Lock 锁对象开始加锁 lock() 和解锁 unlock(),并且一般会在 finally 块中确保用 unlock() 来解锁,以防发生死锁。

与 Lock 显式的加锁和解锁不同的是 synchronized 的加解锁是隐式的,尤其是抛异常的时候也能保证释放锁。

(2)加解锁顺序不同

// Lock 可以不完全按照加锁的反序解锁,比如可以先获取 Lock1 锁,再获取 Lock2 锁
// 解锁时则先解锁 Lock1,再解锁 Lock2,加解锁有一定的灵活度
lock1.lock();
lock2.lock();
...
lock1.unlock();
lock2.unlock();

// synchronized 解锁的顺序和加锁的顺序必须完全相反
 synchronized(obj1){
    synchronized(obj2){
        ...
    }

}

(3)synchronized 锁不够灵活

一旦 synchronized 锁已经被某个线程获得了,此时其他线程如果还想获得,那它只能被阻塞,直到持有锁的线程运行完毕或者发生异常从而释放这个锁。如果持有锁的线程持有很长时间才释放,那么整个程序的运行效率就会降低,而且如果持有锁的线程永远不释放锁,那么尝试获取锁的线程只能永远等下去。

Lock 类在等锁的过程中,如果使用的是 lockInterruptibly 方法,那么如果觉得等待的时间太长了不想再继续等待,可以中断退出,也可以用 tryLock() 等方法尝试获取锁,如果获取不到锁也可以做别的事,更加灵活。

(4)synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制

例如在读写锁中的读锁,是可以同时被多个线程持有的,可是 synchronized 做不到。

(5)性能区别

在 Java 5 以及之前,synchronized 的性能比较低,但是到了 Java 6 以后,发生了变化,因为 JDK 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差。

3.3 如何选择

  1. 如果能不用,最好既不使用 Lock 也不使用 synchronized。大多数情况下可以使用 java.util.concurrent 包中的机制,它会处理所有的加锁和解锁操作,也就是推荐优先使用工具类来加解锁;
  2. 如果 synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少编写代码的数量,减少出错的概率。因为一旦忘记在 finally 里 unlock,代码可能会出很大的问题,而使用 synchronized 更安全;
  3. 如果特别需要 Lock 的特殊功能,比如可中断、超时功能等,才使用 Lock。

4 AQS

AQS 是一个用于构建锁、同步器等线程协作工具类的框架。

4.1 AQS 的内部原理

AQS 有三大核心部分

state 状态

/**
 * The synchronization state.
 */
private volatile int state;

state 的含义并不是一成不变的,它会根据具体实现类的作用不同而表示不同的含义。

(1)在 CountDownLatch 工具类里面,state 表示的是需要“倒数”的数量。一开始我们假设把它设置为 5,当每次调用 CountDown 方法时,state 就会减 1,一直减到 0 的时候就代表这个门闩被放开。

(2)在 ReentrantLock 中它表示的是锁的占有情况。最开始是 0,表示没有任何线程占有锁;如果 state 变成 1,则就代表这个锁已经被某一个线程所持有了。

FIFO 队列

这个队列最主要的作用是存储等待的线程。假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。所以 AQS 的一大功能就是充当线程的“排队管理器”。

获取/释放方法

这些方法是协作工具类的逻辑具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。

获取方法

(1)ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。

(2)Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态,所以这里同样也是和 state 的值相关的。

(3)CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。

“获取方法”在不同的类中代表不同的含义,但往往和 state 值相关,也经常会让线程进入阻塞状态,这也同样证明了 state 状态在 AQS 类中的重要地位。

释放方法

释放方法通常是不会阻塞线程的。

(1) Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1;

(2)CountDownLatch 里面,释放就是 countDown 方法,作用是倒数一个数,让 state 减 1。

在不同的实现类里面,他们对于 state 的操作是截然不同的,需要由每一个协作类根据自己的逻辑去具体实现。

4.2 AQS 用法

(1)新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer,即 AQS;

(2)想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquire 和 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法;

(3)在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。

 

 

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页