【JUC基础】05. Synchronized和ReentrantLock

简介: 前面两篇中分别讲了Synchronized和ReentrantLock。两种方式都能实现同步锁,且也都能解决多线程的并发问题。那么这两个有什么区别呢? 这个也是一个高频的面经题。

 1、前言

前面两篇中分别讲了Synchronized和ReentrantLock。两种方式都能实现同步锁,且也都能解决多线程的并发问题。那么这两个有什么区别呢? 这个也是一个高频的面经题。

image.png

2、相同点

2.1、都是可重入锁

什么是可重入锁?

可重入锁,也称为递归锁,是指同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说线程可以进入任何一个它已经拥有的锁所同步着的代码块。可重入锁是为了避免死锁而出现的一种锁机制,因为当一个线程在持有锁的同时,再次请求获取锁时,如果不是可重入锁,就会发生死锁的情况。

举个例子,当线程 A 获取了锁之后,在锁还没有释放的情况下,再次尝试获取锁时不会阻塞,而是会自动获取锁成功,直到锁的计数器归零后再释放锁。

而ReentrantLock 和 synchronized 都是可重入锁。

简单可重入锁示例:

public class ReentrantLockDemo2 {
    private static Object lock = new Object();
    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        // 使用Synchronized实现可重入锁
        synchronizedMethod();
        // 使用ReentrantLock实现可重入锁
        reentrantLockMethod();
    }
    public static void synchronizedMethod() {
        synchronized (lock) {
            System.out.println("synchronized外层加锁");
            synchronized (lock) {
                System.out.println("synchronized内层加锁");
            }
            System.out.println("synchronized释放内层锁");
        }
        System.out.println("synchronized释放外层锁");
    }
    public static void reentrantLockMethod() {
        reentrantLock.lock();
        try {
            System.out.println("reentrantLock外层加锁");
            reentrantLock.lock();
            try {
                System.out.println("reentrantLock内层加锁");
            } finally {
                reentrantLock.unlock();
                System.out.println("reentrantLock释放内层锁");
            }
        } finally {
            reentrantLock.unlock();
            System.out.println("reentrantLock释放外层锁");
        }
    }
}

image.gif

可以看到返回结果,说明两种锁都是可重入锁。

image.png

2.2、都是独占锁

即同一时间只能有一个线程获得锁,其他线程需要等待锁释放后才能获得锁。

2.3、都是阻塞式锁

当一个线程持有锁时,其他线程会被阻塞,直到锁被释放。

3、不同点

3.1、使用方式不同

3.1.1、获取锁的方式不同

使用synchronized获取锁时,只需要在方法或代码块前面加上synchronized关键字即可,Java虚拟机会自动获取锁。例如:

public synchronized void method() {
    // 代码块
}

image.gif

而使用ReentrantLock获取锁时,需要手动获取锁和释放锁。例如:

// 这里可以指定获取公平锁或非公平锁,默认非公平锁。
// 获取公平锁:ReentrantLock lock = new ReentrantLock();
private ReentrantLock lock = new ReentrantLock();
public void method() {
    lock.lock(); // 获取锁
    try {
        // 代码块
    } finally {
        lock.unlock(); // 释放锁
    }
}

image.gif

3.1.2、锁释放的方式不同

synchronized锁的释放完全交由虚拟机管理。程序中是无法显式的对锁进行释放。

    • 当线程同步方法或代码块执行结束后释放。
    • 或遇到return语句,或异常时也会自动释放锁。

    而ReentrantLock交由程序手动释放。解锁方法:lock.unlock();

    3.1.3、可中断性不同

    使用synchronized获取锁时,如果一个线程正在等待锁,那么只有等到锁被释放,才能继续执行。

    而使用ReentrantLock获取锁时,可以通过lockInterruptibly()方法让等待锁的线程响应中断信号,从而中断等待。例如:

    public void method() throws InterruptedException {
        lock.lockInterruptibly(); // 可中断地获取锁
        try {
            // 代码块
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    image.gif

    3.1.4、条件变量的支持不同

    ReentrantLock可以支持多个条件变量,每个条件变量可以管理一个等待队列。使用Condition对象来实现等待/通知机制,例如:

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void method() throws InterruptedException {
        lock.lock(); // 获取锁
        try {
            while (condition不满足) {
                condition.await(); // 等待
            }
            // 代码块
        } finally {
            lock.unlock(); // 释放锁
        }
    }
    public void signal() {
        lock.lock(); // 获取锁
        try {
            condition.signal(); // 通知
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    image.gif

    而synchronized只能使用Object类的wait()和notify()方法进行等待和通知,例如:

    public synchronized void method() throws InterruptedException {
        while (条件不满足) {
            wait(); // 等待
        }
        // 代码块
    }
    public synchronized void signal() {
        notify(); // 通知
    }

    image.gif

    3.1.5、修饰作用域不同

    Synchronized可以修饰实例方法,静态方法,代码块。

    ReentrantLock一般需要try catch finally语句,在try中获取锁,在finally释放锁。

    3.2、可重入性不同

    虽然前面讲到了两个都是可重入锁。但ReentrantLock是同一个线程可以多次获取同一个锁,而synchronized也是可重入锁,但是需要注意的是,它只能在同一个线程内部进行重入,而不是在不同线程之间。

    在Java中,可重入性是指线程获取了某个锁之后,仍然能够再次获取该锁,而不会被自己所持有的锁所阻塞。在重入时,每次都会增加一次锁的计数器,而每次解锁时,计数器也会减1,当计数器为0时,锁会被释放。

    3.2.1、Synchronized实现可重入锁的机制

    每个对象都有一个监视器锁(monitor),当线程第一次访问该对象时,会获取该对象的监视器锁,并将锁的计数器加1,然后再次进入synchronized块时,会再次获取该对象的监视器锁,此时锁的计数器再次加1,线程在退出synchronized块时,会将锁的计数器减1,当计数器为0时,锁被释放。

    public class SynchronizedDemo {
        public synchronized void methodA() {
            System.out.println("进入 methodA");
            methodB();
        }
        public synchronized void methodB() {
            System.out.println("进入 methodB");
        }
        public static void main(String[] args) {
            SynchronizedDemo demo = new SynchronizedDemo();
            demo.methodA();
        }
    }

    image.gif

    在上面的代码中,methodA和methodB都是synchronized方法,当线程第一次调用methodA时,会获取对象的监视器锁,并将锁的计数器加1,然后再次进入methodB时,会再次获取该对象的监视器锁,此时锁的计数器再次加1,当线程退出methodB时,会将锁的计数器减1,当计数器为0时,锁被释放。由于methodA和methodB都是synchronized方法,它们都使用的是同一个对象的监视器锁,因此线程可以重入这两个方法,即可重入锁。

    3.2.2、ReentrantLock实现可重入锁的机制

    每个ReentrantLock对象都有一个锁计数器和一个线程持有者,当线程第一次获取锁时,锁计数器加1,并且线程持有者是当前线程,当该线程再次获取锁时,锁计数器再次加1,当线程释放锁时,锁计数器减1,当锁计数器为0时,锁被释放。

    以下是一个使用ReentrantLock实现可重入锁的简单例子:

    public class ReentrantLockDemo {
        private ReentrantLock lock = new ReentrantLock();
        public void methodA() {
            lock.lock();
            try {
                System.out.println("进入 methodA");
                methodB();
            } finally {
                lock.unlock();
            }
        }
        public void methodB() {
            lock.lock();
            try {
                System.out.println("进入 methodB");
            } finally {
                lock.unlock();
            }
        }
        public static void main(String[] args) {
            ReentrantLockDemo demo = new ReentrantLockDemo();
            demo.methodA();
        }
    }

    image.gif

    在上面的代码中,methodA和methodB都是使用ReentrantLock实现的可重入锁。当线程第一次调用methodA时,会获取lock对象的锁,并将锁计数器加1,然后再次进入methodB时,会再次获取该锁,此时锁计数器再次加1,当线程退出methodB时,会将锁计数器减1,当计数器为0时,锁被释放。

    总的来说,Synchronized和ReentrantLock都是可重入锁,但是它们实现可重入锁的机制不同,Synchronized是基于对象监视器锁实现的,而ReentrantLock是基于锁计数器和线程持有者实现的。

    3.3、性能不同

    Synchronized是Java语言内置的关键字,通过JVM实现,因此使用起来比较简单方便。Synchronized的实现采用的是悲观锁机制,即线程在访问共享资源时,必须先获得锁,如果获取不到,就进入阻塞状态。Synchronized在获取和释放锁时,需要执行系统调用,这个过程的开销相对较大,因此在高并发的场景下,Synchronized的性能会受到影响。

    ReentrantLock的实现是基于CAS(Compare And Swap)操作和自旋锁的机制,CAS是一种无锁算法,它是通过比较当前内存值与预期内存值的方式来实现锁的获取和释放。自旋锁是一种忙等待的锁机制,当线程获取锁失败时,它不会进入阻塞状态,而是一直循环执行CAS操作,直到获取到锁。因此,ReentrantLock在高并发的场景下,相对于Synchronized有更好的性能表现。

    不过随着JDK版本的升级,Synchronized已经被优化了。优化之前synchronized的性能确实要比ReentrantLock差20%-30%,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized。

    写个简单例子验证一波:

    public class ReentrantLockDemo {
        private static final int LOOP_COUNT = 10000000;
        private static int count = 0;
        public static void main(String[] args) throws InterruptedException {
            long start = System.currentTimeMillis();
            incrementWithSynchronized();
            long end = System.currentTimeMillis();
            System.out.println("Using Synchronized, time cost: " + (end - start) + "ms, count: " + count);
            count = 0; // 重置count值
            start = System.currentTimeMillis();
            incrementWithReentrantLock();
            end = System.currentTimeMillis();
            System.out.println("Using ReentrantLock, time cost: " + (end - start) + "ms, count: " + count);
        }
        private static synchronized void incrementWithSynchronized() {
            for (int i = 0; i < LOOP_COUNT; i++) {
                count++;
            }
        }
        static ReentrantLock lock = new ReentrantLock();
        private static void incrementWithReentrantLock() throws InterruptedException {
            for (int i = 0; i < LOOP_COUNT; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    image.gif

    可以看到很有意思的差距:

    image.png

    3.4、实现原理不同

    Synchronized是Java中的一种内置锁,是Java的保留字。它是基于Java对象头中的Mark Word来实现的。每个Java对象都有一个Mark Word,其中包含了一些标识位,如锁标识位。当一个线程要访问一个被Synchronized修饰的方法或代码块时,它会尝试获取锁标识位。如果锁标识位已经被其他线程占用了,那么该线程就会进入阻塞状态,直到锁标识位被释放。

    image.png

    ReentrantLock是Java中的一种可重入锁,它的实现是基于AQS(AbstractQueuedSynchronizer)的。AQS是Java并发包中的一个基础框架,提供了一组底层的同步工具,如CountDownLatch、Semaphore、ReentrantLock等。ReentrantLock的实现依赖于AQS提供的功能,它可以在同一线程中重复获取锁,并支持公平锁和非公平锁两种模式。

    image.png

    3.5、使用场景

      • Synchronized适用于大多数的同步场景,如单线程访问、多线程串行化等。它是一种轻量级的锁,不需要用户去手动管理锁的获取和释放,具有自动释放的功能,因此使用起来比较简单,但在某些高并发的情况下,性能可能会受到影响。
      • ReentrantLock适用于一些特殊的场景,如需要中断等待、尝试获取锁而不是一直等待、可定时的等待等。

      在实际应用中,我们可以根据具体的场景来选择合适的锁机制。如果程序的并发性比较低,或者是在单线程中进行访问,那么使用Synchronized可能是更好的选择。如果程序的并发性比较高,或者需要一些高级的功能,比如可中断、可定时等,那么可以选择使用ReentrantLock。同时,在使用ReentrantLock时,需要注意手动管理锁的获取和释放,否则可能会导致死锁等问题。

      4、小结

      本片文章大幅介绍了Synchronized和ReentrantLock的区别。因为这是高频的面试题,希望通过这篇文章能够进一步熟悉对于JUC中锁的理解,同时也明白Synchronized和ReentrantLock的一些区别,在项目中不同的场景可以更好的选择适当的锁机制,提升系统的可维护性和健壮性。一起加油学习~

      相关文章
      |
      7月前
      |
      Java
      【面试问题】Synchronized 和 ReentrantLock 区别?
      【1月更文挑战第27天】【面试问题】Synchronized 和 ReentrantLock 区别?
      |
      6月前
      |
      安全 Java 开发者
      Java并发编程:深入理解synchronized和ReentrantLock
      在Java并发编程中,正确使用同步机制是确保线程安全的关键。本文将深入探讨Java内置的两种同步机制——synchronized关键字和ReentrantLock类。我们将通过权威数据、经典理论和实际案例,对比分析它们的性能、用法和适用场景,帮助开发者做出明智的选择。
      44 0
      |
      4月前
      |
      消息中间件 存储 监控
      Java并发知识之ReentrantLock
      本文深入剖析了Java中并发编程的核心概念,特别聚焦于锁的设计思想,通过分析AbstractQueuedSynchronizer(AQS)、ReentrantLock和ReentrantReadWriteLock的实现,揭示了锁的工作原理和高效并发控制策略。
      Java并发知识之ReentrantLock
      |
      4月前
      |
      安全 Java
      Java并发编程实战:使用synchronized和ReentrantLock实现线程安全
      【8月更文挑战第31天】在Java并发编程中,保证线程安全是至关重要的。本文将通过对比synchronized和ReentrantLock两种锁机制,深入探讨它们在实现线程安全方面的优缺点,并通过代码示例展示如何使用这两种锁来保护共享资源。
      |
      6月前
      |
      Java
      Java并发编程:深入理解synchronized与ReentrantLock
      【6月更文挑战第22天】本文将深入探讨Java并发编程中两个重要的同步机制:synchronized关键字和ReentrantLock类。我们将通过实例分析它们之间的差异,以及在实际应用中如何根据场景选择恰当的同步工具。
      |
      5月前
      |
      安全 Java 开发者
      Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
      Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
      63 0
      |
      5月前
      |
      安全 Java
      Java多线程中的锁机制:深入解析synchronized与ReentrantLock
      Java多线程中的锁机制:深入解析synchronized与ReentrantLock
      91 0
      |
      7月前
      |
      安全 Java 程序员
      Java多线程基础-17:简单介绍一下JUC中的 ReentrantLock
      ReentrantLock是Java并发包中的可重入互斥锁,与`synchronized`类似但更灵活。
      61 0
      |
      7月前
      synchronized与ReentrantLock区别与联系
      synchronized与ReentrantLock区别与联系
      42 0
      |
      程序员
      ReentrantLock与synchronized的区别
      ReentrantLock与synchronized的区别