悲观锁顾名思义就是持有悲观态度,线程每次进入临界区处理数据时,都认为数据很容易被其他线程修改。
所以,在线程进入临界区前,会用户锁锁住临界区的资源,并在处理数据的过程中一直保持锁定状态。其他线程由于无法获取到对应的资源,就会阻塞等待,直到获取锁的线程释放锁,等待的线程才能获取到锁。
Java 中的 synchronized 重量级锁就是一种典型的悲观锁。
乐观锁顾名思义就是持有乐观的态度,认为每次访问数据的时候其他线程都不会修改数据。
所以,在访问数据的时候,不会对数据进行加锁操作。当涉及对数据更新的操作时,会检测数据是否被其他线程修改过:如果数据没有被修改过,则当前线程提交更新操作;如果数据被其他线程修改过,则当前线程会尝试再次读取数据,检测数据是否被其他线程修改过,如果再次检测的结果仍然为数据已经被其他线程修改过,则会再次尝试读取数据,如此反复,直到检测到的数据没有被其他线程修改过。
乐观锁在具体实现时,一般会采用版本号机制,先读取数据的版本号,在写数据时比较版本号是否一致,如果版本号一致则更新数据,否则再次读取版本号,比较版本号是否一致,直到版本号一致时更新数据。
Java 中的乐观锁一般都是基于 CAS 自旋实现的。在 Java 中,CAS 是一种原子操作,底层调用的是硬件层面的比较并交换的逻辑。在实现时,会比较当前值与传入的期望值是否相同,如果相同,则把当前值修改为目标值,否则不修改。
Java 中的 synchronized 轻量级锁属于乐观锁,是基于抽象队列同步器(AQS)实现的锁,如 ReentrantLock 等。
临界区:临界区一般表示能够被多个线程共享的资源或数据,但是每次只能提供给一个线程使用。临界区资源一旦被占用,其他线程就必须等待。
在并发编程中,临界区一般指受保护的对象或者程序代码片段,可以通过加锁的方式保证每次只有一个线程进入临界区,从而达到保护临界区的目的。
公平锁的核心思想就是公平,能够保证各个线程按照顺序获取锁,也就是“先来先获取”的原则。
例如,存在三个线程,分别为线程 1 、线程 2 和线程 3 ,并依次获取锁。首先,线程 1 获取锁,线程 2 和线程 3 阻塞等待,线程 1 执行完任务释放锁。然后,线程 2 唤醒并获取锁,执行完任务释放锁。最后,线程 3 被唤醒并获取锁,执行完任务释放锁。这就是公平锁的流程。
非公平锁的核心思想就是每个线程获取锁的机会是不平等的,也是不公平的。先抢占锁的线程不一定能够先获取锁。
例如,存在三个线程,分别为线程 1 、线程 2 和线程 3 ,在线程 1 和线程 2 抢占锁的过程中,线程 1 获取到锁,线程 2 阻塞等待。线程 1 执行完任务释放锁后,在唤醒线程 2 时,线程 3 尝试抢占锁,则线程 3 是可以获取到锁的。这就是非公平锁。
在 Java 中,ReentrantLock 默认的实现为非公平锁,也可以在构造方法中传入 true 来创建公平锁对象。
独占锁也叫排他锁,在多个线程争抢锁的过程中,无论是读操作还是写操纵,只能有一个线程获取到锁,其他线程阻塞等待,独占锁采取的是悲观保守策略。
独占锁的缺点是无论对于读操作还是写操作,都只能有一个线程获取锁。但是读操作不会修改数据,如果当读操作线程获取锁时其他的读线程被阻塞,就会大大降低系统的读性能。此时,就需要用到共享锁了。
共享锁允许多个读线程同时获取临界区资源,它采取的是乐观锁的机制。共享锁会限制写操作与写操作之间的竞争,也会限制写操作与读操作之间的竞争,但是不会限制读操作与读操作之间的竞争。
在 Java 中,ReentrantLock 是一种独占锁,而 ReentrantReadWriteLock 可以实现读/写锁的分离,允许多个读操作同时获取读锁。
可重入锁也叫递归锁,指同一个线程可以多次占用同一个锁,但是在解锁时,需要执行相同次数的解锁操作。
例如,线程 A 在执行任务的过程中获取锁,在后续执行任务的过程中,如果遇到抢占同一个锁的情况,则也会再次获取锁。
不可重入锁与可重入锁在逻辑上是相反的,指一个线程不能多次占用同一个锁。
例如,线程 A 在执行任务的过程中获取锁,在后续执行任务的过程中,如果遇到抢占同一个锁的情况,则不能再次获取锁。只有先释放锁,才能再次获取该锁。
在 Java 中,ReentrantLock 就是一种可重入锁。
可中断锁与不可中断锁主要指线程在阻塞等待的过程中,能否中断自己阻塞等待的状态。
可中断锁指锁被其他线程获取后,某个线程在阻塞等待的过程中,可能由于等待的时间过长而中断阻塞等待的状态,去执行其他任务。
不可中断锁指锁被其他线程获取后,某个线程如果也想获取这个锁,就只能阻塞等待。如果占用锁的线程一致不释放锁,其他想获取锁的线程就会一直阻塞等待。
在 Java 中,ReentrantLock 是一种可中断锁,synchronized 则是一种不可中断锁。
读/写锁分别为读写和写锁,当持有读锁时,能够对共享资源进行读操作,当持有写锁的时,能够对共享资源进行写操作。写操作具有排他性,读锁具有共享性。在同一时刻,一个读/写锁只允许一个线程进行写操作,可以允许多个线程进行读操作。
当某个线程试图获取写锁时,如果发现其他线程已经获取到写锁或者读锁,则当前线程会阻塞等待,直到任何线程不再持有写锁或者读锁。
当某个线程试图获取读锁时,如果发现其他线程已经获取到读锁,则这个线程会直接获取到读锁。
当某个线程试图获取读锁时,如果发现其他线程已经获取到写锁,则这个线程会阻塞等待,直到占有写锁的线程释放锁。
在 Java 中,ReadWriteLock 是一种读/写锁。
自旋锁指某个线程在没有获取到锁时,不会立即进入阻塞等待状态,而是不断尝试获取锁,直到占有锁的线程释放锁。
自旋锁可能引起死锁和占用 CPU 时间过长的问题。
程序不能在占用自旋锁时调用自己,也不能在递归调用时获取相同的自旋锁,可以在一定程度上避免死锁。
当某个线程进入不断尝试获取锁的循环时,可以设定一个循环时间或循环次数,超过这个时间或者次数,就让线程进入阻塞等待的状态,在一定程度上可以有效的避免长时间占用 CPU 的问腿。
在 Java 中,CAS 是在一种自旋锁。
上一篇:自监督学习