欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ReentrantLock重入锁简介源码以及测试死锁时的问题

程序员文章站 2022-06-28 17:06:07
重入锁,顾名思义,就是支持重新进入的锁。它表示该锁能支持一个线程对资源的反复加锁。每一个重入锁都绑定一个计数器和占有它的线程。锁未被占有时,计数器为0。当一个线程请求未被占有的锁时,计数器会被置为1,并且记录下当前占有的线程。如果不同的线程尝试获取锁,会被阻塞。...

what

重入锁,顾名思义,就是支持重新进入的锁。它表示该锁能支持一个线程对资源的反复加锁。每一个重入锁都绑定一个计数器和占有它的线程。锁未被占有时,计数器为0。当一个线程请求未被占有的锁时,计数器会被置为1,并且记录下当前占有的线程。如果不同的线程尝试获取锁,会被阻塞。如果相同的线程获取锁,计数器会递增。当一个线程请求释放已经被占有的锁时,如果不是当前获取锁的线程,则会抛出非法监视器状态异常。如果是当前获取锁的线程,每次释放锁,同步器都会递减,如果计数器为0,则代表锁已经被释放。
除此之外,该锁还支持获取锁时公平锁和非公平锁的选择。如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁就是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁的获取是顺序的。ReentrantLock提供了构造函数,可以用来控制锁是否公平。

where

1.生产消费者模式
如果仓库满了,需要通知生产者暂停生产,如果仓库空了,需要通知消费者暂停消费,如果仓库从满到不满,需要通知生产者继续生产,如果仓库从空到不空,需要通知消费者继续消费。
2.追求性能的非公平锁
3.追求先进先获取的非公平锁

why

ReentrantLock绝大多数功能都和synchronized类似,主要有以下优点:
1.提供了公平锁和非公平锁两种方式
2.提供了可中断、可限时、可尝试三种获取锁的方式,可以有效防止死锁
3.可以生成多个等待队列,实现生产者消费者模式

API

ReentrantLock有三个内部类,Sync、NonfairSync、FairSync,NonfairSync和FairSync继承Sync抽象类,Sync类继承AQS抽象类。Sync和NonfairSync实现的是非公平锁的逻辑,FairSync实现的公平锁的逻辑。

1.Sync

方法名称 描述
abstract void lock() 获取锁,抽象方法,需要NonfairSync和FairSync重写
boolean nonfairTryAcquire(int arg) 非公平尝试获取锁
boolean tryRelease(int arg) 尝试释放锁
boolean isHeldExclusively() 判定锁是否被当前线程独占
ConditionObject newCondition() 新建一个条件组件对象
Thread getOwner() 返回当前获取锁的线程,如果没有返回null
int getHoldCount() 如果是当前线程获取的锁,返回重入次数,否则返回0
boolean isLocked() 判定当前锁是否被持有

2.NonfairSync

方法名称 描述
void lock() 使用非公平的方式获取锁
boolean tryAcquire(int arg) 使用非公平的方式尝试获取锁,会直接调用nonfairTryAcquire()方法

3.FairSync

方法名称 描述
void lock() 使用公平的方式获取锁
boolean tryAcquire(int arg) 使用公平的方式尝试获取锁

4.ReentrantLock

方法名称 描述
ReentrantLock() 无参默认构造方法,默认实现非公平锁
ReentrantLock(boolean fair) 传入True为公平锁,false为非公平锁
void lock 获取锁的顶层入口方法,会调用Sync的实现类的lock()方法
void lockInterruptibly() throws InterruptedException 可中断的获取锁,会调用AQS的acquireInterruptibly()方法
void tryLock() 尝试获取锁,会调用Sync实现类的tryAcquire()方法
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException 可限时的获取锁,会调用AQS的tryAcquireNanos()方法
void unlock() 释放锁,会调用AQS的release方法

源码

1.非公平锁和公平锁

公平锁和非公平锁的区别就在tryAcquire方法的实现逻辑上。

 //非公平锁
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        } 
 //公平锁
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        } 
 //查询是否有任何线程在等待获取比当前线程更长的时间。
    //请注意,由于中断和超时导致的取消可能随时发生,因此{@code true}返回值不能保证其他线程将在当前线程之前获取。
    //同样,由于队列为空,此方法返回{@code false}后,另一个线程也有可能先获取到锁。
    //此方法设计为由公平同步器使用
	//返回true代表当前线程是头节点的后继线程,或者队列为空,也就是头节点的后继线程为空
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    } 

从上我们可以看出,公平锁和非公平锁实现逻辑唯一的区别就是判断条件多了hasQueuedPredecessor()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示在队列中有当前节点的前驱节点,因此需要等待前驱节点获取并释放锁之后才能继续获取锁。

为什么要加这个方法呢,我们可以看下调用tryAcquire方法调用的逻辑。

 //非公平锁
    static final class NonfairSync extends Sync {
		//此方法调用AQS的acquire()方法
        final void lock() {
        	//这边多尝试一次获取锁
        	//但是只是在锁没有被占有的时候才能成功,因此这次是不支持重入的
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    } 
 //公平锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    } 
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    } 

我们可以看到,fairSync和nonfairSync的lock方法都会调用AQS的acqure方法,而acquire方法在获取锁时会主动先尝试获取一次锁,而不是先入队,所以公平锁要先确定一下当前线程是不是头节点的后继线程,否则就不能达到公平的效果。
这里有个小细节,nonfairSync的lock方法会在一开始就尝试获取一次锁,但是这次获取只是在锁没有被占有的时候能成功,如果是重入会失败。

公平锁和非公平锁释放锁的逻辑是一样的。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        } 

从这里我们可以看出,互斥重入锁只有当同步状态state值为0的时候,才能代表成功释放,否则都不能返回true。

2.非公平锁和公平锁性能比较

我们编写一个测试类观察公平和非公平锁在获取锁时的区别,在测试用例中定义了内部类ReentrantLock2,该类主要公开了getQueueudThreads方法,该方法返回正在等待获取锁的线程列表,由于列表是逆序输出,为了方便观察,将其进行反转。

public class FairAndUnfairTest {
	
	private static ReentrantLock2 fairLock = new ReentrantLock2(true);
	private static ReentrantLock2 unfairLock = new ReentrantLock2(false);
	
	public void fair() {
		testLock(fairLock);
	}
	
	public void unfair() {
		testLock(unfairLock);
	}
	
	private void testLock(ReentrantLock2 lock) {
		for(int i = 0; i < 4; i++) {
			 Job job = new Job(lock);
			 job.setName("Thread" + i);
			 job.start();
		}
	}
	
	private static class Job extends Thread{
		private ReentrantLock2 lock;
		public Job(ReentrantLock2 lock) {
			this.lock = lock;
		}
		
		public void run() {
			for(int i = 0; i < 2; i++) {
				try {
					lock.lock();
					System.out.println(Thread.currentThread().getName());
					System.out.println("--i--");
					System.out.println(lock.getQueuedThreads());
				} finally {
					lock.unlock();
				}
			}

		}
	}
	
	private static class ReentrantLock2 extends ReentrantLock{
		public ReentrantLock2(boolean fair) {
			super(fair);
		}
		public Collection<Thread> getQueuedThreads(){
			List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());
			Collections.reverse(arrayList);
			return arrayList;
		}
	}

} 

非公平锁返回结果

Thread0
--1--
[Thread[Thread1,5,main]]
Thread0
--2--
[Thread[Thread1,5,main], Thread[Thread2,5,main], Thread[Thread3,5,main]]
Thread1
--1--
[Thread[Thread2,5,main], Thread[Thread3,5,main]]
Thread1
--2--
[Thread[Thread2,5,main], Thread[Thread3,5,main]]
Thread2
--1--
[Thread[Thread3,5,main]]
Thread2
--2--
[Thread[Thread3,5,main]]
Thread3
--1--
[]
Thread3
--2--
[] 

公平锁返回结果

Thread0
--1--
[Thread[Thread1,5,main]]
Thread1
--1--
[Thread[Thread2,5,main], Thread[Thread3,5,main], Thread[Thread0,5,main]]
Thread2
--1--
[Thread[Thread3,5,main], Thread[Thread0,5,main], Thread[Thread1,5,main]]
Thread3
--1--
[Thread[Thread0,5,main], Thread[Thread1,5,main], Thread[Thread2,5,main]]
Thread0
--2--
[Thread[Thread1,5,main], Thread[Thread2,5,main], Thread[Thread3,5,main]]
Thread1
--2--
[Thread[Thread2,5,main], Thread[Thread3,5,main]]
Thread2
--2--
[Thread[Thread3,5,main]]
Thread3
--2--
[] 

观察结果可得,公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平锁出现了一个线程连续获得锁的情况。
为什么会出现连续获取锁的情况呢?回顾nonfairTryAcquire(int arg)方法,当一个线程请求锁时,只要获取了同步状态即可成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。
非公平锁可能会使线程饥饿,为什么它又被设定成默认的实现呢?再次观察上表的结果,如果把每次不同线程获取到锁定义为1次切换,公平锁在测试中进行了8次切换,而非公平锁只有4次切换,这说明非公平锁的开销更小。下面我们测试运行时间:10个线程,每个线程获取1,000,000次锁,每个线程获取锁之后自旋1,000,000下

public class FairAndUnfairTest {
	
	static long start = 0;
	
	private static ReentrantLock2 fairLock = new ReentrantLock2(true);
	private static ReentrantLock2 unfairLock = new ReentrantLock2(false);
	
	public void fair() {
		start = System.currentTimeMillis();
		testLock(fairLock);
	}
	
	public void unfair() {
		start = System.currentTimeMillis();
		testLock(unfairLock);
		
		
	}
	
	private void testLock(ReentrantLock2 lock) {
		for(int i = 0; i < 10; i++) {
			 Job job = new Job(lock);
			 job.setName("Thread" + i);
			 job.start();
		}
	}
	
	private static class Job extends Thread{
		private ReentrantLock2 lock;
		public Job(ReentrantLock2 lock) {
			this.lock = lock;
		}
		
		public void run() {
			for(int i = 0; i < 1000000; i++) {
				try {
					lock.lock();
					for(int j =0; j < 1000000; j++) {
						
					}
					
				}finally {
					lock.unlock();
				}
			}

			long end = System.currentTimeMillis();
			System.out.println("运行时间:" + (end - start));
		}
	}
	
	private static class ReentrantLock2 extends ReentrantLock{
		public ReentrantLock2(boolean fair) {
			super(fair);
		}
		public Collection<Thread> getQueuedThreads(){
			List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());
			Collections.reverse(arrayList);
			return arrayList;
		}
	}

} 

非公平锁结果:

运行时间:260
运行时间:277
运行时间:290
运行时间:296
运行时间:296
运行时间:300
运行时间:301
运行时间:303
运行时间:306
运行时间:306 

公平锁结果:

运行时间:68456
运行时间:68458
运行时间:68460
运行时间:68460
运行时间:68460
运行时间:68460
运行时间:68461
运行时间:68461
运行时间:68461
运行时间:68461 

在测试中,公平锁与非公平锁相比,总耗时是其20多倍。可以看出,公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平锁虽然可能造成线程饥饿,但是极少的线程切换,保证了其更大的吞吐量。

3.实现生产者/消费者模式一对一交替输出

一个ConditionObject对应一个等待队列,我们可以使用2个等待队列完成生产者消费者模式。

public class ConditionTest {

	private ReentrantLock lock = new ReentrantLock();
	private Condition setCondition = lock.newCondition();
	private Condition getCondition = lock.newCondition();
	
	private boolean hasValue = false;
	
	public void set() {
		try {
			lock.lock();
			if(hasValue == true) {
				setCondition.await();
			}
			System.out.println("生产1个");
			hasValue = true;
			getCondition.signal();
		}catch (Exception e) {
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
	
	public void get() {
		try {
			lock.lock();
			if(hasValue == false) {
				getCondition.await();
			}
			System.out.println("消费1个");
			hasValue = false;
			setCondition.signal();
		}catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
		
	}
	
	static class ThreadSet extends Thread{
		private ConditionTest conditionTest;
		
		public ThreadSet(ConditionTest conditionTest) {
			super();
			this.conditionTest = conditionTest;
		}
		
		public void run() {
			for(int i = 0; i < 10; i++) {
				System.out.println("已生产个数" + i);
				conditionTest.set();
			}
		}
	}
	
	static class ThreadGet extends Thread{
		private ConditionTest conditionTest;
		
		public ThreadGet(ConditionTest conditionTest) {
			super();
			this.conditionTest = conditionTest;
		}
		
		public void run() {
			for(int i = 0; i < 10; i++) {
				System.out.println("已消费个数" + i);
				conditionTest.get();
			}
		}
	}
	
	public static void main(String[] args) {
		ConditionTest conditionTest = new ConditionTest();
		
		ThreadSet threadSet = new ThreadSet(conditionTest);
		threadSet.start();
		
		ThreadGet threadGet = new ThreadGet(conditionTest);
		threadGet.start();
		
		

	}
} 

打印结果如下:

已生产个数0
生产1个
已生产个数1
已消费个数0
消费1个
已消费个数1
生产1个
已生产个数2
消费1个
已消费个数2
生产1个
已生产个数3
消费1个
已消费个数3
生产1个
已生产个数4
消费1个
已消费个数4
生产1个
已生产个数5
消费1个
已消费个数5
生产1个
已生产个数6
消费1个
已消费个数6
生产1个
已生产个数7
消费1个
已消费个数7
生产1个
已生产个数8
消费1个
已消费个数8
生产1个
已生产个数9
消费1个
已消费个数9
生产1个
消费1个 

这样我们可以让不同功能的线程进入不同的等待队列进行等待,然后条件满足之后分别进行唤醒。

4.死锁

我们可以看个代码

public class TryLockTest {

static Lock lockA = new ReentrantLock();
	static Lock lockB = new ReentrantLock();
	
	public static void get(Lock a, Lock b){
		try {
			System.out.println(Thread.currentThread().getName() + "得到了a锁");
			a.tryLock();
			System.out.println(Thread.currentThread().getName() + "得到了b锁");
			b.tryLock();
		}finally {
			try {
				System.out.println(Thread.currentThread().getName() + "释放了a锁");
				b.unlock();
			}catch(Exception e){
				e.printStackTrace();
			}
			
			try {
				System.out.println(Thread.currentThread().getName() + "释放了b锁");
				a.unlock();
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		Thread a = new Thread("线程A") {
			public void run() {
				get(lockA, lockB);	
				System.out.println(Thread.currentThread().getName() + "完成了任务");
			}
		};
		Thread b = new Thread("线程B") {
			public void run() {
				get(lockB,lockA);
				System.out.println(Thread.currentThread().getName() + "完成了任务");
			}
		};
		a.start();
		b.start();
	}
} 

运行结果是

线程B得到了a锁
线程A得到了a锁
线程A得到了b锁
线程B得到了b锁 

此时就会死锁。

但是我们如果将lock改为用tryLock方法

try {
			System.out.println(Thread.currentThread().getName() + "得到了a锁");
			a.tryLock();
			System.out.println(Thread.currentThread().getName() + "得到了b锁");
			b.tryLock();
		}finally {
			try {
				System.out.println(Thread.currentThread().getName() + "释放了a锁");
				b.unlock();
			}catch(Exception e){
				e.printStackTrace();
			}
			
			try {
				System.out.println(Thread.currentThread().getName() + "释放了b锁");
				a.unlock();
			}catch(Exception e){
				e.printStackTrace();
			}
		} 

结果为:

线程A得到了a锁
线程A得到了b锁
线程A释放了a锁
线程B得到了a锁
线程B得到了b锁
线程B释放了a锁
线程A释放了b锁
线程A完成了任务
java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(Unknown Source)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source)
	at java.util.concurrent.locks.ReentrantLock.unlock(Unknown Source)
	at com.morlia.platform.aqs.TryLockTest.get(TryLockTest.java:27)
	at com.morlia.platform.aqs.TryLockTest$2.run(TryLockTest.java:50)
线程B释放了b锁
线程B完成了任务 

此时就不会产生死锁

思维导图ReentrantLock重入锁简介源码以及测试死锁时的问题

有个奇怪的现象,求大神解答

关于前面死锁的程序,有两种写法
第一种写法:

public class TryLockTest {

    static Lock lockA = new ReentrantLock();
	static Lock lockB = new ReentrantLock();
	
	public static void get(Lock a, Lock b){
		try {
			System.out.println(Thread.currentThread().getName() + "得到了a锁");
			a.lock();
			System.out.println(Thread.currentThread().getName() + "得到了b锁");
			b.lock();
		}finally {
			try {
				System.out.println(Thread.currentThread().getName() + "释放了a锁");
				b.unlock();
			}catch(Exception e){
				e.printStackTrace();
			}
			
			try {
				System.out.println(Thread.currentThread().getName() + "释放了b锁");
				a.unlock();
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		Thread a = new Thread("线程A") {
			public void run() {
				get(lockA, lockB);	
				System.out.println(Thread.currentThread().getName() + "完成了任务");
			}
		};
		Thread b = new Thread("线程B") {
			public void run() {
				get(lockB,lockA);
				System.out.println(Thread.currentThread().getName() + "完成了任务");
			}
		};
		a.start();
		b.start();
	}
} 

结果为:

线程A得到了a锁
线程B得到了a锁
线程B得到了b锁
线程A得到了b锁 

可以看出线程A和B都分别尝试获取了两次锁。

另一种写法:

public class ReentrantLockTest {
	
	static Lock lock1 = new ReentrantLock();
	static Lock lock2 = new ReentrantLock();
	
	public static void main(String[] args) {
		Thread thread1 = new Thread(new ThreadDemo(lock1, lock2));
		Thread thread2 = new Thread(new ThreadDemo(lock2, lock1));
		thread1.start();
		thread2.start();
	}

	static class ThreadDemo implements Runnable{
		Lock A = null;
		Lock B = null;
				
		public ThreadDemo(Lock a, Lock b) {
			A = a;
			B = b;
		}
				
		public void run() {
			try {
				A.lock();
				System.out.println(Thread.currentThread().getId() + "A lock");
				B.lock();
				System.out.println(Thread.currentThread().getId() + "B lock");
			}finally {
				B.unlock();
				System.out.println(Thread.currentThread().getId() + "B unlock");
				A.unlock();
				System.out.println(Thread.currentThread().getId() + "A unlock");
			}
		}
	}
} 

结果为:

10A lock
11A lock 

可以看出线程10和11都分别是获取了一次锁
怎么两次获取锁的次数不一致,希望大神能解答一下,谢谢!!

本文地址:https://blog.csdn.net/d303577562/article/details/108206541

相关标签: java