深入学习Lock锁(3)——重入锁ReentrantLock

简介: 参考资料《Java并发编程的艺术》

1.简介

    重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时的公平和非公平性选择

    所谓重复加锁,就是某个线程中当调用lock方法对临界区加锁之后,在临界区中再次调用lock方法来进行加锁,如果同步组件不支持重复加锁就会对自己(当前线程)阻塞。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁。

    关于锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。但是也因此导致公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

2.实现重进入

    1.重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题:

    1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。     2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁 被释放时,计数自减,当计数等于0时表示锁已经成功释放。

    2.代码实现分析:ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现代码为例

        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;
        }

    该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来 决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。 成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值。释放代码如下

        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;
        }

3.公平性与非公平性获取锁的区别

   1.代码实现分析: 公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。

        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;
        }
    }

    该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。测试代码:

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock {
	private static ReentrantLock1 lock1=new ReentrantLock1(true);
	private static ReentrantLock1 lock2=new ReentrantLock1(false);
	static class ReentrantLock1 extends ReentrantLock{
		public ReentrantLock1(boolean isfair){
			super(isfair);
		}
		public Collection<Thread> getQueuedThreads(){
			ArrayList arrayList = new ArrayList<Thread>(super.getQueuedThreads());
			Collections.reverse(arrayList);
			return arrayList;
		}
	}
	static class TestThread extends Thread{
		ReentrantLock1 lock;
		int index;
		public TestThread(ReentrantLock1 l,int i){
			lock=l;
			index=i;
		}
		public String toString(){
			return index+"";
		}
		public void run(){
			lock.lock();
			try {
				System.out.println("线程"+Thread.currentThread()+"持有锁:"
			+"等待队列"+lock.getQueuedThreads());
			} catch (Exception e) {
				// TODO: handle exception
			}finally {
				System.out.println("释放");
				lock.unlock();
			}
			lock.lock();
			try {
				System.out.println("线程"+Thread.currentThread()+"持有锁:"
			+"等待队列"+lock.getQueuedThreads());
				
			} catch (Exception e) {
				// TODO: handle exception
			}finally {
				System.out.println("释放");
				lock.unlock();
			}
		}
	}
	static void testLock(ReentrantLock1 l){
		TestThread t1=new TestThread(l,1);
		TestThread t2=new TestThread(l,2);
		TestThread t3=new TestThread(l,3);
		TestThread t4=new TestThread(l,4);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
	public static void main(String[] args) {
		testLock(lock1);//测试公平锁
		//testLock(lock2);//测试不公平锁
	}

}
测试显示结果是随机的,对于已加入同步队列的线程执行确实是依据FIFO规则,但是线程加入同步队列的次序是由CPU调配的。
公平锁测试结果显示:
线程1持有锁:等待队列[2, 3, 4]
释放
线程2持有锁:等待队列[3, 4, 1]
释放
线程3持有锁:等待队列[4, 1, 2]
释放
线程4持有锁:等待队列[1, 2, 3]
释放
线程1持有锁:等待队列[2, 3, 4]
释放
线程2持有锁:等待队列[3, 4]
释放
线程3持有锁:等待队列[4]
释放
线程4持有锁:等待队列[]
释放

不公平锁测试结果
线程1持有锁:等待队列[2, 3, 4]
释放
线程1持有锁:等待队列[2, 3, 4]
释放
线程2持有锁:等待队列[3, 4]
释放
线程2持有锁:等待队列[3, 4]
释放
线程3持有锁:等待队列[4]
释放
线程3持有锁:等待队列[4]
释放
线程4持有锁:等待队列[]
释放
线程4持有锁:等待队列[]
释放

    对比测试结果可以知道,公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁出现了一个线程连续获取锁的情况。

    为什么会出现线程连续获取锁的情况呢?在nonfairTryAcquire(int acquires)方法中,当一个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。而之所以将非公平锁设为默认,就是因为它减少了线程间的切换,比如该测试中,非公平锁会切换4次,而公平锁则会切换8次,这就会带来极大地开销。但是非公平锁也会造成线程”饥饿”的问题。

相关文章
|
1月前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
4月前
|
Java
JUC(11)各种锁的理解(公平锁、可重入锁、自旋锁、死锁)
这篇文章介绍了Java并发包中的各种锁机制,包括公平锁与非公平锁、可重入锁、自旋锁以及死锁的概念、实现和示例,以及如何使用jps和jstack工具来检测和诊断死锁问题。
|
安全 Java
synchronized 锁与 ReentrantLock 锁的区别
synchronized 锁与 ReentrantLock 锁的区别
120 0
|
Java
JUC基础(三)—— Lock锁 及 AQS(2)
JUC基础(三)—— Lock锁 及 AQS
96 0
|
算法 调度
JUC基础(三)—— Lock锁 及 AQS(1)
JUC基础(三)—— Lock锁 及 AQS
136 0
【JUC基础】04. Lock锁
java.util.concurrent.locks为锁定和等待条件提供一个框架的接口和类,说白了就是锁所在的包。
5525 0
|
安全 Java
Java并发编程之Lock(同步锁、死锁)
这篇文章是接着我上一篇文章来的。
133 0
线程同步的方法:Synchronized、Lock、ReentrantLock分析
线程同步的方法:Synchronized、Lock、ReentrantLock分析
synchronized 锁的是什么?(二)
每个对象都存在着一个 Monitor 对象与之关联。执行 monitorenter 指令就是线程试图去获取 Monitor 的所有权,抢到了就是成功获取锁了;执行 monitorexit 指令则是释放了 Monitor 的所有权。
synchronized 锁的是什么?(二)