什么是公平锁,什么是非公平锁,两者有什么区别?


公平锁和非公平锁是并发编程中的概念,用于描述线程获取锁的方式和顺序。它们的区别在于线程在竞争锁时的公平性。

  1. 公平锁(Fair Lock):公平锁是指多个线程按照申请锁的顺序获取锁,即按照线程的先后顺序来排队获取锁。当一个线程释放锁后,等待时间最长的线程会获得锁的访问权。公平锁能够保证每个线程都有机会获取到锁,避免饥饿现象的发生。
  2. 非公平锁(Unfair Lock):非公平锁是指多个线程获取锁的顺序是不确定的,不按照申请锁的顺序来排队。一个线程在等待锁时,不管自己是不是在等待队列的头部,都有机会在其他线程释放锁后立即获取锁。非公平锁允许某些线程相对于其他线程具有更高的获取锁的机会,可能会导致某些线程长时间地无法获取到锁,产生饥饿现象。

两者的区别在于线程获取锁的顺序和公平性。公平锁保证了线程按照申请锁的顺序获取锁,公平性较高,但可能会导致线程等待时间较长。非公平锁允许线程插队获取锁,提高了整体的吞吐量,但可能会导致某些线程一直无法获取到锁。

在Java中,ReentrantLock类可以作为公平锁或非公平锁来使用,通过构造函数的参数来指定。默认情况下,ReentrantLock是非公平锁。



什么是可重入锁?


可重入锁(Reentrant Lock)是一种特殊类型的锁,也称为递归锁。它允许同一个线程多次获取同一个锁,而不会造成死锁。当一个线程已经持有锁时,它可以再次获取该锁而不被阻塞,而其他线程在获取该锁时会被阻塞,直到该线程释放锁。

可重入锁的主要特点是线程可以重复获取锁,每次获取锁后,锁的持有计数会加1,每次释放锁后,锁的持有计数会减1。只有当锁的持有计数为0时,其他线程才能获取该锁。这样可以避免同一个线程在递归调用或嵌套方法中重复获取锁而造成死锁。

可重入锁的一个典型应用场景是在一个线程中调用了一个同步方法,而该同步方法又调用了同一个类中的另一个同步方法。在这种情况下,如果没有可重入锁的支持,线程在调用第二个同步方法时会被自己持有的锁阻塞,导致死锁。而可重入锁允许线程重复获取锁,避免了死锁的问题。

在Java中,ReentrantLock类就是可重入锁的一种实现。它提供了与synchronized关键字类似的功能,但更加灵活和可控。通过ReentrantLock,可以实现更复杂的线程同步和互斥操作,并提供了额外的特性,如可定时的、可中断的锁等。



什么是自旋锁?


自旋锁(Spin Lock)是一种线程同步的机制,与传统的互斥锁(如互斥量)不同,自旋锁不会将线程阻塞在等待锁的位置,而是通过循环忙等(自旋)的方式尝试获取锁,直到成功获取为止。

当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,则该线程不会被阻塞,而是会在一个循环中不断地检查锁是否被释放。这种自旋的行为可以避免线程切换的开销,因为线程不需要进入阻塞状态,而是一直处于活跃状态等待锁的释放。只有当自旋锁的持有者释放锁时,等待获取锁的线程才能成功获取锁并继续执行。

自旋锁适用于以下情况:

  • 锁被持有的时间很短,期望通过自旋等待锁的释放来避免线程切换的开销。
  • 线程在获取锁时,锁的竞争情况较为轻微,即很少发生锁的争用。
  • 系统具有多处理器或多核心,使得自旋等待期间可以让其他线程继续执行。

需要注意的是,自旋锁在多核心或多处理器系统中才能发挥较好的效果。如果系统只有一个处理器,自旋等待的线程会一直占用处理器资源,导致其他线程无法执行,这种情况下使用自旋锁反而会降低性能。

在Java中,自旋锁的一种实现是AtomicInteger类的compareAndSet()方法,它使用了硬件级别的原子操作来实现自旋等待。此外,Java 5及以上版本还引入了java.util.concurrent包中的SpinLock类,提供了更完整的自旋锁实现。


自旋锁代码示例

可以使用AtomicBoolean来实现一个简单的自旋锁。下面是一个基本的自旋锁代码示例:

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 自旋等待锁释放
        }
    }

    public void unlock() {
        locked.set(false);
    }
}

在上面的代码中,AtomicBoolean用于表示锁的状态,locked变量初始化为false表示锁是未被占用的状态。lock()方法使用一个循环来自旋等待锁的释放,直到成功获取到锁。compareAndSet(false, true)方法会尝试将locked变量从false设置为true,如果设置成功,则表示获取到了锁。unlock()方法将锁的状态设置为false,表示锁被释放。

你可以在多线程环境下使用这个自旋锁,例如:

public class Main {
    private static SpinLock spinLock = new SpinLock();
    private static int counter = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                spinLock.lock();
                counter++;
                spinLock.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                spinLock.lock();
                counter--;
                spinLock.unlock();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Counter: " + counter);
    }
}

在上面的示例中,我们创建了两个线程,一个线程对counter进行增加操作,另一个线程对counter进行减少操作。在每个操作之前,线程会先获取自旋锁,然后执行操作,最后释放自旋锁。通过自旋锁的使用,可以保证对counter的操作是线程安全的。

通过使用自旋锁,线程在获取锁的过程中不会进入阻塞状态,而是通过不断地自旋等待锁的释放。这可以有效地减少线程的切换开销,并提高程序的性能。

需要注意,自旋锁适用于短时间内持有锁的情况,如果锁的持有时间较长或者线程竞争激烈,自旋等待的时间会很长,影响性能。在这种情况下,可以考虑使用其他更高级的同步机制,如ReentrantLock、Semaphore等。


今夜有点儿凉
40 声望3 粉丝

今夜有点儿凉,乌云遮住了月亮。