关于 RentrantLock
jdk1.6 之前synchronized 关键字是一把重量级锁(不论如何都会调用操作系统方法实现的)
ReentrantLock 产生于此时,它在串行/单线程时候 java层就能实现锁,性能远远优于synchronized,
jdk1.6 sun公司对synchronized进行了大优化,目前这两个锁的性能相差不多。
首先 ReentrantLock 默认实现非公平锁, 公平锁的创建需要通过这个构造方法的。
公平锁:先来后到,线程进入先排队,不是队首不尝试获得锁。
非公平锁:排队,但是,碰巧我刚来,锁开了。我能持有锁。(可以理解为都是排队,非公平可以插队,公平不允许插队)
ReentrantLock 的唤醒: 锁被释放时, 唤醒均是从Node队列中找第二个唤醒(第一个是Null 设计相关,下面会说)
重入锁:线程可以重复获得锁,不会死锁。
互斥锁:一次只能有一个线程进入到临界区;
读写锁:
在读取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)
在写数据的时候,无论是读线程还是写线程都是互斥的
**自适应自旋锁:**每个线程会自己判断自己需要自旋的次数,若一个线程经常拿到这个锁,那么它自旋的次数会多一些,反之少。经验之谈吧。
死锁:获取线程的锁不释放锁,后续线程无法获取送一直处于阻塞。
RentrantLock 得手动释放锁, 且释放的次数和加锁的次数得一致。否则会死锁。
IDEA 多线程调试:
CAS:
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作
基本流程概况:
测试类:
package com.haylion.demo;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author meng
* @create 2019/12/4
*/
public class ThreadTest extends Thread {
static ReentrantLock lock = new ReentrantLock();
static int i = 0;
public ThreadTest(String name) {
super.setName(name);
}
@Override
public void run() {
System.out.println(System.currentTimeMillis() + " " + this.getName() + "进入run");
lock.lock();
try {
System.out.println("操作A -------------------" + this.getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("已释放锁");
}
}
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
ThreadTest test1 = new ThreadTest("thread1");
ThreadTest test2 = new ThreadTest("thread2");
ThreadTest test3 = new ThreadTest("thread3");
test1.start();
test2.start();
test3.start();
}
}
公平锁 基本流程:
最初 第一个线程 :
拿到当前线程,获取锁的状态, 判断锁是否被人持有 state == 0 。
未被人持有:hasQueuedPredecessors() 判断是否需要排队,
临时变量 t h分别指向 AQS的 首尾,当第一个线程进来时,首尾均为空,
h != t 返回 flase 方法返回。
! hasQueuedPredecessors() 则返回 true
compareAndSetState() CAS操作,修改锁的状态值,均成功后执行下一步。
setExclusiveOwnerThread(current); 设置持有锁线程是当前线程。
整个tryAcquire()方法返回 true , !tryAcquire 返回false. 方法体结束,加锁成功;
当第二个线程进入, 且第一个线程未释放锁。
tryAcquire()
执行下一个 if, 判断当前线程是否是持有锁的线程(重入判断),不进入方法体。方法返回false
执行下一个方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
addWaiter()
EXCLUSIVE:空的Node队列
Node的组成 :
// waitStatus :当前线程的状态:
nextWaiter: 代码里是初始化的时候赋予的一个 空的Node,
new 了一个 Thread是当前线程的 Node队列。
因为是第一个线程, 直接进enq()
进入 for ()
第一次进入:
执行compareAndSetHead(),设置AQS的head。
结果如图 顶端未AQS 队列。
AQS的 首尾都指向同一个null Node。
for死循环 ,进入第二次循环: t不为空
结果如下:
return 一个 Node t ;
addWaiter()执行完毕返回, 执行 acquireQueued()方法
node.predecessor(),返回 P的上一个节点。
p = head 成立,去尝试获取一遍锁,若获取到了 则是正常流程若没有获取到 则执行
shouldParkAfterFailedAcquire()检查是否需要 park。
第一次进入shouldParkAfterFailedAcquire(),pred 是P 也就是上一个 Node队列.(值得注意:第一个线程 获取锁后,并未进入队列 。队列的头永远是 一个null Node)
初始的 waitStatus 是 0 ; 往下
将 上一个Node 的ws(waitStatus ) 改为 -1 ,整个方法结束 return flase。
接着进入 for 循环 再次进入shouldParkAfterFailedAcquire()
此时 ws 已经变为 -1 方法直接返回 true。前往执行park 让当前线程 park .
线程阻塞 于此。等等被唤醒。
ws 是上一个Node的状态 而不是 本Node的状态,线程不能设置自身的状态,应该是考虑的park异常的情况。
unlock:
断点 进入的是第一个实现方法。
c = state - 1 ;( > 0 重入相关)
if 当前线程不是 AQS的独占线程这抛错,
c == 0 若不等于 0 当为线程持有的不止一把锁,-1 等待 别的锁释放。 当前我测试的是只有一把锁。 所以 c =0
将AQS独占线程置空, 状态置 0 方法返回 false
前面说的 waitStatus.
现在进入unparkSuccessor()
CAS 修改node中 ws(waitStatus)值为 0
这里可以明确的看到 它唤醒的是 head的 next, 而不是 head。
所以应该是这种结构。
第一个 Node 均为空,说不好是为啥这样设计,再深入看看。todo。
线程被唤醒 unlock 结束,
线程从parkAndCheckInterrupt 继续运行
继续 开始 for 循环。尝试拿锁。----
发现漏了一个 队列规整的方法,若只是唤醒队列还没维护好 下一个线程怎么能拿到锁。 去找了一下 发现在这里。:
node 是当前线程的Node 拿到锁后 重新设置AQS的头。setHead 中
队列顺序更新 第一个依然是null节点(不是null 是Null节点)。
非公平锁
与公平锁不同, 非公平锁一进来就尝试修改 锁状态。
后面的和公平锁一样。
唤醒和公平锁一样 从Node队列中唤醒。
美团技术团队分享的ReentrantLock:文章
补充一些知识点:
自旋锁
锁竞争是kernal mode下的,会经过user mode(用户态)到kernal mode(内核态) 的切换,是比较花时间的。
自旋锁出现的原因是人们发现大多数时候锁的占用只会持续很短的时间,甚至低于切换到kernal mode所花的时间,所以在进入kernal mode前让线程等待有限的时间,如果在此时间内能够获取到锁就避免了很多无谓的时间,若不能则再进入kernal mode竞争锁。
在JDK 1.6中引入了自适应的自旋锁,说明自旋的时间不固定,要不要自旋变得越来越聪明。
自旋锁在JDK1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning
参数来开启,在JDK1.6中就已经改为默认开启了。
锁消除
如果JVM明显检测到某段代码是线程安全的(言外之意:无锁也是安全的),JVM会安全地原有的锁消除掉!
锁粗化
默认情况下,总是推荐将同步块的作用范围限制得尽量小。
但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,频繁地进行互斥同步操作也会导致不必要的性能损耗。
JVM会将加锁的范围扩展(粗化),这就叫做锁粗化。
轻量级锁
轻量级锁能提升程序同步性能的依据是**“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”**,这是一个经验数据。
- 如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销
- 但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
简单来说:如果发现同步周期内都是不存在竞争,JVM会使用CAS操作来替代操作系统互斥量。这个优化就被叫做轻量级锁。
互斥量:
操作系统中同一时刻 保证只有一个线程能占用。
偏向锁
作是出现在循环体中的,频繁地进行互斥同步操作也会导致不必要的性能损耗。
JVM会将加锁的范围扩展(粗化),这就叫做锁粗化。
轻量级锁
轻量级锁能提升程序同步性能的依据是**“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”**,这是一个经验数据。
- 如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销
- 但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
简单来说:如果发现同步周期内都是不存在竞争,JVM会使用CAS操作来替代操作系统互斥量。这个优化就被叫做轻量级锁。
互斥量:
操作系统中同一时刻 保证只有一个线程能占用。
偏向锁
偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了!