手写ReentrantLock
最近学习了Java语言中锁相关知识,看了一下ReentrantLock
源码,自己手写了一个ReentrantLock
。
ReentrantLock
是一个可重入锁,并且在源码中通过构造函数可以使其在公平锁和非公平锁之间转换。
可重入锁即当前线程可以在不释放锁的情况下多次获取锁,但是释放锁的次数应与获取锁的次数相同,否则会抛出IllegalMonitorStateException
异常。
公平锁和非公平锁的区别在与,公平锁使先进入等待队列的线程先获取锁,每个线程都有机会获取锁。非公平锁则是每个线程获取锁的几率不确定,非公平锁并发性较好,但容易造成某些线程长时间获取不到锁。
不可重入ReentrantLock,非公平
/**
* 不可重入锁
*/
public class WonderReentranrLock{
//标记获取锁的线程,原子类型的引用保证原子性
private AtomicReference<Thread> owner = new AtomicReference<>();
//抢锁失败时进入等待队列
private Queue<Thread> waitQueue = new LinkedBlockingQueue();
/**
* 加锁
*/
public void lock(){
//抢锁失败
if(!tryLock()){
Thread current = Thread.currentThread();
//线程进入等待队列
waitQueue.offer(current);
//继续尝试抢锁
for(;;){
//获取队列头部
Thread head = waitQueue.peek();
//如果当前线程在队列头部
if(current == head){
//抢锁
if(!tryLock()){
//如果抢锁失败
LockSupport.park();
}else{
//如果抢锁成功,线程出队列
waitQueue.poll();
return;
}
}else{
//如果不在队列头部,将线程挂起
LockSupport.park();
}
}
}
//成功则抢到了锁
}
/**
* 尝试加锁
* @return
*/
public boolean tryLock(){
Thread current = Thread.currentThread();
//CAS原子性操作修改标记,即抢锁
return owner.compareAndSet(null,current);
}
/**
* 解锁
*/
public void unLock(){
//当前线程释放锁成功
if(tryUnLock()){
Thread head = waitQueue.peek();
//队列头部是否存在
if(head!=null){
//存在则唤醒队列头部线程去抢锁
LockSupport.unpark(head);
}
}
}
/**
* 尝试解锁
* @return
*/
public boolean tryUnLock(){
Thread current = Thread.currentThread();
//判断当前线程是否持有锁
if(owner.get()!=current){
throw new IllegalMonitorStateException();
}else{
//持有则CAS操作释放锁
return owner.compareAndSet(current,null);
}
}
}
这个ReentrantLock
是一个非公平,不可重入的锁。
这里有一个坑:就是虽然加锁时判断了是否时队列头部,如果是队列头部就让它去抢锁,如果不是头部则挂起,这里看上去是队列的先进先出,好像是个公平锁,但是在多线程中可能出现队列头部与不在队列中的线程抢锁,头部线程可能抢锁失败,所以是非公平的。
再写一个可重入锁(非公平),公平锁貌似实现比较复杂,先不考虑。
可重入ReentrantLock,非公平
/**
* 可重入锁
*/
public class WonderReentrantLock2 {
//标记获取锁的线程,抢的不是owner不用原子类型
private Thread owner = null;
//记录重入次数,抢锁,抢的是count
private AtomicInteger count = new AtomicInteger();
//抢锁失败时进入等待队列
private Queue<Thread> waitQueue = new LinkedBlockingQueue();
/**
* 加锁,与不可重入锁一样
*/
public void lock(){
//抢锁失败
if(!tryLock()){
Thread current = Thread.currentThread();
//线程进入等待队列
waitQueue.offer(current);
//继续尝试抢锁
for(;;){
//获取队列头部
Thread head = waitQueue.peek();
//如果当前线程在队列头部
if(current == head){
//抢锁
if(!tryLock()){
//如果抢锁失败
LockSupport.park();
}else{
//如果抢锁成功,线程出队列
waitQueue.poll();
return;
}
}else{
//如果不在队列头部,将线程挂起
LockSupport.park();
}
}
}
//成功则抢到了锁
}
/**
* 尝试加锁
* @return
*/
public boolean tryLock(){
Thread current = Thread.currentThread();
//抢到锁的次数
int ct = count.get();
//次数不为0,意味着有线程已经抢到锁
if (ct!=0){
//如果抢到锁的线程是当前线程
if(owner==current){
//重入,次数+1
count.set(ct+1);
return true;
}else{
//不是当前线程,抢锁失败
return false;
}
}else{
//次数为0,意味着没有线程抢到锁,CAS操作抢锁
if(count.compareAndSet(ct,ct+1)){
//抢到锁后将owner标记为当前线程
owner = current;
return true;
}else{
//CAS抢锁失败
return false;
}
}
}
/**
* 解锁
*/
public void unLock(){
//当前线程释放锁成功
if(tryUnLock()){
Thread head = waitQueue.peek();
//队列头部是否存在
if(head!=null){
//存在则唤醒队列头部线程去抢锁
LockSupport.unpark(head);
}
}
}
/**
* 尝试解锁
* @return
*/
public boolean tryUnLock(){
Thread current = Thread.currentThread();
//判断当前线程是否持有锁
if(owner!=current){
throw new IllegalMonitorStateException();
}else{
//获取加锁次数
int ct = count.get();
//解锁后count应为next的值
int next = ct - 1;
//count.set(next); 坑!
if(next==0){
//解锁后count为0的话,即所加的锁全部解开,将owner置为null
owner = null;
//owner为null后再将count值修改为0
count.set(next);
return true;
}else{
//如果next不为0,意味着还有重入的锁未解,修改count即可,owner不变
count.set(next);
return false;
}
}
}
}
可重入锁和不可重入锁的区别在于:不可重入锁是线程争抢owner(线程标记)的过程,可重入锁增加了count记录一个线程重入的次数。因此,可重入锁在抢锁时,首先判断count是否为0,count为0则锁空闲,先使用CAS
修改count,成功后修改owner为当前线程,抢锁成功。若count不为0,说明锁被某一线程占用,则判断占用锁的线程是否是当前线程,如果是,为count加1即可实现重入,如果不是当前线程则抢锁失败挂起。
在写可重入锁时遇到了一个坑,代码中tryUnLock()
方法如果owner是当前线程则开始执行解锁,刚开始写的是先修改count值为原值减1,再将owner置为null。运行后发现一直报IllegalMonitorStateException
错误,就是说当前线程加锁后,尝试解锁时owner不是当前线程。这是因为先修改了count后修改owner,如果修改后count值为0,那么就会有其他线程抢到锁并且修改owner,此时owner就不是原先的线程了。这里将修改count放在owner=null之后就可以解决问题。也可以将owner改为AtomicReference
类型解决。
测试代码
public class Test {
static WonderReentrantLock lock = new WonderReentrantLock();
static WonderReentrantLock2 lock2 = new WonderReentrantLock2();
volatile static int count=0;
public static void main(String[] args) throws InterruptedException {
for(int i=0; i<10; i++){
new Thread(){
@Override
public void run() {
for(int j=0; j<10000; j++){
lock2.lock();//重入
count++;
}
for(int j=0; j<10000; j++){
lock2.unLock();//加锁多少次,解锁多少次
}
System.out.println("over...");
}
}.start();
}
Thread.sleep(4000);
System.out.println(count);
/*for(int i=0; i<10; i++){
new Thread(){
@Override
public void run() {
for(int j=0; j<10000; j++){
lock.lock();//不可重入
count++;
lock.unLock();
}
System.out.println("over...");
}
}.start();
}
Thread.sleep(4000);
System.out.println(count);*/
}
}