在编写多线程代码的时候, 对于不允许并发的代码, 很多需要加锁进行处理. 在进行加锁处理时候, synchronized 作为 java 的内置锁, 同时也是 java 关键字, 最为被人熟知, 即使是最初级的 java 程序员, 只要知道 java 并发处理的, 都会知道 syschronized.
java5.0 之后, java 提供了另外一种加锁机制, ReentrantLock 提供了更多更灵活的功能, 在很多复杂场景下, ReentrantLock 相比 synchronized 会更合适.
synchronized 和 ReentrantLock 也经常会拿出来比较, 这也是在面试中, 面试官也经常会问到的一个问题. 这里就简单整理一下 synchronized 与 ReentrantLock 的异同.
相同点
比较 synchronized 和 ReentrantLock 的相同点, 毋庸置疑, 它们的功能都是作为锁对访问进行控制. 防止并发造成逻辑错误.
1, 在获取 ReentrantLock 的时候, 有着与 synchronized 相同的互斥性 (互斥性: 当一个线程已经获取到某个锁的时候, 其他线程无法获取到该锁)
2,ReentrantLock 和 syschronized 提供的相同的内存可见性 (可见性: 当一个线程修改一个变量的时候, 其他的线程能够及时知道)
3, 虽然 ReentrantLock 命名为可重入锁, 但是实际上 synchronized 也是一样可重入的 (可重入: 当一个线程拿到某个锁后, 当该线程在释放该锁之前, 可以重复获取该锁)
打个比方: 比如你去食堂排队打饭, 饭桶只有一个饭勺. 你手中拿着勺子, 其他人要打饭就得等你打完, 这就叫互斥性 (独占性), 勺子就是锁. 你一开始左手拿着勺子, 觉得姿势不爽, 换成右手来拿勺子, 此时勺子在你手里, 你可以重复左手右手一个慢动作, 这就叫可重入. 当你打完饭的时候, 饭桶里的饭明显少了很多 (真的很能吃), 其他来打饭的人马上就能看到了, 这就叫可见性, 饭桶是主存, 你的饭盆就是工作内存.
不同点
写法
首先最直观的不同点, 就是写法上的不同.
- Object object = new Object();
- synchronized (object) {
- doSomeThing(s);
- }
- Lock lock=new ReentrantLock();
- lock.lock();
- try {
- doSomeThing(s);
- }finally {
- lock.unlock();
- }
synchronized 的写法相比较更简单, 使用 synchronized 会自动加锁, synchronized 同步代码块退出后, 会自动释放锁.
ReentrantLock 需要在 fiinally 手动释放锁, 如果忘记释放锁, 会造成很大的麻烦.
synchronized 必须写在同一个代码块中, 无法进行拆分. ReentrantLock 在可以在不同的地方进行 lock,unLock, 代码也可以进行灵活编写. 比如 concurrentHashMap 中, Segment 继承 ReentrantLock 来进行锁操作.
功能
synchronized 提供的功能相对单一, 当需要对锁进行复杂操作的时候, synchronized 就会显得力不从心. 比如轮询锁, 定时锁, 可中断锁.
轮询锁
第一种场景, 假设有一段代码同时需要获取两个锁, 当我们使用 synchronized 的时候, 会这么写
- Object object1 = new Object();
- Object object2 = new Object();
- synchronized (object1) {
- synchronized (object2){
- doSomeThing(object1,object2);
- }
- }
如果现在有另外一段代码, 仍然需要同时锁住 object1 和 object2.synchronized 就必须保证同步的顺序, 如果先锁住 object2, 再锁住 object1, 就有可能产生死锁. 当一段程序用到多个锁, 容易搞乱顺序. 特别在团队开发的时候, 成员之间不会知道他人通过什么样的顺序进行加锁.
这个时候 ReentrantLock 更加灵活的优点就体现出来了. ReentrantLock 提供了 tryLock() 方法, 我们可以用这个方法来实现轮询.
- Lock lock1 = new ReentrantLock();
- Lock lock2 = new ReentrantLock();
- while (true) {
- if (lock1.tryLock()) {
- try {
- if (lock2.tryLock()) {
- try {
- doSomeThing(s);
- } finally {
- lock2.unlock();
- }
- }
- } finally {
- lock1.unlock();
- }
- }
- SECONDS.sleep(1);
- }
使用 ReentrantLock 的 tryLock(), 如上述代码所示, 当我们尝试获取 lock1 成功, 获取 lock2 失败的时候, 我们会释放 lock1, 休眠一秒后重新获取两个锁. 这样的好处通过放弃已经获取到的锁避免了死锁的出现, 代码的安全性更高. 而且获取锁的过程中, 我们可以灵活的插入一些代码, 比如 17 行所示的, 每当两个锁没有同时获取成功, 我们就进行一秒钟休眠 (虽然正常项目中下我们不会这么处理).
定时锁
第二种场景, 如果我们需要一个程序在特定时间内完成, 如果特定时间内没有拿到锁, 就直接返回, 放弃该任务. synchronized 面对这种场景同样为难, 又到了 ReentrantLock 登场时间. reentrantLock 的 tryLock(long timeout, TimeUnit unit) 方法提供了超时返回的功能. 如下示例代码:
- Lock lock = new ReentrantLock();
- if(!lock.tryLock(1,SECONDS)){
- return;
- }
- try {
- doSomeThing();
- }finally {
- lock.unlock();
- }
上例中, ReentrantLock 尝试使用 1 秒的时候去获取锁, 如果超过时间仍然没有获取到锁, 则返回失败. 如果获取锁成功, 则继续执行目的代码.
可中断锁
在使用 synchronized 进行获取锁排队的时候, 是无法响应中断的, 当业务场景必须实时响应线程中断时, synchronized 就不那么合适了. 使用 ReantrantLock 可以使用 lockInterruptibly() 方法. 该方法支持线程在获取锁的过程中, 对线程进行中断.
- Lock lock = new ReentrantLock();
- lock.lockInterruptibly();
- try {
- doSomeThing(s);
- } finally {
- lock.unlock();
- }
性能
在 java 6.0 之前, synchronized 的性能方便比 ReentrantLock 弱一截, java 发布 6.0 之后, 重新优化了 synchronized 的算法. ReentrantLock 的性能仅比 synchronized 的性能稍微强一些. 至于现在, 在不同系统, 不同硬件下, 测试出来的性能都会有偏差. 这边我也懒得自己再去做实验尝试, 所以就不多逼逼了.
公平性
当多个线程先后请求一个被占用的锁的时候, 当该锁释放, 如果有算法保证最早排队的线程最先拿到锁, 则这个锁就是公平锁.
synchronized 是不公平, reentrantLock 则同时提供了公平锁和不公平锁两种. 公平锁的好处在于, 线程的先后顺序, 等待最久的线程先执行. 然而公平锁的性能却不如非公平锁, 原因在于, 假设线程 A 持有一个锁, 并且 B 线程请求这个锁. 由于这个锁已经被 A 线程持有, 因此 B 将被挂起. 当 A 释放的锁的时候, B 将被唤醒, 然后重新尝试获取该锁. 与此同时, 如果 C 线程也同时请求该锁, 那么 C 可能在 B 被完全唤醒之前获取该锁, 并执行任务, 释放锁. 即线程 C 可能在 A,B 线程持有锁的间隔中, 完成操作. B 线程完全唤醒之后会发现锁已经被释放了. 因此并不影响 B 线程的执行. 通过这种插队行为, 能提高这个程序的吞吐量.
默认创建的 ReentrantLock 都是非公平的锁, 如果想创建一个公平锁, 可以利用 Reentrant 的构造器.
- //ReentrantLock 构造器
- public ReentrantLock( boolean fair){
- sync = fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync();
- }
- Lock lock = new ReentrantLock(true);
以下简单贴一下 reentranLock 的源码, 看下公平锁和非公平锁的区别:
- static final class FairSync extends ReentrantLock.Sync {
- 3
- // 公平锁, 直接进行队列
- final void lock() {
- acquire(1);
- }
- }
- static final class NonfairSync extends ReentrantLock.Sync {
- 13
- /**
- * Performs lock. Try immediate barge, backing up to normal
- * acquire on failure.
- */
- // 非公平锁, 可以进行插队
- final void lock() {
- if (compareAndSetState(0, 1))
- setExclusiveOwnerThread(Thread.currentThread());
- else
- acquire(1);
- }
- protected final boolean tryAcquire(int acquires) {
- return nonfairTryAcquire(acquires);
- }
- }
java 语言规范并没有要求 JVM 以公平的方式来实现内置锁, 各种 JVM 也确实没有这么做. ReentrantLock 并没有进一步降低锁的公平性, 在等待线程队列中, 依旧遵循先进先出的原则.
总结
synchronized 与 ReentrantLock 相比较之后, 感觉 ReeantrantLock 比 synchronized 有更强大的功能, 更灵活的操作. 但是相比起来, syschronzied 使用门槛更低, 简单粗暴, 而且不会出现忘记解锁的状况.Java 并发编程实战中建议, 在 synchronized 无法满足需求的时候, 再用 ReeantrantLock, 原因大致如下:
1,synchronized 简单易用, 容易上手, 而且不容易出现忘记解锁的情况
2, 未来更可能提高 synchronized 的性能, 因为 synchronized 是 JVM 的内置属性
来源: https://www.cnblogs.com/null-qige/p/9331129.html