Java 中我们经常听到各种锁,例如悲观锁,乐观锁,自旋锁等等。今天我们将 Java 中的所有锁放到一起比较一下,并分析各自锁的特点,让大家能够快捷的理解相关知识。
1、悲观锁 VS 乐观锁
从概念上来说
悲观锁:
在对同一个数据并发操作的时候,认为使用到的数据一定被别的线程修改数据,因此获取数据的时候先加锁,保证数据不会被别的线程修改。例如: 常用的 synchronized 关键字和 Lock 的实现类的使用都是悲观锁
乐观锁:
同悲观锁相反,认为使用到的数据不会有别的线程修改数据,所以不会添加锁,只有在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果没有被更新,则将修改数据写入,如果数据被其他线程修改,则报错或者重试。通常是使用 CAS 算法操作。
从上面的概念来说
悲观锁适合写操作多的场景: 先加锁可以保证写操作时数据正确
乐观锁适合读操作多的场景: 不加锁的特点能够使其读操作的性能大幅提高。
代码实例:
悲观锁的调用方式
//synchronized使用方式
public synchronized void testMethod() {
// 操作同步业务
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock();
public void modifyPublicResources() {
lock.lock();
// 操作同步业务
lock.unlock();
}
乐观锁的调用方式
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet(); //执行自增,底层是 CAS 算法。
总结:
其实悲观锁和乐观锁的区别在于是否锁住同步资源
. 如果锁住同步资源就是悲观锁,不锁住同步资源就是乐观锁。
2、自旋锁 VS 适应性自旋锁
Java 线程状态切换是需要操作系统操作 CPU 状态完成的。也就是需要从用户态切换到内核态。在很多场景下,同步资源的锁定还是非常短,为了这一段时间去调用内核去切换进行状态,就会得不偿失。因此优化这种情况,我们不如让线程‘等一等’。例如自旋一下,当同步资源立即的时候,能够立即操作,提升速度。
从上面两张图,我们可以看下自旋锁和非自旋锁的区别。
自旋锁本身也是有缺点的,它不能代替阻塞。短暂自旋等待可以提高同步资源的加锁速度。但是长时间的自旋,就会白白浪费 CPU, 因此一般我们会尝试设置自旋次数,如果超过这个次数之后,就应该挂起线程。
可以通过
-XX:PreBlockSpin
参数来设置一个大致的自旋次数范围。在实际应用中,一般不建议手动设置这个参数,因为自适应自旋锁通常能够做出更合理的决策。
讲完自旋锁,那我们看下自适应自旋锁。
所谓的自适应自旋锁就是自旋次数不是固定的。而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获取过锁。并且持锁的线程并在运行,那么虚拟机就会认为这次自旋很有可能再次成功,进而允许自旋等待持续相对更长的时间。例如前一次是 10次循环,这次可能是 20 次循环
总结:
其实所谓的自旋锁就是在加锁同步资源失败的时候,线程不阻塞的情况。
3、无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
以上是 synchronized 锁膨胀的过程,其中涉及到无锁,轻量级锁,重量级锁的转变过程。这里我们先大致了解一下。synchronized 锁膨胀的详细过程 后续详细分析。
总结:
偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞
4、公平锁 VS 非公平锁
上图是 AQS 的整体原理图。所谓的公平和非公平就是如果获取不到同步资源的时候,进入队列的方式是否公平。
在 java 1.8 中 ReentrantLock的实现方式 分为公平锁和非公平锁两种实现方式。其中主要区别就是在进入队列的时候,是否尝试去加锁。
公平锁: 如果获取不到同步资源就是乖乖的进入阻塞队列
非公平锁: 如果获取不到同步资源,在进入队列之前,再次去尝试加锁。如果加锁失败才会进去到队列中。
非公平锁比较常用
因为非公平锁的效率较高。同步加锁的时候比较短,在进队列之前再重新加锁的时候,有可能获加锁成功。并且线程不会进入到内核态,并阻塞唤醒。
5、可重入锁和非可重入锁
public class Reentrant {
public synchronized void method1() {
System.out.println("method1");
method2();
}
public synchronized void method2() {
System.out.println("method2");
}
}
我们看下上面的代码。当一个线程调用 method1()的时候,需要加锁成功。当调用 method2()的时候,也需要加锁成功才能执行。如果synchronized 内置锁是不可重入的。那就会导致无法调用 method2()。因为调用 method1()的加锁成功,但是锁未释放。就会导致调用 method2()方法 无法加锁成功。造成死锁问题。
synchronized 和 ReentrantLock 都是比较常见的重入锁。
总结:
如果一个线程在多个流程中能获取到同一把锁,就是可重入锁。如果不能,则是非可重入锁。
6、独享锁和共享锁
独享锁,是指该锁一次只能被一个线程所持有。如果线程A对数据x加上排它锁后,则其他线程不能再对x加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
共享锁 是指该锁可被多个线程所持有。如果线程A对数据x加上共享锁后,则其他线程只能对x再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
总结:
多个线程能不能共享一把锁?如果能,就是共享锁,如果不能 就是排他锁。
以上就是 Java 中的各种锁。部分锁的内容讲的比较概括,后续会有文章详细地讲解。