在之前的文章中,我们学习了Java并发中使用频率最高的同步锁ReentrantLock和synchronized。这两种同步机制在很多大厂的面试题中都是经典考点,尤其是一个经典的问题是:“ReentrantLock 与 synchronized的异同点对比!”今天,我们将针对这一考题进行尽可能全面的总结。
ReentrantLock 与 synchronized
ReentrantLock是一种独占式的可重入锁,位于java.util.concurrent.locks中,是Lock接口的默认实现类。它与synchronized关键字类似,但更加灵活、功能更强大,因此是目前实战中使用频率非常高的同步类。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized
是依赖于JVM实现的。虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReentrantLock
是在JDK层面实现的(即API层面,需要lock()和unlock()方法配合try/finally语句块来完成)。ReentrantLock比synchronized增加了一些高级功能。
区别罗列
- ReentrantLock是一个类,而synchronized是Java中的关键字;
- ReentrantLock必须手动释放锁。通常需要在finally块中调用unlock方法以确保锁被正确释放;
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。公平锁指的是先等待的线程先获得锁;
- synchronized会自动释放锁,当同步块执行完毕时,由JVM自动释放,不需要手动操作;
- ReentrantLock可以实现多路选择通知(可以绑定多个Condition),而synchronized只能通过wait和notify/notifyAll方法唤醒一个线程或者唤醒全部线程(单路通知);
- ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。而synchronized不具备这种特点;
- ReentrantLock通常提供更好的性能,特别是在高竞争环境下;
- synchronized在某些情况下,性能可能稍差一些,但随着JDK版本的升级,性能差距已经不大了。
【注】: Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能,即在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活,我们在后面的学习中会详细探讨这一点!
性能对比
尽管在JDK1.6之后,synchronized的性能得到了很大的提升,但是相比之下,两者之间仍然存在性能差异。我们通过一个小的演示来测试一下。
public class Test {
private static final int NUM_THREADS = 10;
private static final int NUM_INCREMENTS = 1000000;
private int count1 = 0;
private int count2 = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Object syncLock = new Object();
public void increment1() {
lock.lock();
try {
count1++;
} finally {
lock.unlock();
}
}
public void increment2() {
synchronized (syncLock) {
count2++;
}
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
// ReentrantLock性能测试
long startTime = System.nanoTime();
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
test.increment1();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.nanoTime();
System.out.println("ReentrantLock完成时间: " + (endTime - startTime) + " ns");
// synchronized性能测试
startTime = System.nanoTime();
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
test.increment2();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
endTime = System.nanoTime();
System.out.println("synchronized完成时间: " + (endTime - startTime) + " ns");
}
}
我们采用10个线程,每个线程执行1000000次操作,执行时间对比如下:
//1000000次操作时
ReentrantLock完成时间: 272427700 ns
synchronized完成时间: 675759100 ns
//10000次操作时
ReentrantLock完成时间: 52207600 ns
synchronized完成时间: 11291600 ns
很明显在数据量较大且竞争激烈的情况下,ReentrantLock的性能要比synchronized好很多,但在数据量较小的情况下,结果可能会有所不同。
结尾彩蛋
如果本篇博客对您有一定的帮助,请记得 留言+点赞+收藏 。原创不易,转载请联系Build哥!
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!