Java多线程之ReentrantLock

74 阅读4分钟

鄙人记忆中的ReentrantLock,是一个可以实现可重入功能的锁,它是基于AQS实现的锁,什么是可重入锁,即当一个线程在这个线程已经处于得到同步资源的状态时还要获取该同步资源的时候,它不会获取同步状态失败并且被加入到同步队列队尾,它只会判断是否是同一个线程后在自己的计数器上加1,而它释放同步状态的条件就是当这个计数器为0的时候

其次,就是公平锁和非公平锁,公平锁的性能要比非公平锁要低,对于公平锁而言,就是严格遵循同步队列的FIFO,先到先得,不会出现插队的情况,每个线程获取同步状态时需要判断自己的前驱结点是否是头节点;而非公平锁,是不需要按照FIFO的,每个线程都有获取得到同步状态的机会,而且当前获取同步状态的线程在释放资源以后是有很大概率再获取得到该资源的(大大的减少了线程之间的切换)

回忆完了。。。进入正题!

ReentrantLock

  • 表示该锁能够支持一个线程对资源的重复加锁,还支持获取锁时的公平和非公平性选择
  • ReentrantLock在调用lock()方法的时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞
    • 公平锁:如果在绝对时间上,先对锁进行获取的请求一定先被满足
    • 非公平锁:反之,则为非公平锁
    • 公平锁的机制往往没有非公平的效率高,但公平锁可以减少"饥饿"发生的概率,等待越久的请求越是能够得到优先满足。

实现可重入

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

尝试获取锁的逻辑

final boolean nonfairTryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //尝试获取同步状态
            int c = getState(); 
            //如果同步状态==0表示获取成功,通过CAS设置当前状态
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                //设置为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果同步状态不为0那么就需要判断是否是当前线程持有锁
            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 tryRelease(int releases) {
             //获取同步状态并进行相减
            int c = getState() - releases;
            //线程不一致,则直接报错
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果当前状态为0,则释放锁成功
            if (c == 0) {
                free = true;
                //将线程置为null
                setExclusiveOwnerThread(null);
            }
            设置新的状态
            setState(c);
            return free;
        }

公平锁和非公平锁

公平锁

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //获取同步状态
            int c = getState();
            if (c == 0) {
            //判断没有前驱结点并且CAS设置同步状态成功,则获取锁成功
                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;
        }
    }

获取同步状态,如果它有前驱节点,那么代表有线程比当前线程更早地请求获取了锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁

非公平锁

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //只要通过CAS设置状态成功即可
        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;
}

为什么会出现在非公平锁中线程连续获取锁的情况?

从nonfairTryAcquire(int acquires)方法,当一个线程请求锁时,只要获取了同步状态及成功获取锁,所以在这个前提下,刚释放锁的线程再获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待(省去了线程的切换)

总结:

公平锁保证锁的获取按照FIFO原则,而代价是进行大量的线程切换(即一个线程执行完毕之后就会从同步队列移除),而非公平锁,由于只需要获取同步状态成功,那么就代表可以成功获取,对于刚刚释放的线程在获取同步状态的几率会非常大。减少了线程之间切换所带来的开销,虽然可能会造成线程的"饥饿",但极少的线程切换,保证了其更大的吞吐量。