1、前言
前面两篇中分别讲了Synchronized和ReentrantLock。两种方式都能实现同步锁,且也都能解决多线程的并发问题。那么这两个有什么区别呢? 这个也是一个高频的面经题。
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释放外层锁"); } } }
可以看到返回结果,说明两种锁都是可重入锁。
2.2、都是独占锁
即同一时间只能有一个线程获得锁,其他线程需要等待锁释放后才能获得锁。
2.3、都是阻塞式锁
当一个线程持有锁时,其他线程会被阻塞,直到锁被释放。
3、不同点
3.1、使用方式不同
3.1.1、获取锁的方式不同
使用synchronized获取锁时,只需要在方法或代码块前面加上synchronized关键字即可,Java虚拟机会自动获取锁。例如:
public synchronized void method() { // 代码块 }
而使用ReentrantLock获取锁时,需要手动获取锁和释放锁。例如:
// 这里可以指定获取公平锁或非公平锁,默认非公平锁。 // 获取公平锁:ReentrantLock lock = new ReentrantLock(); private ReentrantLock lock = new ReentrantLock(); public void method() { lock.lock(); // 获取锁 try { // 代码块 } finally { lock.unlock(); // 释放锁 } }
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(); // 释放锁 } }
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(); // 释放锁 } }
而synchronized只能使用Object类的wait()和notify()方法进行等待和通知,例如:
public synchronized void method() throws InterruptedException { while (条件不满足) { wait(); // 等待 } // 代码块 } public synchronized void signal() { notify(); // 通知 }
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(); } }
在上面的代码中,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(); } }
在上面的代码中,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(); } } } }
可以看到很有意思的差距:
3.4、实现原理不同
Synchronized是Java中的一种内置锁,是Java的保留字。它是基于Java对象头中的Mark Word来实现的。每个Java对象都有一个Mark Word,其中包含了一些标识位,如锁标识位。当一个线程要访问一个被Synchronized修饰的方法或代码块时,它会尝试获取锁标识位。如果锁标识位已经被其他线程占用了,那么该线程就会进入阻塞状态,直到锁标识位被释放。
ReentrantLock是Java中的一种可重入锁,它的实现是基于AQS(AbstractQueuedSynchronizer)的。AQS是Java并发包中的一个基础框架,提供了一组底层的同步工具,如CountDownLatch、Semaphore、ReentrantLock等。ReentrantLock的实现依赖于AQS提供的功能,它可以在同一线程中重复获取锁,并支持公平锁和非公平锁两种模式。
3.5、使用场景
- Synchronized适用于大多数的同步场景,如单线程访问、多线程串行化等。它是一种轻量级的锁,不需要用户去手动管理锁的获取和释放,具有自动释放的功能,因此使用起来比较简单,但在某些高并发的情况下,性能可能会受到影响。
- ReentrantLock适用于一些特殊的场景,如需要中断等待、尝试获取锁而不是一直等待、可定时的等待等。
在实际应用中,我们可以根据具体的场景来选择合适的锁机制。如果程序的并发性比较低,或者是在单线程中进行访问,那么使用Synchronized可能是更好的选择。如果程序的并发性比较高,或者需要一些高级的功能,比如可中断、可定时等,那么可以选择使用ReentrantLock。同时,在使用ReentrantLock时,需要注意手动管理锁的获取和释放,否则可能会导致死锁等问题。
4、小结
本片文章大幅介绍了Synchronized和ReentrantLock的区别。因为这是高频的面试题,希望通过这篇文章能够进一步熟悉对于JUC中锁的理解,同时也明白Synchronized和ReentrantLock的一些区别,在项目中不同的场景可以更好的选择适当的锁机制,提升系统的可维护性和健壮性。一起加油学习~