对ReentrantLock的认知
什么是ReentrantLock?
对于锁(Lock,Synchronized等)相信大伙都不会陌生的吧,个人认为锁就为了保证数据的并发安全性(在存在线程竞争同意资源的时候),早在Lock工具类出来前在Java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来处理。那ReentrantLock是什么呢?包路径:java.util.concurrent.locks.ReentrantLock, 简称:JUC工具类
显然是锁,感觉是废话,直接进入主题. Lock其实是一个接口,在JDK1.5以后开始提供,ReentrantLock对Lock接口进行了实现。ReentrantLock一般都会和Synchronized进行比较,其功能应该以及用法跟Sycnhronized 哪些不一样呢?后面我们会聊到的
ReentrantLock能做什么?
废话不多说,首先来个Demo吧,快速入门,这样从实践中可能跟好的了解它,直接上代码,如下:
public class LockDemo {
private static int num = 1; // 定义全局变量
// 获取锁对象
private static ReentrantLock lock = new ReentrantLock();
public static void incrm(){
try {
// 加锁,针对在并发情况下可能导致数据的可见性等问题
lock.lock();
num++;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 注意需要 在finally里面将锁释放
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
LockDemo lockDemo = new LockDemo();
// 模拟多线程进行修改值
for(int i=0;i< 10;i++){
new Thread(()->{
lockDemo.incrm();
}).start();
Thread.sleep(100);
System.out.println(num);
}
}
}
// 执行结果
2 3 4 5 6 7 8 9 10 11
我对ReentrantLock的理解
跑完ReentrantLock的Demo第一感觉就是这个工具能保证在多线程情况下保证原子性,可见性的,但是它底层是如何实现呢?
接下了我们通过 lock.lock();为入口,去一探究竟,有大神说过:看源码呢,先七分猜测,在三分验证(通过猜测的话,就有个依据有个主线,至少知道每一步想要去干什么,如何实现的,就不会太盲目)。那么我们今天就先来猜测下ReentrantLock底层是怎么实现的呢
原理猜测
1、应该有状态state=0 ,默认0,有线性抢占到了变为 :1.
2、因为支持重入,那么必然有标识为重入次数的
3、既然可以重入,那么必然需要记录线程的相关信息的(如线程Id,状态等)
4、应该有个集合保存线程的(保存各个状态(wait,notify/notifyAll等)下的线程)
等等...
好了,基于这些猜想,接下来我们一步步的进行验证吧..Go...Go...
验证
找到对应的验证入口,这一步很重要,前面说了这里就不说了,直接
lock.lock(); // 验证入口
假设有ThreadA ,ThreadB,ThreadC 三个线程共同竞争
1、抢占到锁 假设A抢占到了锁 -- > tryAcquire() - > setExclusiveOwnerThread(current); cas操作获取的锁 compareAndSetState(0, acquires) ;那么state=0 -> 1 ;
1.1 current == getExclusiveOwnerThread() 如果是同一线程,增加重入次数
2、没抢占到锁 ,即ThreadB,ThreadC --> acquireQueued(addWaiter(Node.EXCLUSIVE), arg); 由于没抢占到锁,但后期还有机会去抢占锁,因此需要将这些线程都存起来,由AQS队列完成这个一操作。分下面几部
1、构建队列(双向队列),addWaiter(Node.EXCLUSIVE)
2、通过自旋构建头结点
3、是否需要唤醒(不断的自旋去获取锁)
3.1 p == head && tryAcquire(arg) 当前结点是头结点,并且获取到锁,将该结点切断,从AQS那拿走(p.next = null; 垃圾回收处理)返回复位位置。
3.2 shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()
3.2.1 shouldParkAfterFailedAcquire(p, node) 主要判断状态,将single的状态说明有条件可以获取锁,还将无效(cancel)kill掉(node.prev = pred = pred.prev;)并且将这些结点都cas赋值(SIGNAL状态)
3.2.2 parkAndCheckInterrupt() 将线程挂起 LockSupport.park(this);
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
}
sync.lock(); //存在公平和非公平
竞争锁,修改状态
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 构建Node结点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// 构建空的头结点, 指定新的尾节点
private Node enq(final Node node) {
for (;;) { // 自旋2次,创建空的头结点
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 没抢占到锁,进入AQS队列中
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋
final Node p = node.predecessor();
// 创建AQS的head,
// 是头结点,并且获取到了锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 设为头结点
// help GC,拿出,将该结点拿出AQS队列
p.next = null;
failed = false;
return interrupted; //返回复位信号
}
// 尝试获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; // 挂起,
}
} finally {
if (failed)
cancelAcquire(node);
}
}
大致的流程图 (如有误,还望请大神指出)
公平锁和非公平锁的区别
主要区别 :非公平锁,一进来就先去抢占锁(tryAcquire(1)),详细见代码
// 公平锁
final void lock() {
// 老老实实进行排队去
acquire(1);
}
// 非公平锁
final void lock() {
// 先抢占,先试下
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
ReentrantLock与Synchronized 的比较
相似性:都是通过加锁的方式,进行同步,即只要有一个进入加锁的模块(方法,或一段逻辑等),那么其他在进去就阻塞,等待唤醒才后才能再次进入。
功能区别: syn 是java的关键字,ReentrantLock是JUC工具类提供的API的互斥锁。当然就使用的便捷性来说,sync是绝对的方便的,ReentrantLock如果使用者忘记了释放锁了,造成了死锁就尴尬了。锁的细粒度来说ReentrantLock还是比Sync要优于些的
性能方便的区别:Sync在1.5及之前是没有优化的情况下,性能是很堪忧的,但是在1.6优化之后(新增了偏向锁,轻量级锁,重量级锁,通过锁的不断升级,包括也用了自旋,cas的手段去优化性能)还是很大的改善了的。目前这2种方式基本差不太多,官方还是建议用Sync。 哈哈哈...
还有一点:synchronized是非公平锁,而ReentrantLock是提供了公平和非公平锁的
见代码,如下:
// 默认非公平的方式
public ReentrantLock() {
sync = new NonfairSync();
}
// 设置 公平/ 非公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}