Java并发包中锁原理剖析
6.1LockSupport
跟wait()不同!
引入了一个许可证的概念。
void park()方法
检测此时的线程是否拥有许可证,有的话。就通过,没有的话就阻塞。
LockSupport.park():在哪儿调用就是检查哪个线程
void unpark(Thread thread)方法
LockSupport.unpark(t):给t线程发一个许可证。
LockSupport.parkNanos(long nanos)
如果有许可证,直接返回
如果没有许可证,那就等一会再返回
LockSupport.park(Object blocker)方法
讲blocker参数传入到阻塞线程中去
LockSupport.parkNanos(Object blocker, long nanos)
相对时间
LockSupport.parkUnitl(Object blocker, long deadline)
准确时间
大名鼎鼎的AQS
AbstractQueuedSynchronized:简称AQS,抽象同步队列;实现同步锁的基础组件。
并发包中的锁底层就是使用AQS实现的。
AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型是Node。
Node节点用来保存一条线程:
- Shared:标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的
- EXCLUSIVE:标记线程是获取独占资源时被挂起后放入AQS队列的
- CANCLELED:线程被取消
- SIGNAL:线程需要被唤醒
- CONDITION:线程条件队列里面等待
- PROPAGATE:释放共享资源时需要通知其它节点
- pre:前驱节点
- next:后继节点
最终要的AQS中的state是实现不同锁的基础!
AQS中有内部类ConditionObject类:每个对象对应一个条件队列(单向链表队列):其中放调用条件变量await方法后被阻塞的线程。
对于ReentrantLock来说:state指的是可重入锁的次数
对于ReetrantReadWriteLock来说:高十六位是:读的重入数;低十六位:写锁的重入数;
对于CountDownlatch来说:state用来表示计数器当前的值
根据state是否属于一个线程:操作state的方式分为独占式和共享式。
独占式
谁获得了锁,state从0---->1;假如重入获取,state+1;解锁:state-1;
没有获取到锁:放入AQS阻塞队列中
释放资源
boolean release(int args)
获取资源
void acquire(int arg):不响应中断
获取锁,没有就讲线程保存到AQS的阻塞队列里头。
void acquireInterruptibly(int arg):响应中断
共享式
例如信号量Semaphore:多个线程去请求资源时通过CAS方式竞争资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式获取即可。
释放资源
boolean releaseShared(int args)
获取资源
void acquireShared(int arg):不响应中断
void acquireInterruptiblyShared(int arg):响应中断
----------------------------------------------------------------------------------------------------------------------------------------------------------------
获取锁:都是CAS
成功:执行
失败:LockSupport.park()假如到AQS队列里头等待
释放:释放成功,LockSupport.unpar()唤醒一个,CAS获取锁咯。
上述的tryxx的方法AQS需要实现的子类自己写
例如:ReentrantReadWriteLock
读锁重写tryAcquireShared时,首先查看写锁是否被其它线程持有,如果有则直接返回false,否则使用CAS递增state的高16位;
释放读锁时:重写tryRealeaseShared时,在内部需要使用CAS算法把当前state的高16位减1.
isHeldExclusively():判断锁是被当前线程独占还是被共享。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
node节点入队操作
AQS---条件变量的支持
就是之前的哪个ConditionObject类
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ConditionTest { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); new Thread(new Runnable() { @Override public void run() { lock.lock(); System.out.println("I am one"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); }finally { System.out.println("I am one signal"); lock.unlock(); } } }).start(); new Thread(new Runnable() { @Override public void run() { lock.lock(); System.out.println("I am two"); condition.signal(); System.out.println("I am two"); lock.unlock(); } }).start(); } /** * I am one * I am two * I am two * I am one signal */ }
其实没什么难得,就是不在AQS的队列中等,在ConditionObject的队列中等。await和singal-------------》wait()和notify()差不多
当一个线程调用条件变量的signal方法时,会将条件队列的一个线程节点从条件队列中移到AQS的队列中,然后激活这个线程。
signalAll是将所有的线程放到AQS中
什么是虚假唤醒
例如:生产者和消费者
有一个生产者A和两个消费者B.C
- B来消费,发现没有有东西,await了,释放锁,进入wait队列
- A生产咯:生产过程中;C来了,因为锁被A占有了,所以直接进入了AQS队列
- A生产完了,唤醒B;但是此时C得到了锁,消费了一个商品。B被锁在了外头。
- 等C消费完了,释放锁,B才获得锁,这个时候啥都没了。
- 这个时候你的程序可能会报错。
所以,在判断的时候用while,每次进来都判断一下。这样才能避免虚假唤醒。
手动实现基于AQS的锁
重写tryAcquire(int acquires)、tryRelease(int releases)、newCondition()、isHeldExclusively()
独占锁ReentrantLock原理
获取锁lock()
假如是非公平锁:
公平锁
就是判断head指针的下一个节点是不是S,是的就冲了,不是就等等!,反正我是这么理解的
tryLock()方法
释放锁 void unlock()方法
ReentrantReadWriteLock原理
读写锁:维护了一个ReadLock和WriteLock
基于AQS实现
state高16位表示读状态
低16位标识写状态
WriteLock
如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。
如果当前线程有线程获取的读锁和写锁,写锁会被阻塞起来
重入+1
- void lock()
- void lockInterruptibly()
- boolean tryLock()
- boolean tryLock(long timeout, TimeUnit unit)
- void unlock()
ReadLock
如果当前没有线程获得写锁,那就可以获取读锁
- void lock()
- void lockInterruptibly()
- boolean tryLock()
- boolean tryLock(long timeout, TimeUnit unit)
- void unlock()
StampedLock
提供三种锁:
写锁WriteLock
悲观读锁readLock
可以转换位写锁
乐观读锁tryOptimisticRead
不用加锁,只需要在使用的时候,判断一下有没有写锁!