zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java锁机制4.0

JAVA 机制 4.0
2023-09-11 14:22:32 时间

Java锁机制3.0 从源码角度分析了 AQS ,那么本章就来聊一聊一些主流的基于 AQS 的组件工具

1.啥是ReentrantLock?

ReentrantLock: 基于 AQS 在并发编程中它可以实现公平锁和非公平锁来对共享资源进行同步。
同时,和 synchronized 一样,ReentrantLock 支持可重入,除此之外,ReentrantLock 在调度上更灵活,支持更多丰富的功能。

下面是一些名词的释义:

公平锁: 按照请求锁的顺序分配(排队),拥有稳定获取锁的机会,但是性能比非公平锁低。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。


非公平锁: 不按照请求锁的顺序分配(哄抢),不一定拥有获得锁的机会,但是性能可能比公平锁高。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。


可重入: 单个线程执行时,重新进入同一个子程序仍然是线程安全的。

可重入可以这样理解:线程A在上下文中获取了某把锁,当A想再次获取该锁的时候,不会因为锁已经被自己占用而需要等待锁释放。加入A线程既获得了所,又在等待自己释放锁,那么就会造成死锁。
可重入简单来说,就是一个线程可以不用释放而重复获得一把锁多次,只是在释放的时候,也需要相应释放多次。

1.介绍一下Lock接口

先来看一看 ReentrantLock 的源码:

实现了Lock接口
public class ReentrantLock implements Lock, java.io.Serializable {
......

那么就进入到Lock接口看一看:
我们可以看到,这是一个注释非常非常非常多的一个接口。

public interface Lock {
	/*
		获取锁,
		如果锁被其他线程占用,则当前线程将等待直到获得锁为止。
	*/
    void lock();
public interface Lock {
	/*
		获取锁,
		但如果当前线程在等待锁的过程中被中断,就会退出等待,并且抛出InterruptedException
	*/
    void lockInterruptibly() throws InterruptedException;
	/*
		尝试获取锁,并立即返回,返回值为boolean类型
		boolean 代表是否获得锁
	*/
    boolean tryLock();
    /*
		在一段时间内尝试获取锁
		假如期间被中断,将会抛出中断异常
	*/
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
	/*
		释放锁
	*/
    void unlock();
	/*
		新建一个绑定在当前Lock上的Condition对象
	*/
    Condition newCondition();
}

condition对象有什么用呢?
简而言之,它表示一个等待的状态。
我们试想一下,获得锁的线程可能在某些时刻需要等待一些条件的完成继续执行,那么它可以通过 await 方法注册在 condition 对象上进行等待,然后通过 condition 对象的signal方法将其唤醒,这有点类似 Objectwaitnotify 方法。

但,不同的是:一个Lock对象可以关联多个Condition,多个线程可以绑定在不同的Condition对象上,这样就可以分组唤醒。 此外Condition还提供了和限时、中断相关的功能,丰富了线程的调度策略

Lock接口的介绍就到这里,可以看出它定义了一些广泛的操作边界。

2.ReentrantLock是如何按照Lock定义的规范来实现的?

ReentrantLock ——可重入的锁

打开 ReentrantLock 源码,可以看到,它只有一个属性 sync

private final Sync sync;

此外 ReentrantLock 内部最核心的三个内部类 SyncNonfairSyncFairSync,以及对上述Lock接口的实现

剩下的都是一些类私有的方法,不是很核心,主要是为了方便开发者来获取一些状态值。

1.Sync类

先来看一下内部类 Sync 的构造:

除了 lockreadObject 其他方法都被 final 修饰,不可被子类修改。

/*
	继承AQS,说明AQS中所有预设的机制,这边都可以直接使用
	抽象类,说明它需要通过子类来实例化
	而NonfairSync与FairSync则是它唯二的子类
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

		/*
			lock()是一个空实现等待子类来实现
				可以说明,公平模式(FairSync)、飞公平模式(NonfairSync)对于获取锁的实现一定是不一样的
		*/
        abstract void lock();

		......

		/*
			readObject(java.io.ObjectInputStream s)用于反序列化自身,很少使用
				可以忽略
		*/
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }

final 修饰的方法——nonfairTryAcquire

		/*
			非公平地尝试获取锁
		*/
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) { //当state=0时,锁状态为空闲,可以进行一次CAS来原子地修改state
                if (compareAndSetState(0, acquires)) { //如果state被修改成功,则代表获取了锁,将当前线程置为独占线程,并返回true
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) { //当state不为0,首先判断当前线程是否为独占线程(对可重入性的实现),即是否是自己占用了该锁
                int nextc = c + acquires;//要累加state来记录重入的次数
                if (nextc < 0) // overflow 当重入次数>Integer.MAX_VALUE就会溢出
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;//如果独占的线程不是自己,那么尝试获取锁失败,返回false,能满足可重入性,只不过我们需要累加state来记录重入的次数(nextc),因为到时释放的时候也要释放相应的次数
        }

final 修饰的方法——tryRelease
这里的返回值 truefalse 代表的是 是否完全被释放 而不是是否成功释放

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

final 修饰的方法——isHeldExclusively
判断当前线程是否为获取锁的独占线程

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

......

2.NonfairSync类

为什么有些情况下非公平锁的效率要高于公平锁呢?

在程序中,因为非公平锁意味着后请求锁的线程可能在前面休眠线程恢复前拿到锁,这样就可能提高并发性能。
这是因为通常情况下,去唤醒一个挂起的线程,线程状态切换之间会产生短暂的延时,非公平锁就可以利用这段时间来完成操作。

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))//不用管其他线程是否在等待,直接获取锁(非公平的),但只有一次获取锁的机会
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);//没获取到锁,就调用AQS的acquire方法,这时候就变成了公平锁
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//插队获取锁,调用父类方法
        }
    }

看一下acquire

	//再次尝试获取一下锁,尝试获取锁失败,就进入等待队列
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

小结一下:

  • lock提供了两次非公平获取锁的方法 compareAndSetState(0, 1)acquire(1) 。如果没获取到锁,就进入等待队列
  • tryAcquire 调用了父类的插队方法,如果一直想插队获取锁,可以在外部写一个循环来实现

3.FairSync

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);//调用父类方法
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            /*
				如果锁空闲且FIFO队列中没有排在当前线程之前的线程,
        		当前线程就可以去尝试获取锁,获取失败就返回false并进入acquire()进入排队流程
			*/
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            /*
				如果锁一开始就不是空闲的,为了满足可重入,也要做出相应判断
        			如果当前线程不是持有锁的独占线程,返回false
			*/
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

小结一下:

  • 如果当前线程已经获得锁,对state进行累加,可以继续取得锁,此外,只有在FIFO队列中当前线程之前没有其他线程的情况下可以直接尝试去拿锁,否则就排队

4.ReentrantLock对Lock的实现

构造器:

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

注意: sync 是被 final 修饰的,一旦初始化时选好实现类型,就无法改变


lock()lockInterruptibly()的区别:

  • lock 实际上是对 AQSacquire 的调用
    如果线程在排队等待锁的过程中被调用了中断,该线程不会立即抛出中断异常,而是存储该中断的中断值,直到获取锁之后才会抛出。
  • lockInterruptibly 调用了 AQSacquireInterruptibly
    该方法的主要实现为:线程在排队等待锁的过程中,如果被调用了中断,则直接抛出异常

Java的中断机制:

tryLock() 直接调用了sync.nonfairTryAcquire(1)
无论Sync的实现是公平锁还是非公平锁,tryLock的实现都是公平的

其他方法省…