前言
在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。
锁旨在强制实施互斥排他、并发控制策略。
锁的分类
乐观锁和悲观锁
乐观锁/悲观锁不是指具体类型的锁,而是看待并发的角度。
乐观锁
乐观锁认为不存在很多的并发更新操作,不需要加锁;
数据库中乐观锁的实现一般采用版本号,Java中可使用CAS实现乐观锁。
- ABA问题(JDK1.5之后已有解决方案):CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
- 循环时间长开销大:CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
- 只能保证一个共享变量的原子操作(JDK1.5之后已有解决方案):对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
使用场景:
比较适合读取操作比较频繁的场景。
实现方式:
1.版本号
2.CAS
3.原子类(Java的java.util.concurrent.atomic包下)
悲观锁
悲观锁认为存在很多并发更新操作,采取加锁操作,如果不加锁一定会有问题;
Java 里面的同步原语 synchronized 关键字的实现就是悲观锁。
使用场景:
比较适合写入操作比较频繁的场景。
实现方式:
synchronized 和 Lock
公平锁和非公平锁
公平锁
指多个线程按照申请锁的顺序来获取锁;
非公平锁
指多个线程获取锁的顺序是随机的,并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
有可能,会造成优先级反转或者饥饿现象。
饥饿现象
线程一直无法获取锁或其他资源,导致线程一直无法到执行的状态;
导致无法获取的原因:
- 线程优先级较低,没办法获取cpu时间;
- 其他线程总是能在它之前持续地对该同步块进行访问;
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒;
实现方式:
ReenTrantLock(公平(true)/非公平(false))
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大;
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的控制线程对锁的获取, 所以并没有任何办法使其变成公平锁;
可重入锁
可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁;
ReentrantLock和Synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁;
需要注意的是,可重入锁加锁和解锁的次数要相等;
独享锁和共享锁(读写锁)
独享锁是指该锁一次只能被一个线程所持有, 具体实现有ReentrantLock、Synchronized;
共享锁是指该锁可被多个线程所持有,具体实现有ReadWriteLock;
独享锁和共享锁(读写锁)也是通过AQS来实现的;
锁升级:读锁到写锁 (不支持)
锁降级:写锁到读锁 (支持)
偏向锁/轻量级锁/重量级锁(jdk 1.6 以后)
偏向锁:当前只有这个线程获得,没有发生争抢,此时将方法头的markword设置成0,然后每次过来都cas一下就好,不用重复的获取锁,降低获取锁的代价;
轻量级锁:在偏向锁的基础上,有线程来争抢,此时膨胀为轻量级锁,多个线程获取锁时用cas自旋获取,而不是阻塞状态;
重量级锁:轻量级锁自旋一定次数后,膨胀为重量级锁,其他线程阻塞,当获取锁线程释放锁后唤醒其他线程。(线程阻塞和唤醒比上下文切换的时间影响大的多,涉及到用户态和内核态的切换)
实现方式: synchronized
分段锁
分段锁是一种锁的设计,并不是一种具体的锁。
对于ConcuttentHashMap就是通过分段锁实现高效的并发操作,有兴趣的可以去阅读下相关源码;
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作;
自旋锁
自旋锁是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁。
好处是减少上下文切换,缺点是一直占用CPU资源;
网友评论