AQS实现原理
ReentrantLock和Synchronization一样是可重入锁,Synchronization是sun公司开发,而ReentrantLock是一个叫Doug Lea的人写出来的。它控制锁的状态是通过AQS(抽象队列同步器)来实现的,说白了就是 等待队列 + CAS。
(1)aqs内部有一个被volatile修饰的int变量state,、默认是0,还有一个变量记录当前加锁线程、默认为null。
(2)线程1调用lock()方法后进行加锁,由于还没被别人获取到锁,所以state就成了1,并且记录当前线程为线程1。其实每次线程一重入加锁1次,会判断当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把 state 的值给累加 1,unlock()就减一,别的没啥变化。
(3)此时线程二来加锁,发现state 不等于0,然后去检查是否自己获取到了锁,发先获取锁失败,则进入AQS的等待队列。直到锁被释放后,线程二就被唤醒。
源码解读
aqs是如何基于cas加锁的
我们 new ReentrantLock(); 就是创建了一个 NonfairSync 它是Sync 抽象静态内部类的实现,重写了lock方法。这个 Sync 又继承了 AbstractQueuedSynchronizer 类,也就是我们常说的 AQS 类。
我们执行 lock() 加锁,实际就是调用的 Unsafe 的 CAS 方法,我在 atomic 源码 里面已经分析过,就不再多说了。只是 atomic 是 while() 无限循环尝试加锁,AQS 加锁一次,没有抢到锁就扔到队列里面去了。
aqs如何基于state实现重入锁的
上面如果 cas 加锁成功,就会标记线程占用标识;那么此时又有一个线程进来加锁呢?这时就走到了 acquire(1) 方法了。可以看到在 tryAcquire 里面首先是获取到当前线程,然后获取到此时 state 的值。由于已经被人修改过,那肯定是1,这里再 if(c==0) 只是保证代码的健壮性。(你在外面 !=0 , 万一现在已经释放了呢)。
然后再 if (current == getExclusiveOwnerThread()) 判断占用锁的线程和自己是不是同一个线程,是的话就相加,nextc 就成了2然后赋值到 state 里。返回加锁成功!
异步入队阻塞等待
上面是同一个线程,进行重入。那如果是不同线程呢,肯定是获取不到锁的。此时就会先走 addWaiter() 方法,他会将自己封装成一个 node 入队,阻塞等待别人释放锁, 通过分析它的属性看出它是一个 双向链表 的队列结构。addWaiter() 方法就是将当前请求排队到队列中去。
请求入队后,现在就来到了 acquireQueued() ,它负责把当前线程挂起来,阻塞等待。首先又是一个无线for循环,获取到 node 的上一个节点 p,如果 p 是头节点,则说明当前 node 是排队的第一个节点(因为上面看到了 head 其实就是 new 出来的一个空 node) ,那么 node 就会执行 tryAcquire() 又去尝试获取锁。然后将自己从这个队列中摘除掉;如果 p 不是头节点或者 node 没有抢到锁,就执行了下面的 if 分支,shouldParkAfterFailedAcquire 获取当前线程的 waitStatus 状态,默认是 0 ,然后将它更新为 SIGNAL,然后走到下面通过调用 LockSupport.park(this); 将该线程阻塞等待、一直卡着不动, 等待被别人唤醒。所以我们接下来看看解锁的逻辑
解锁
我们加锁是 state+1, 然后 标记线程占用标识。那么解锁肯定是要去除线程标识、state-1的,只是还有线程挂在队列中,得去从头部叫醒才行。
tryRelease 先是 getState 获取到的是1,因为被人加锁了(如果重入了就大于1),然后 int c = 1-1; 此时把 0 setState,然后将线程设置为空。再执行 unparkSuccessor , 获取到头部node s , LockSupport.unpark() 唤醒它。阻塞的线程被唤醒后,又会再次执行它的 acquireQueued() 方法的 for 循环去抢锁。
公平锁
java 并发包很多锁默认的策略都是非公平的,也就是可能后来的线程先加锁,先来的线程后加锁。比如刚才线程二在排队时,线程一释放锁之后,可能被线程三抢先拿到锁。通过设置ReentrantLock lock = new ReentrantLock(true)就成了公平锁,他们会按照队列先后顺序获取锁。可以看到:
NonfairSync:他就是非公平锁,每次 lock() 都会去 compareAndSetState() 抢一下锁。
FairSync:公平锁,每次 lock() 时,都会 !hasQueuedPredecessors() 判断一下是否可以尝试加锁。主要就是看有没有节点在排队;如果有,则看看是否是当前线程。
lock() 与 tryLock()
没啥好说的,就时间判断而已;lock 拿不到锁就 阻塞等待了;tryLock 到了时间返回 false 跳出循环而已。
ReentrantLock和Synchronization比较
ReentrantLock和synchronized在低并发的时候性能差距不大,高并发时ReentrantLock性能要稍微高一些。虽然sync做了优化但是在竞争激烈的时候还是会从偏向锁升级为重量级锁,是用户态切换到内核态的一个过程 比较消耗资源,lock有利用CAS自旋操作来实现锁则会稍微好一点。but !大量开源框架中,还是 sync 用的多。
。