锁的分类
在多线程的情况下,对共享变量的操作会出现线程安全的问题。使用互斥锁是一种最常见的策略,除了互斥锁,还有很多不同的锁。我们主要介绍以下几种常见的锁:
1. 乐观锁和悲观锁
1.1 乐观锁
- 概念
乐观锁
是一种乐观思想,在读取数据的时候认为别人不会修改,所以不上锁。只在写数据的时候,判断当前值是否与期望值相同,如果相同就进行更新。(更新操作为原子操作)。
- 应用场景
1.2 悲观锁
悲观锁
是一种悲观思想,认为遇到并发的场景比较多,每次取数据的时候都会有其他线程修改,所以每次在拿数据的时候都会进行加锁操作。因此其他线程都会被阻塞,直到当前线程执行完毕,释放锁之后,其他线程才可以竞争锁。保证了同一时刻只有一个线程可以进入临界区。
- 应用场景
1.3 小结
乐观锁适合多读少写的场景,悲观锁适合读多写少的场景。
乐观锁的缺点:
- ABA问题
定义
:如果线程1读取数据V的值为A,这时候另一个线程2也对V进行操作,将V的值先修改为B,再修改为A。这时候线程1发现V的值还是A,就认为V没有被修改。
ABA问题就是说张三的女朋友跟李四跑了,然后又和张三和好了,那他的女朋友还是先前的女朋友吗?(可怜的张三)
解决办法:
使用版本号
- 循环时间开销很大
如果CAS长时间不能成功,就会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作
当对一个共享变量进行操作时,可以使用CAS来保证原子操作,但是对多个共享变量操作,CAS就无法保证操作的原子性。
2. 公平锁和非公平锁
2.1 公平锁
公平锁
是一种思想:多个线程按照申请锁的先后顺序获取锁。在并发环境中,每个线程在获取锁的时候会先查看该锁维护的等待队列,如果该队列为空,则当前线程占有锁,如果当前等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然后占有锁。
优点:不会被饿死
缺点:整体吞吐效率相较于非公平锁而言效率较低,等待队列中除第一个线程意外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
2.2 非公平锁
公平锁
是一种思想:多个线程获取锁的时候,会直接尝试获取锁,如果获取不到,就按照非公平锁的方式。多尔线程之间获取锁的方式不是按照先到先得的顺序。
优点:整体吞吐效率较高,因为线程获取锁的时候可能直接就获取到了,而不用唤醒所有的线程进行竞争。
缺点:等待队列中的线程可能会饿死,或等很久才能获取锁。
3. 可重入锁
可重入锁
是一种思想:多个线程获取锁的时候,
4. 共享锁和独占锁
4.1 共享锁
可以被多个线程持有,以共享的方式持有锁。
当只有读操作的时候,锁在多个读线程之间是共享的。
4.2 独占锁
又叫排他锁,只能被一个线程持有,以独占的方式持有锁。JDK中的synchronized和JUC中的Lock就是互斥锁。
5. 读写锁
读写锁
:读操作使用读锁,而写操作使用写锁。在没有写锁的情况下,读锁是无阻塞的,在一定程度上提高了执行效率。也就是说在同一时刻只允许一个写操作,而读操作之间互不影响。读写锁分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥。读写锁在java中的实现为ReentrantReadWriteLock。
6. 分段锁
ConcurrentHashMap: ConcurrentHashMap中被分为了多个segment,一共有16个segment。每次操作的时候都是先定位到相应的segment,再对该segment进行加锁操作,而不是对整个HashMap进行加锁操作。
参考文章:
juejin.cn/post/686792…