在分布式开发中,锁是线程控制的重要途径。Java 为此提供了两种锁机制,synchronized 和 lock。ReentrantLock 在内存上的语义与 synchronized 相同,但是它提供了额外的功能,可以作为一种高级工具。当需要一些可定时,可轮询,可中断的锁获取操作,或者希望使用公平锁,或者使用非块结构的编码时才应该考虑 ReetrantLock。在业务并发简单清晰的情况下推荐 synchronized,在业务逻辑并发复杂,或对使用锁的扩展性要求较高时,推荐使用 ReentrantLock 这类锁。
一、synchronized 和 Lock 的用法区别
1️⃣synchronized:在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
2️⃣lock:需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效。且在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以要在 finally 块中写 unlock() 以防死锁。
二、synchronized 和 Lock 的性能区别
synchronized 是托管给 JVM 执行的,而 lock 是 Java 写的控制锁的代码。在 Java1.5 中,synchronized 是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。但是 Java1.6,synchronized 语义清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等,因此 Java1.6 的 synchronized 的性能并不比 Lock 差。官方也表示更支持 synchronized,在未来的版本中还有优化余地。
synchronized 原始采用的是 CPU 悲观锁机制,即线程获得的是排他锁。排他锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在 CPU 转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起 CPU 频繁的上下文切换导致效率很低。
而 Lock 用的是乐观锁机制。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。细读 ReentrantLock 的源码,会发现其中比较重要的获得锁的一个方法是 compareAndSetState。这里其实就是调用的 CPU 提供的特殊指令。
现代的 CPU 提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
三、synchronized 和 Lock 的用途区别
二者在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用 ReentrantLock ,特别是遇到下面需求的时候。
1️⃣某个线程在等待一个锁的控制权的这段时间需要中断。
2️⃣需要分开处理一些 wait-notify,ReentrantLock 里面的 Condition 应用,能够控制 notify 哪个线程。
3️⃣具有公平锁功能,每个到来的线程都将排队等候。
第一种情况,ReentrantLock 的锁机制有两种,忽略中断锁和响应中断锁,这带来了很大的灵活性。比如:如果 A/B 两个线程去竞争锁,A 线程得到了锁,B 线程等待,但是 A 线程这个时候实在有太多事情要处理,就是一直不返回,B 线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候 ReentrantLock 就提供了两种机制:可中断/可不中断:
①B 线程中断自己(或者别的线程中断它),但是 ReentrantLock 不去响应,继续让 B 线程等待,你再怎么中断,我全当耳边风(synchronized 原语就是如此);
②B 线程中断自己(或者别的线程中断它),ReentrantLock 处理了这个中断,并且不再等待这个锁的到来,完全放弃。
四、总结
1️⃣二者都是排他锁且皆可重入
- 【synchronized】是 Java 的一个关键字,synchronized 是内置的语言实现。【悲观锁】
- 【Lock】
ReentrantLock是它的实现类
是一个接口。【乐观锁】
2️⃣异常是否释放锁
- 【synchronized】易于操作,但不够灵活。因为加锁和解锁自动进行,在发生异常时候会自动释放占有的锁,因此不会出现死锁。
- 【Lock】不易操作,但非常灵活。加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。发生异常时候,不会主动释放占有的锁,必须手动 unlock 来释放锁,可能引起死锁的发生。最好将同步代码块用 try catch 包起来,finally 中写入 unlock,避免死锁的发生。
3️⃣响应是否可中断
- 【synchronized】只能等待锁的释放,不能响应中断。一个线程获取不到锁就一直等着。
- 【Lock】等待锁过程中可以用 interrupt 来中断等待。
4️⃣是否知道获取锁
- 【synchronized】不能。
- 【Lock】可以通过 trylock 来知道有没有获取锁。
5️⃣公平锁机制
何谓公平锁?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
- 【synchronized】不能实现。
- 【Lock】可以实现。
6️⃣线程调度
- 【synchronized】使用 Object 对象本身的 wait 、notify、notifyAll 调度机制。
- 【Lock】使用 Condition 进行线程之间的调度。
7️⃣【Lock】可以提高多个线程进行读操作的效率。(可以通过 ReadWriteLock 实现读写分离)
8️⃣在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。所以说,在具体使用时要根据适当情况选择。