一、锁分类
锁名称 | 应用 |
乐观锁 | CAS |
悲观锁 | synchronized |
自旋锁 | CAS |
二、锁详解
1、乐观锁
乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会修改数据(所以不会加锁)。写数据时,判断当前与期望值是否相同,如果相同则进行更新(更新期间加锁,保证原子性)。
CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值。CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
示例:
public class Demo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
//期望时10,如果是10则改成2022
System.out.println(atomicInteger.compareAndSet(10, 2022) + "\t" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(10, 2022) + "\t" + atomicInteger.get());
}
}
输出:由于前面修改了,后面修改失败,故先true后false。
2、悲观锁
悲观锁是一种悲观思想,假定当前环境是写多读少,遇到并发写的可能性很高,每次去拿数据的时候都认为其他线程会修改,所以每次读写数据都会认为其他线程会修改,所以每次读写数据都会上锁。其他线程想要读写这个数据时,会被这个线程block,直到这个线程的锁释放。
Java中的synchronized修饰的方法和方法快,ReentrantLock都是悲观锁。
3、自旋锁
自旋锁是一种技术,当一个线程尝试区获取某一把锁的时候,如果这个锁此时已经被别人获取,那么此线程不会立即阻塞,而会不断循环判断锁的状态,直到获取。
优点:避免了线程切换的开销,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性带来了很大的压力。
缺点:占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,带来性能的浪费。因此自选等待的时间必须有一定的限度,如果自选超过了限定的次数仍然么有获取成功,就应当使用传统的方式区挂起线程。
自旋默认次数:10次,使用-XX:PreBlockSpin来进行修改
自适应自旋:自适应意味着自旋的时间不在是固定的,而是由前一次在同一个锁上的自旋时间及锁拥有者的状态来决定的。
Java中的自旋锁:CAS操作中比较操作失败后的自旋等待。
4、可重入锁
可重入锁是一种技术,任意线程在获取锁之后能够再次获取锁而不会被阻塞。
原理:通过组合自定义同步器来实现锁的获取与释放。再次获取锁的时候,识别获取锁的线程是否为当前占据锁的线程,如果是,再次获取成功后,进行计数自增,释放锁的时候,进行计数自减。
作用:避免死锁
Java中的可重入锁:synchronized修饰的方法和代码块,ReentrantLock
5、读写锁
读写锁是一种技术,通过ReentrantReadWriteLock类来实现,读写锁分为读锁和写锁,多个读锁互不互斥,读锁和写锁互斥;
读锁:允许多个线程获取读锁,同时访问同一个资源。
写锁:只允许一个线程获取写锁,不允许同时访问同一个资源。
6、公平锁
公平锁是一种思想:多个线程按照申请锁的顺序来获取锁。在并发环境中,每个线程会先查看此锁维护的等待队列,如果当前队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然会占有锁。
7、非公平锁
非公平锁是一种思想:线程尝试获取锁,如果获取不到,则再采用公平锁的方式。多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程有限获取锁。
优点:非公平锁的性能高于公平锁。
缺点:有可能造成线程饥饿(某一线程很长一段时间获取不到锁)
Java中synchronized是非公平锁,ReentrantLock通过构造函数指定该锁是公平还是非公平的,默认非公平的。
8、共享锁
共享锁是一种思想:可以有多个线程获取读锁,以共享的方式持有锁。和乐观锁、读写锁同义。
Java中用到的共享锁:ReentrantReadWriteLock
9、独占锁
独占锁是一种思想,只能有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同以。
Java中用到的独占锁:synchronized,ReentrantLock
10、重量级锁
重量级锁是一种称谓,synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层操作系统的Mutex Lock来实现,操作系统实现线程切换需要从用户态换到核心态,成本非常高。这种依赖于操作系统Mutex Lock来实现的锁称为重量级锁。
11、轻量级锁
轻量级锁是JDK6时加入的一种锁优化机制,轻量级锁时是无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
优点:如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。
缺点:如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
12、偏向锁
偏向锁是JDK6时加入的一种锁优化机制,偏向锁偏向于第一个获取它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步快时,虚拟机都可以不在进行任务同步操作。
优点:把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点:如果程序中大多数的锁总是被多个不同的线程访问,那偏向锁就是多余的。
13、分段锁
分段锁是一种机制:ConcurrentHashMap的内部细分类若干个小的hashmap,称之为段(segment)。默认情况下一个ConcurrentHashMap被进一步细分为16个段,如果需要在ConcurrentHashMap添加一项key-value,并不是将整个HashMap加锁,而是首先根据hashcode得到该key-value应该存放在哪个段中,然后对该段加锁,并完成put操作。
14、互斥锁
互斥锁与悲观锁、独占锁同义,表示某个资源只能被一个线程访问,其他线程不能访问。
15、同步锁
同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
16、锁粗化
锁粗所是一种优化技术:如果一系列的连续操作都对同一个对象反复加锁和解锁,就算没有线程竞争,频繁的进行互斥同步操作将会导致不必要的性能消耗,所有就采取了一种方案:把加锁的范围扩展到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗。
17、锁消除
锁消除是一种优化技术:当Java虚拟机运行时发现有些共享数据不会被线程竞争时就可以进行锁消除。
三、面试题合集
(1)可重入锁如果加了两把,但是只释放了一把会出现什么问题?
程序卡死,线程不能出来,申请了几把锁,就需要释放几把锁。
(2)如果只加了一把锁,释放两次会出现什么问题?
会报错,java.lang.illegalMonitorStateException.