synchronized 和 ReentrantLock的区别
synchronized 和 ReentrantLock的区别
在讨论synchronized 和 ReentrantLock的区别前,我们先了解一下什么是公平锁和非公平锁。
一、 公平锁和非公平锁
从公平的角度来说,Java 中的锁总共可分为两类:公平锁和非公平锁。但公平锁和非公平锁有哪些区别?孰优孰劣呢?在 Java 中的应用场景又有哪些呢?接下来我们一起来看。
1. 定义
公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
2. 代码演示
接下来我们使用 ReentrantLock 来演示一下公平锁和非公平锁的执行差异,首先定义一个公平锁,开启 3 个线程,每个线程执行两次加锁和释放锁并打印线程名的操作,如下代码所示:
非公平锁:
1 public class ReentrantLockFairTest { 2 static Lock lock = new ReentrantLock(); 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 3; i++) { 5 new Thread(() -> { 6 for (int j = 0; j < 2; j++) { 7 lock.lock(); 8 try { 9 System.out.println("当前线程:" + Thread.currentThread().getName()); 10 } finally { 11 lock.unlock(); 12 } 13 } 14 }).start(); 15 } 16 } 17 } 18 19 //执行结果 20 当前线程:Thread-0 21 当前线程:Thread-0 22 当前线程:Thread-1 23 当前线程:Thread-1 24 当前线程:Thread-2 25 当前线程:Thread-2
公平锁:
1 public class ReentrantLockUnfairTest { 2 static Lock lock = new ReentrantLock(true); 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 3; i++) { 5 new Thread(() -> { 6 for (int j = 0; j < 2; j++) { 7 lock.lock(); 8 try { 9 System.out.println("当前线程:" + Thread.currentThread().getName()); 10 } finally { 11 lock.unlock(); 12 } 13 } 14 }).start(); 15 } 16 } 17 } 18 19 //执行结果 20 当前线程:Thread-0 21 当前线程:Thread-1 22 当前线程:Thread-2 23 当前线程:Thread-0 24 当前线程:Thread-1 25 当前线程:Thread-2
说明:
从上述结果可以看出,使用公平锁线程获取锁的顺序是:A -> B -> C -> A -> B -> C,也就是按顺序获取锁。而非公平锁,获取锁的顺序是 A -> A -> B -> B -> C -> C,原因是所有线程都争抢锁时,因为当前执行线程处于活跃状态,其他线程属于等待状态(还需要被唤醒),所以当前线程总是会先获取到锁,所以最终获取锁的顺序是:A -> A -> B -> B -> C -> C。
3. 执行流程分析
1)公平锁执行流程
获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
2)非公平锁执行流程
当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
公平锁和非公平锁的性能测试结果如下,以下测试数据来自于《Java并发编程实战》:
说明:从上述结果可以看出,使用非公平锁的吞吐率(单位时间内成功获取锁的平均速率)要比公平锁高很多。
4. 公平锁和非公平锁有何区别
公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。
1)公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。
2)非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。
结论:公平锁指的是哪个线程先运行,那就可以先得到锁。非公平锁是不管线程是否是先运行,新的线程都有可能抢占已经在排队的线程的锁。
5. 优缺点分析
公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。
非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况。
二、synchronized(内置锁)
对于synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。
以下面代码为例:
1 public class SynchronizedTest { 2 public static void main(String[] args) { 3 synchronized (SynchronizedTest.class) { 4 System.out.println("Java"); 5 } 6 } 7 }
查看编译后生成的字节码:
可以看出 JVM(Java 虚拟机)是采用 monitorenter 和 monitorexit 两个指令来实现同步的,monitorenter 指令相当于加锁,monitorexit 相当于释放锁。而 monitorenter 和 monitorexit 就是基于 Monitor 实现的。synchronized 本质是通过进入和退出的 Monitor 对象来实现线程安全的。
三、ReentrantLock (可重入锁)
ReentrantLock(可重入锁)是 Java 5 开始提供的锁实现,它的功能和 synchronized 基本相同。相比于synchronized,ReentrantLock在功能上更加丰富,它具有可重入、可中断、可限时、公平锁等特点。
在jdk1.5里面,ReentrantLock的性能是明显优于synchronized的,但是在jdk1.6里面,synchronized做了优化,他们之间的性能差别已经不明显了。
如下图:
1. 非阻塞地获取锁
当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。使用tryLock()方法。
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
尝试获取锁 tryLock(),代码示例如下:
1 public class Test{ 2 public static Lock lock = new ReentrantLock(); 3 4 public String test() throws Exception { 5 if(lock.tryLock()){//尝试获取锁 6 try { 7 //TODO 8 } catch (Exception e) { 9 throw new Exception(e); 10 } finally { 11 lock.unlock(); 12 } 13 }else{ 14 //不能获取锁 15 } 16 return null; 17 } 18 }
2. 可限时获取锁
在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁,则返回。
tryLock() 有一个扩展方法 tryLock(long timeout, TimeUnit unit) 用于尝试一段时间内获取锁,具体实现代码如下:
1 public class TryLockTest2 { 2 static Lock reentrantLock = new ReentrantLock(); 3 4 public static void main(String[] args) { 5 // 线程一 6 new Thread(() -> { 7 reentrantLock.lock(); 8 try { 9 System.out.println(LocalDateTime.now()); 10 Thread.sleep(2 * 1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } finally { 14 reentrantLock.unlock(); 15 } 16 }).start(); 17 // 线程二 18 new Thread(() -> { 19 try { 20 Thread.sleep(1 * 1000); 21 System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS)); 22 System.out.println(LocalDateTime.now()); 23 } catch ( InterruptedException e ){25 e.printStackTrace(); 26 } 27 }).start(); 28 } 29 }
说明:tryLock(long time, TimeUnit unit) 方法和 tryLock() 方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
比如:tryLock(3, TimeUnit.SECONDS) 表示获取锁的最大等待时间为 3 秒,期间会一直尝试获取,而不是等待 3 秒之后再去获取锁。
3. 可中断锁
该方式会尝试获取锁,并且是阻塞的,但当未获取到锁时,如果当前线程被设置了中断状态,则会抛出InterruptedException异常。
使用lockInterruptibly()方法,代码示例如下:
1 public class DeadLockWithReentrantLock implements Runnable{ 2 private boolean flag; 3 //锁1 4 private static ReentrantLock lock1 = new ReentrantLock(); 5 //锁2 6 private static ReentrantLock lock2 = new ReentrantLock(); 7 8 public DeadLockWithReentrantLock(boolean flag) { 9 this.flag = flag; 10 } 11 12 public static void main(String[] args) throws InterruptedException { 13 Thread t1 = new Thread(new DeadLockWithReentrantLock(true)); 14 t1.setName("A"); 15 Thread t2 = new Thread(new DeadLockWithReentrantLock(false)); 16 t2.setName("B"); 17 t1.start(); 18 t2.start(); 19 TimeUnit.SECONDS.sleep(5); 20 System.out.println("线程B设置中断标记,线程B将退出死锁状态"); 21 t2.interrupt(); 22 23 } 24 25 @Override 26 public void run() { 27 try { 28 if (flag) { 29 //获取锁 30 lock1.lockInterruptibly(); 31 System.out.println("线程 : " + Thread.currentThread().getName() + " get lock1"); 32 TimeUnit.SECONDS.sleep(2); 33 System.out.println("线程 : " + Thread.currentThread().getName() + " try to get lock2"); 34 lock2.lockInterruptibly(); 35 } else { 36 lock2.lockInterruptibly(); 37 System.out.println("线程 : " + Thread.currentThread().getName() + " get lock2"); 38 TimeUnit.SECONDS.sleep(2); 39 System.out.println("线程 : " + Thread.currentThread().getName() + " try to get lock1"); 40 lock1.lockInterruptibly(); 41 } 42 } catch (InterruptedException e) { 43 e.printStackTrace(); 44 } finally { 45 //如果当前线程持有锁1,释放锁1 46 if (lock1.isHeldByCurrentThread()) { 47 lock1.unlock(); 48 } 49 //如果当前线程持有锁2,释放锁2 50 if (lock2.isHeldByCurrentThread()) { 51 lock2.unlock(); 52 } 53 System.out.println("线程 : " + Thread.currentThread().getName() + " 退出"); 54 } 55 } 56 }
执行结果:
PS:使用 thread.interrupt() 方法可以中断线程执行。
注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
4. 使用注意事项
在使用ReentrantLock类的时,一定要注意三点:
1)在使用 ReentrantLock 时要特别小心,unlock 释放锁的操作一定要放在 finally 中,否者有可能会出现锁一直被占用,从而导致其他线程一直阻塞的问题。
2) 不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
3)如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
3) ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。
四、synchronized 和 ReentrantLock的区别
ReentrantLock是Lock的实现类,是一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现。
1. 锁类型不同
在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁。
new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用 new ReentrantLock(true)。
2. 获取锁和释放锁的方式不同
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock示例如下图:
synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁,
synchronized示例如下图:
3. 用法比较
ReentrantLock 只适用于代码块锁,而 synchronized 使用场景更广,可以修饰普通方法、静态方法和代码块等。
4. 响应中断不同
ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
ReentrantLock 可以使用 lockInterruptibly 获取锁并响应中断指令,而 synchronized 不能响应中断,也就是如果发生了死锁,使用 synchronized 会一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而使用 ReentrantLock 可以响应中断并释放锁,从而解决死锁的问题。
5. 底层实现
synchronized 是 JVM 层面通过监视器(Monitor)实现的,而 ReentrantLock 是通过 CAS+AQS(AbstractQueuedSynchronizer)程序级别的 API 实现。
下面通过伪代码,进行更直观的比较:
1 // **************************Synchronized的使用方式************************** 2 // 1.用于代码块 3 synchronized (this) {} 4 // 2.用于对象 5 synchronized (object) {} 6 // 3.用于方法 7 public synchronized void test () {} 8 // 4.可重入 9 for (int i = 0; i < 100; i++) { 10 synchronized (this) {} 11 } 12 // **************************ReentrantLock的使用方式************************** 13 public void test () throw Exception { 14 // 1.初始化选择公平锁、非公平锁 15 ReentrantLock lock = new ReentrantLock(true); 16 // 2.可用于代码块 17 lock.lock(); 18 try { 19 try { 20 // 3.支持多种加锁方式,比较灵活; 具有可重入特性 21 if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ } 22 } finally { 23 // 4.手动释放锁 24 lock.unlock() 25 } 26 } finally { 27 lock.unlock(); 28 } 29 }
ReentrantLock的底层就是由AQS来实现的。AQS详情请参考文章《AQS介绍》
六、总结
在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配,所以我们要选择合适的场景来应用二者。
ReentrantLock 使用更加灵活,效率也高,不过 ReentrantLock 只能修饰代码块,使用 ReentrantLock 需要开发者手动释放锁,如果忘记释放则该锁会一直被占用。synchronized 使用场景更广,可以修饰普通方法、静态方法和代码块等。
参考链接:
https://www.cnblogs.com/vipstone/p/16248006.html
https://juejin.cn/post/6844903695298068487