Appearance
锁的分类
看在前面
- 轻量级锁(锁膨胀、锁自选、偏向锁、锁消除):https://www.cnblogs.com/greengages/p/16620550.html
- java对象在内存中的结果(HotSpot虚拟机):https://www.cnblogs.com/duanxz/p/4967042.html
一、乐观锁和悲观锁
悲观锁和乐观锁并不是某个具体的“锁”而是一种并发编程的基本概念。乐观锁和悲观锁最早出现在数据库的设计当中,后来逐渐被 Java 的并发包所引入。
悲观锁的历史到乐观锁的演进
悲观锁中互斥同步最主要的问题是线程阻塞和唤醒锁带来的性能问题。因此这种同步也称为阻塞同步(Blocking Synchronization)。
从处理问题的方式上来说,互斥同步属于一种悲观的并发策略。这种策略总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要加锁和做用户态核心态转换。它还要做维护锁计数器、检查是否有被阻塞的线程需要唤醒这类操作。这里讨论的是概念模型,虚拟机会优化掉很大一部分不必要的加锁。
随着硬件指令集的发展,我们有了另外一种选择:基于冲突检测的乐观并发策略。通俗的说,就是先操作。如果没有其他线程争用共享数据,那操作就成功了。如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施。最常见的补偿措施就是不断的重试,直到成功为止。这种乐观的并发策略的实现都不需要把线程挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization)。
上述乐观锁并发策略需要“硬件指令集的发展”,原因是操作和检测这两个步骤需要具备原子性。保证方法:不能用互斥同步来保证,否则就失去意义了,只能靠硬件来完成这件事情。硬件保证从一个语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。这种指令常用的有:
- 测试并设置,Test-and-Set
- 获取并增加,Fetch-and-Increment
- 交换,Swap
- Compare-and-Swap,简称CAS
- 加载链接/条件存储(Load-Linked/Store-Conditional,上文称LL/SC)
悲观锁
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题。
乐观锁
乐观锁正好和悲观锁相反。它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁。只有在更新数据时,才通过判断现有数据和原数据是否一致,来判断数据是否被其他线程操作。如果没被其他线程修改,则更新数据。如果被其他线程修改,则不更新数据。
二、公平锁和非公平锁
根据线程获取锁的抢占机制,锁又可以分为公平锁和非公平锁。
公平锁
公平锁是指多多个线程按照申请锁的顺序来获取锁。
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序。后申请的线程有可能比先申请的线程优先获取锁。ReentrantLock 提供了公平锁和非公平锁的实现。
公平锁:new ReentrantLock(true)
非公平锁:new ReentrantLock(false)
如果构造函数不传任何参数的时候,默认提供的是非公平锁。
三、独占锁和共享锁
根据锁能否被多个线程持有,可以把锁分为独占锁和共享锁。
独占锁
独占锁是指任何时候都只有一个线程能执行资源操作。
共享锁
共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。比如 Java 中的 ReentrantReadWriteLock 就是共享锁的实现方式,它允许一个线程写操作,允许多个线程读操作。
ReentrantReadWriteLock 共享锁演示代码如下:
java
public class ReadWriteLockTest {
public static void main(String[] args) throws InterruptedException {
final MyReadWriteLock rwLock = new MyReadWriteLock();
// 创建读锁 r1 和 r2
Thread r1 = new Thread(new Runnable() {
@Override
public void run() {
rwLock.read();
}
}, "r1");
Thread r2 = new Thread(new Runnable() {
@Override
public void run() {
rwLock.read();
}
}, "r2");
r1.start();
r2.start();
// 等待同时读取线程执行完成
r1.join();
r2.join();
// 开启写锁的操作
new Thread(new Runnable() {
@Override
public void run() {
rwLock.write();
}
}, "w1").start();
new Thread(new Runnable() {
@Override
public void run() {
rwLock.write();
}
}, "w2").start();
}
static class MyReadWriteLock {
ReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
try {
lock.readLock().lock();
System.out.println("读操作,进入 | 线程:" + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("读操作,退出 | 线程:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
public void write() {
try {
lock.writeLock().lock();
System.out.println("写操作,进入 | 线程:" + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("写操作,退出 | 线程:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
}
}以上程序执行结果如下:
java
读操作,进入 | 线程:r1
读操作,进入 | 线程:r2
读操作,退出 | 线程:r1
读操作,退出 | 线程:r2
写操作,进入 | 线程:w1
写操作,退出 | 线程:w1
写操作,进入 | 线程:w2
写操作,退出 | 线程:w2四、可重入锁
可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。
五、偏向锁 & 轻量级锁 & 重量级锁
JDK 1.6 引入偏向锁、轻量级锁、自旋/自适应自旋、锁消除、锁粗化等优化后,锁随竞争激烈程度逐级升级,可以升级不可降级(这种策略是为了提高获得锁和释放锁的效率)。偏向锁、轻量级锁加锁解锁的完整详解与 synchronized 的 6 项优化见 synchronized。
锁升级状态表如下(锁标志位位于对象头 Mark Word 中):
| 锁状态 | 标志位 | 触发/适用场景 | 一句话原理 |
|---|---|---|---|
| 无锁 / 偏向锁 | 01 | 始终只有一个线程获取该锁 | 偏向第一个获得它的线程,Mark Word 记录线程 ID,无竞争时连 CAS 都不做;出现竞争即撤销偏向 |
| 轻量级锁 | 00 | 少量线程交替竞争,基本无竞争 | 栈帧 Lock Record + CAS 替换 Mark Word,避免使用操作系统互斥量 |
| 重量级锁 | 10 | 竞争激烈,自旋失败/锁膨胀 | 依赖对象监视器 ObjectMonitor / 操作系统互斥量,等待线程进入阻塞(挂起)状态 |
偏向锁、轻量级锁、重量级锁的升级过程:
六、CAS与ABA
CAS(Compare and Swap)比较并交换,是一种乐观锁的实现,用非阻塞算法来代替锁定,java.util.concurrent 包下的 AtomicInteger 即借助 CAS 实现;自旋 CAS 不断循环重试,长时间不成功会给 CPU 带来大的执行开销,因此 JDK 1.6 又引入了自适应自旋。
CAS 著名的副作用是 ABA 问题:值被改为其他值又改回原值,CAS 会误认为它没有被修改过,常用版本号方案(AtomicStampedReference)解决。CAS 的原理、三大问题与 ABA 的图文详解见 CAS。