Java中各种锁的概念
# 悲观锁 VS 乐观锁
对于同一个数据的并发操作:
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。(Java 中, synchronized关键字
和 Lock的实现类
都是悲观锁。)
乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法。(Java 原子类
中的递增操作就通过 CAS 自旋实现的。)
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
为什么不所有场景都使用乐观锁?
在实际开发中,数据操作大部分时候都是在和数据库打交道,报错意味着事务回滚,频繁回滚会导致数据库性能降低。
# 自旋锁
在 JDK1.4 中就引入了,当时默认关闭的。在 JDK 1.6 后默认为开启状态。
没有自旋锁的场景
当下计算机处理器核心普通都有双核或以上,因此可以同时执行两个或以上的线程任务。
现有线程 1、2 同时抢夺同一个锁,线程 1 抢到了,由于线程 2 没抢到,因此需要阻塞线程,把处理器资源让出来。而阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在 “没有自旋锁的场景” 中,如果线程 2 “稍等一下”,那么线程 2 反而能够很快获得锁,从而提高了程序的效率。而为了让当前线程 “稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁的缺点:不适合锁被占用的时间长的场景,会白浪费处理器资源。
所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用 -XX:PreBlockSpin
来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
笔记
自旋锁可以这么理解,在挂起线程前,先尝试 count 遍获得锁。
for(int i = 0;i < 10;i++){
if(getLock()){
do();
break;
}
}
# 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
todo
# 公平锁 VS 非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
优点:等待锁的线程不会饿死。
缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。
缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
笔记
公平锁,老老实实排队,先到先得。
非公平锁,先试试插队,不行再排队。
为什么需要非公平锁?
跟自旋锁一样,为了尽可能的提升程序执行效率。
# 可重入锁 VS 非可重入锁
可重入锁,又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。
Java 中 ReentrantLock
和 synchronized
都是可重入锁,
优点:可一定程度避免死锁。
笔记
可重入锁允许同一个线程,反复(递归)获取同一把锁。
非可重入锁只允许同一个线程,获取一次锁,再次获取就会阻塞,死锁。
# 独享锁 (互斥锁) VS 共享锁
独享锁,也叫排他锁、互斥锁,是指该锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
JDK 中的 synchronized
和 JUC 中 Lock
的实现类就是互斥锁。
共享锁是指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
ReentrantReadWriteLock
中的 WriteLock
为共享锁。
笔记
在上述描述中共享锁只读,只是一个约定,如果在代码中非要在获取读锁(共享锁)后修改数据是不会报错的,但我们约定应在修改数据时获取写锁,只读数据时获取读锁。
共享锁不会单独使用,通常作为读写锁中的读锁出现。
读(锁)写(锁)、写读、写写的过程互斥,即读锁上锁时,写锁上锁阻塞...