轻量级锁
轻量级锁
是指当前锁是偏向锁的时候,资源被另外的线程所访问,那么偏向锁就会升级为轻量级锁
,其他线程会通过自旋
的形式尝试获取锁,不会阻塞,从而提高性能,下面是详细的获取过程。
轻量级锁加锁过程
- 紧接着上一步,如果 CAS 操作替换 ThreadID 没有获取成功,执行下一步
- 如果使用 CAS 操作替换 ThreadID 失败(这时候就切换到另外一个线程的角度)说明该资源已被同步访问过,这时候就会执行锁的撤销操作,撤销偏向锁,然后等原持有偏向锁的线程到达
全局安全点(SafePoint)
时,会暂停原持有偏向锁的线程,然后会检查原持有偏向锁的状态,如果已经退出同步,就会唤醒持有偏向锁的线程,执行下一步 - 检查对象头中的 Mark Word 记录的是否是当前线程 ID,如果是,执行同步代码,如果不是,执行偏向锁获取流程 的第2步。
如果用流程表示的话就是下面这样(已经包含偏向锁的获取)
重量级锁
重量级锁的获取流程比较复杂,小伙伴们做好准备,其实多看几遍也没那么麻烦,呵呵。
重量级锁的获取流程
1. 接着上面偏向锁的获取过程,由偏向锁升级为轻量级锁,执行下一步
2 . 会在原持有偏向锁的线程的栈中分配锁记录,将对象头中的 Mark Word 拷贝到原持有偏向锁线程的记录中,然后原持有偏向锁的线程获得轻量级锁,然后唤醒原持有偏向锁的线程,从安全点处继续执行,执行完毕后,执行下一步,当前线程执行第4步
3. 执行完毕后,开始轻量级解锁操作,解锁需要判断两个条件
如果上面两个判断条件都符合的话,就进行锁释放,如果其中一个条件不 符合,就会释放锁,并唤起等待的线程,进行新一轮的锁竞争。
-
拷贝在当前线程锁记录的 Mark Word 信息是否与对象头中的 Mark Word 一致。
判断对象头中的 Mark Word 中锁记录指针是否指向当前栈中记录的指针
4. 在当前线程的栈中分配锁记录,拷贝对象头中的 MarkWord 到当前线程的锁记录中,执行 CAS 加锁操作,会把对象头 Mark Word 中锁记录指针指向当前线程锁记录,如果成功,获取轻量级锁,执行同步代码,然后执行第3步,如果不成功,执行下一步
5. 当前线程没有使用 CAS 成功获取锁,就会自旋一会儿,再次尝试获取,如果在多次自旋到达上限后还没有获取到锁,那么轻量级锁就会升级为 重量级锁
如果用流程图表示是这样的
锁的公平性与非公平性
我们知道,在并发环境中,多个线程需要对同一资源进行访问,同一时刻只能有一个线程能够获取到锁并进行资源访问,那么剩下的这些线程怎么办呢?这就好比食堂排队打饭的模型,最先到达食堂的人拥有最先买饭的权利,那么剩下的人就需要在第一个人后面排队,这是理想的情况,即每个人都能够买上饭。那么现实情况是,在你排队的过程中,就有个别不老实的人想走捷径,插队打饭,如果插队的这个人后面没有人制止他这种行为,他就能够顺利买上饭,如果有人制止,他就也得去队伍后面排队。
对于正常排队的人来说,没有人插队,每个人都在等待排队打饭的机会,那么这种方式对每个人来说都是公平的,先来后到嘛。这种锁也叫做公平锁。
那么假如插队的这个人成功买上饭并且在买饭的过程不管有没有人制止他,他的这种行为对正常排队的人来说都是不公平的,这在锁的世界中也叫做非公平锁。
那么我们根据上面的描述可以得出下面的结论
公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
锁公平性的实现
在 Java 中,我们一般通过 ReetrantLock
来实现锁的公平性
我们分别通过两个例子来讲解一下锁的公平性和非公平性
锁的公平性
public class MyFairLock extends Thread{ private ReentrantLock lock = new ReentrantLock(true); public void fairLock(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + "正在持有锁"); }finally { System.out.println(Thread.currentThread().getName() + "释放了锁"); lock.unlock(); } } public static void main(String[] args) { MyFairLock myFairLock = new MyFairLock(); Runnable runnable = () -> { System.out.println(Thread.currentThread().getName() + "启动"); myFairLock.fairLock(); }; Thread[] thread = new Thread[10]; for(int i = 0;i < 10;i++){ thread[i] = new Thread(runnable); } for(int i = 0;i < 10;i++){ thread[i].start(); } } }
我们创建了一个 ReetrantLock,并给构造函数传了一个 true,我们可以查看 ReetrantLock 的构造函数
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
根据 JavaDoc 的注释可知,如果是 true 的话,那么就会创建一个 ReentrantLock 的公平锁,然后并创建一个 FairSync
,FairSync 其实是一个 Sync
的内部类,它的主要作用是同步对象以获取公平锁。
而 Sync 是 ReentrantLock 中的内部类,Sync 继承 AbstractQueuedSynchronizer
类,AbstractQueuedSynchronizer 就是我们常说的 AQS ,它是 JUC(java.util.concurrent) 中最重要的一个类,通过它来实现独占锁和共享锁。
abstract static class Sync extends AbstractQueuedSynchronizer {...}
也就是说,我们把 fair 参数设置为 true 之后,就可以实现一个公平锁了,是这样吗?我们回到示例代码,我们可以执行一下这段代码,它的输出是顺序获取的(碍于篇幅的原因,这里就暂不贴出了),也就是说我们创建了一个公平锁
锁的非公平性
与公平性相对的就是非公平性,我们通过设置 fair
参数为 true,便实现了一个公平锁,与之相对的,我们把 fair 参数设置为 false,是不是就是非公平锁了?用事实证明一下
private ReentrantLock lock = new ReentrantLock(false);
其他代码不变,我们执行一下看看输出(部分输出)
Thread-1启动 Thread-4启动 Thread-1正在持有锁 Thread-1释放了锁 Thread-5启动 Thread-6启动 Thread-3启动 Thread-7启动 Thread-2启动
可以看到,线程的启动并没有按顺序获取,可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程。也就是说,我们把 fair 参数设置为 false 便实现了一个非公平锁。