自学内容网 自学内容网

java多线程——synchronized的偏向锁、轻量级锁和重量级锁

在Java中,synchronized关键字是用于实现线程同步的一种机制,它提供了偏向锁、轻量级锁和重量级锁三种锁状态来优化同步性能。以下是这三种锁的原理和使用方法的详细解释:

一、偏向锁(Biased Locking)

  1. 原理

    • 偏向锁的目的是为了优化只有一个线程访问同步块的场景。在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁会在对象头(Mark Word)中存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。
    • 偏向锁的线程会检查锁对象的Mark Word中是否存放着自己的线程ID。如果存放的是当前线程的ID,表示偏向锁现在就是偏向当前线程的,无需再尝试获得锁,可以直接进入同步块。如果不是当前线程的ID,则会发生竞争,当前线程会尝试通过CAS(Compare-And-Swap)操作更新锁对象Mark Word中的线程ID。
    • 如果更新成功,锁重新偏向为当前线程,锁仍然为偏向锁。如果更新失败,表示之前的线程还在持有锁,那么当前线程会进行CAS自旋操作,当达到一定次数后还没成功,则会发生偏向锁到轻量级锁的升级。
  2. 使用方法

    • 偏向锁是Java虚拟机(JVM)在JDK 1.6及以后版本中默认启用的优化机制,无需开发者显式使用。但可以通过JVM参数-XX:+UseBiasedLocking来显式启用或-XX:-UseBiasedLocking来禁用偏向锁。
    • 偏向锁在应用程序启动几秒钟(默认延迟4秒)之后才会激活,可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,让其在程序启动时立刻启动。

二、轻量级锁

  1. 原理

    • 轻量级锁是为了减少线程从内核态和用户态的切换而设计的,适用于多线程竞争不激烈的情况。它通过CAS机制来竞争锁,避免了重量级锁产生的性能消耗。
    • 当一个线程尝试获取轻量级锁时,JVM会首先在抢锁线程的栈帧中建立一个锁记录(Lock Record),用于存储对象目前的Mark Word拷贝。然后,抢锁线程通过自旋操作尝试将内置锁对象头的Mark Word的锁记录指针(ptr_lock_record)更新为抢锁线程中锁记录的地址。如果更新成功,这个线程就拥有了这个对象锁。
    • 轻量级锁主要有两种:普通自旋锁和自适应自旋锁。普通自旋锁就是当线程来竞争锁时,抢锁线程会原地等待,直到那个占有锁的线程释放锁。自适应自旋锁则会根据线程之前获取锁的成功率来动态调整自旋次数。
  2. 使用方法

    • 轻量级锁也是JVM默认启用的优化机制,无需开发者显式使用。
    • 开发者可以通过JVM参数来调整自旋次数,例如-XX:PreBlockSpin来设置普通自旋锁的自旋次数。

三、重量级锁

  1. 原理

    • 重量级锁通过JVM中的监视器(Monitor)来实现,它保证了任何时间内只允许一个线程通过监视器保护的临界区代码。
    • 当一个线程尝试获取重量级锁时,如果该锁已被其他线程持有,则当前线程会被阻塞,并放入竞争队列(Cxq)中等待。当持有锁的线程释放锁时,JVM会从竞争队列中唤醒一个线程来竞争锁。竞争成功的线程会成为新的持有者,并继续执行临界区代码。
    • 重量级锁会导致线程在用户态和核心态之间切换,带来较大的性能损耗。因此,在多线程竞争激烈的情况下,应尽量避免使用重量级锁。
  2. 使用方法

    • 重量级锁是synchronized关键字在竞争激烈情况下的默认行为,无需开发者显式使用。
    • 开发者可以通过优化代码逻辑、减少同步块的范围或使用其他并发工具(如ReentrantLock、Semaphore等)来避免重量级锁的使用。

四、代码举例 

一、偏向锁代码示例

偏向锁是JVM在JDK 1.6及以后版本中默认启用的优化机制,无需开发者显式使用。但可以通过JVM参数来启用或禁用偏向锁,并观察其状态变化。

public class BiasedLockingDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 禁用偏向锁延迟,使偏向锁立即生效
        System.setProperty("java.vm.ci.compilerThreshold", "0");
        // 通过JVM参数设置立即生效偏向锁
        // -XX:BiasedLockingStartupDelay=0 需要在启动JVM时设置

        // 打印锁对象的初始状态
        printLockState(lock);

        // 持有偏向锁
        synchronized (lock) {
            // 打印锁对象在持有偏向锁时的状态
            printLockState(lock);
        }

        // 打印锁对象在释放偏向锁后的状态
        printLockState(lock);
    }

    // 使用jol工具打印锁对象的状态
    // 需要添加jol依赖,并在代码中引入相关类
    // <dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version></dependency>
    private static void printLockState(Object obj) {
        // 使用jol的ClassLayout.parseInstance方法打印对象内部状态
        // 这里省略了具体的jol代码实现,需要自行添加
        // System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

注意:上述代码中的printLockState方法需要使用JOL(Java Object Layout)工具来实现,该工具可以打印出Java对象的内部布局和状态。由于JOL工具的使用涉及较多细节,这里省略了具体的代码实现,需要开发者自行添加。

另外,偏向锁的状态变化可以通过观察锁对象的Mark Word来确认。在JVM中,Mark Word存储了对象的哈希码、分代年龄、锁标记位等信息。当偏向锁被启用时,Mark Word中会存储偏向线程的ID。

二、轻量级锁代码示例

轻量级锁也是JVM默认启用的优化机制,无需开发者显式使用。但可以通过观察多线程竞争锁的行为来间接了解轻量级锁的工作原理。

public class LightweightLockingDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                // 模拟同步块中的操作
                try {
                    Thread.sleep(100); // 让出CPU时间片,模拟其他线程竞争锁
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Thread 1 holds the lock");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 2 holds the lock");
            }
        });

        t1.start();
        t2.start();
    }
}

在这个示例中,两个线程t1t2竞争同一个锁对象lock。由于轻量级锁的存在,当t1持有锁时,t2会尝试通过CAS操作来获取锁。如果t1很快释放锁,t2可能会成功获取锁并打印出“Thread 2 holds the lock”。如果t1持有锁时间较长,t2可能会进入自旋等待状态,直到t1释放锁。

三、重量级锁代码示例

重量级锁是多线程竞争激烈时的默认行为,也无需开发者显式使用。但可以通过构造一个多线程竞争锁的场景来观察重量级锁的行为。

public class HeavyweightLockingDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Runnable task = () -> {
            synchronized (lock) {
                // 模拟同步块中的长时间操作
                try {
                    Thread.sleep(1000); // 让线程持有锁一段时间
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println(Thread.currentThread().getName() + " holds the lock");
            }
        };

        // 启动多个线程来竞争锁
        for (int i = 0; i < 10; i++) {
            new Thread(task, "Thread-" + i).start();
        }
    }
}

在这个示例中,启动了10个线程来竞争同一个锁对象lock。由于线程数量较多,且每个线程都会持有锁一段时间(通过Thread.sleep模拟),因此会导致多线程竞争激烈。在这种情况下,JVM可能会将轻量级锁升级为重量级锁,以保证线程同步的正确性。

注意:上述代码示例中的锁升级行为是JVM内部实现的,开发者无法直接观察到锁的具体升级过程。但可以通过观察程序的运行情况和性能表现来间接了解锁升级的影响。例如,当多线程竞争激烈时,程序的响应时间可能会变长,CPU使用率可能会升高,这些都是重量级锁带来的性能损耗的表现。 

五、总结

  • 偏向锁:适用于只有一个线程访问同步块的场景,通过存储锁偏向的线程ID来优化性能。在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
  • 轻量级锁:适用于多线程竞争不激烈的情况,通过CAS机制和自旋操作来竞争锁。由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  • 重量级锁:适用于多线程竞争激烈的情况,通过监视器来保证线程同步,但会导致较大的性能损耗。如果次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞

 


原文地址:https://blog.csdn.net/oopxiajun2011/article/details/144036665

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!