Java中的锁机制

Java中的锁机制

在并发环境下,java提供了同步锁保证线程同步,主要为synchornized关键字和Lock类。在此基础上,提供丰富的锁机制保证线程执行效率。
在这里简单介绍相关的机制,不做深入分析。

乐观锁与悲观锁

乐观锁

乐观锁认为获取的资源不会被其他线程修改,因此,在并发环境下不会对资源进行加锁。乐观锁在完成操作写入数据的时候对原值和当前值做比较,如果二者一致,则直接写入数据。否则,根据实现策略不同,执行报错或重试。

在java中,乐观锁一般通过CAS算法实现。

悲观锁

悲观锁认为获取的资源一定会被其他线程修改,因此,在并发环境下获取资源时会对资源加锁,锁定期间其他线程无法访问资源。

在java中,悲观锁一般通过 synchornized 关键字或者 Lock 类实现。

自旋锁与非自旋锁

当一段添加同步锁的代码片段被多个线程竞争时,其中一个线程获取到锁执行同步代码,此时其他线程由于没有竞争到锁而处于阻塞状态。这种情况下,线程状态由 RUNNING 转变为 BLOCKED ,线程被添加到一个等待队列中等待重新获取锁。这里存在一个问题,如果被锁定的代码片段的执行时间很短,即每个线程获取锁的时间很短,那么会出现大量的线程频繁的进行 RUNNINGBLOCKED 再到 RUNNING 的状态的切换,导致大量的处理器时间被浪费。

针对以上情形,java提供自旋锁以避免线程状态切换。简单理解,对于锁定时间很短的资源,未获取到锁的线程不会进行状态切换,而是一直处于 RUNNING 状态在原地等待并尝试获重新取锁,当锁的持有者释放锁之后,可以立即获取到资源并执行逻辑处理。

在使用自旋锁之后,能有效地减少处理器时间的浪费,但是同时,由于未获取到锁的线程需要在原地等待,从而会导致CPU性能浪费。

  • 使用synchornized模拟自旋锁
import java.util.concurrent.locks.LockSupport;

public class SpinLockDemo {

    private final Object lock = new Object();
    private Thread owner = null;

    public void lock() {
        Thread current = Thread.currentThread();
        while (!tryLock(current)) {
            // 自旋等待
            LockSupport.parkNanos(1); // 可以模拟自旋
        }
    }

    public void unlock() {
        synchronized (lock) {
            if (Thread.currentThread() == owner) {
                owner = null;
                lock.notifyAll(); // 唤醒等待的线程
            }
        }
    }

    private boolean tryLock(Thread current) {
        synchronized (lock) {
            if (owner == null) {
                owner = current;
                return true;
            }
            return false;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo demo = new SpinLockDemo();

        Thread threadA = new Thread(() -> {
            demo.lock();
            try {
                System.out.println("Thread A acquired the lock.");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                demo.unlock();
                System.out.println("Thread A released the lock.");
            }
        }, "Thread-A");

        Thread threadB = new Thread(() -> {
            demo.lock();
            try {
                System.out.println("Thread B acquired the lock.");
            } finally {
                demo.unlock();
                System.out.println("Thread B released the lock.");
            }
        }, "Thread-B");

        threadA.start();
        Thread.sleep(100); // 让线程 A 先启动
        threadB.start();

        threadA.join();
        threadB.join();
    }
}

无锁、偏向锁、轻量级锁、重量级锁

无锁(Lock-Free)、偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)和重量级锁(Heavyweight Locking)是Java提供的不同层次的锁机制。

无锁

无锁是指不使用传统的锁机制来实现线程安全的操作。它通常依赖于原子操作(如 CAS, Compare-And-Swap)来确保多个线程可以同时访问共享资源而不会产生数据竞争。

使用CAS等算法实现无锁操作,可以有效地减少处理器时间占用,避免线程阻塞和上下文切换带来的性能损耗。

偏向锁

偏向锁是对同步锁的一种优化措施,假设锁只会被一个线程持有。当第一次获取锁时,JVM 会将锁“偏向”给该线程,之后该线程再次获取锁时无需进行额外的同步操作。

在使用偏向锁之后,如果另外一个线程试图获取锁,则撤销偏向锁而转为更高级的锁。

偏向锁实现原理

初次获取锁: 当一个线程第一次获取某个对象的锁时,JVM 会将该锁“偏向”给这个线程。这意味着在之后的锁获取操作中,只要还是同一个线程,它就可以直接使用锁而不需要进行额外的同步操作。

锁竞争发生: 如果另一个线程尝试获取已经被偏向的锁,JVM 会检测到这种竞争并执行以下步骤:

  • 撤销偏向: 停止对该锁的偏向。
  • 锁膨胀: 将锁升级为轻量级锁(自旋锁),甚至进一步升级为重量级锁,具体取决于当前的竞争情况。

后续行为: 一旦锁被撤销偏向,它通常不会再回到偏向状态,直到下次重新初始化锁为止。

偏向锁控制

在JVM中,默认开启了偏向锁,可以通过参数控制关闭偏向锁:

# 禁用偏向锁
-XX:-UseBiasedLocking

# 开启偏向锁
-XX:+UseBiasedLocking

在少量线程频繁获取同一把锁的时候,使用偏向锁能有效提高系统性能。但是对于多线程高并发环境下,必然存在多个线程获取同一把锁的情况,此时再启用偏向锁会导致不必要的锁切换,反而会影响系统性能。

公平锁与非公平锁

refs:

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

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

直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。

如上图所示,假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。

但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。如下图所示:

img

著作权归@pdai所有
原文链接:https://pdai.tech/md/java/thread/java-thread-x-lock-all.html

共享锁与排他锁

共享锁(Shared Lock)和排他锁(Exclusive Lock),也分别称为读锁和写锁,它们的主要区别在于是否允许多个线程或进程同时访问资源。

共享锁

共享锁 允许多个线程或进程同时持有该锁,以进行只读操作。只要没有线程持有排他锁,任何数量的线程都可以获得共享锁。

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();

readLock.lock();
try {
    // 执行读操作
} finally {
    readLock.unlock();
}

排他锁

排他锁 确保在同一时刻只有一个线程能够持有该锁,用于执行写操作。它不仅阻止其他线程获取排他锁,也阻止它们获取共享锁。排他锁在任何时间点上只能有一个线程持有排他锁,其他所有尝试获取锁的线程都必须等待当前线程释放锁。

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock writeLock = rwLock.writeLock();

writeLock.lock();
try {
    // 执行写操作
} finally {
    writeLock.unlock();
}

类似文章