精华内容
下载资源
问答
  • 2020-06-06 16:32:20

    上篇我们讲了Java的AQS详解1–独占锁的获取及释放,本篇接着讲共享锁的获取及释放。

    加锁

    共享锁加锁的方法入口为:

    public final void acquireShared(int arg) {
    	if (tryAcquireShared(arg) < 0)
    		doAcquireShared(arg);
    }
    

    tryAcquireShared(arg)尝试获取锁,由AQS的继承类实现。

    若返回值为负,证明获取锁失败,紧接着执行doAcquireShared(arg)方法。

    doAcquireShared

    private void doAcquireShared(int arg) {
    	// 将该线程封装成共享节点,并追加到同步队列中
    	final Node node = addWaiter(Node.SHARED);
    	// 失败标志
    	boolean failed = true;
    	try {
    		// 中断标志
    		boolean interrupted = false;
    		for (;;) {
    			// 获取node的前继节点
    			final Node p = node.predecessor();
    			// 若node的前继节点为head节点,则执行tryAcquireShared方法尝试获取锁(资源)
    			if (p == head) {
    				int r = tryAcquireShared(arg);
    				// 若返回值>=0,表明获取锁成功
    				if (r >= 0) {
    					// 将当前节点设置为head节点,并唤醒后继节点
    					setHeadAndPropagate(node, r);
    					p.next = null; // help GC
    					// 如果中断标志位true,响应掉中断
    					if (interrupted)
    						selfInterrupt();
    					failed = false;
    					return;
    				}
    			}
    			// 若前继节点不为head节点或者前继节点为head,但tryAcquireShared获取锁失败
    			// shouldParkAfterFailedAcquire自旋CAS将node的前继节点的状态设置为SIGNAL(-1),并返回true
    			// parkAndCheckInterrupt将线程阻塞挂起,重新被唤醒后检查阻塞期间是否被中断过,将interrupted置为true
    			if (shouldParkAfterFailedAcquire(p, node) &&
    				parkAndCheckInterrupt())
    				interrupted = true;
    		}
    	} finally {
    		// 若线程异常,则放弃获取锁
    		if (failed)
    			cancelAcquire(node);
    	}
    }
    

    可以看到,doAcquireShared方法和独占锁的acquireQueued方法逻辑类似,主要有2点不同:

    • doAcquireShared方法直接将中断响应掉了,而acquireQueued只是返回中断标志,是否响应留在了acquire方法中;
    • doAcquireShared方法获取锁成功之后,除了将当前节点设置为head之外,还有个唤醒后继节点的操作,即setHeadAndPropagate方法。

    setHeadAndPropagate

    private void setHeadAndPropagate(Node node, int propagate) {
    	// 原有head节点备份
    	Node h = head; 
    	// 将当前节点设置为head
    	setHead(node);
    	
    	// 若propagate>0(有剩余资源)或者原head节点为null或原head节点的状态值<0
    	if (propagate > 0 || h == null || h.waitStatus < 0 ||
    		(h = head) == null || h.waitStatus < 0) {
    		// 获取node的后继节点
    		Node s = node.next;
    		// 若后继节点为null或者为共享节点,则执行doReleaseShared方法继续传递唤醒操作
    		if (s == null || s.isShared())
    			doReleaseShared();
    	}
    }
    

    doReleaseShared

    private void doReleaseShared() {
    	for (;;) {
    		// 此时的head节点已经被替换为node节点了
    		Node h = head;
    		// 若head不为null且不是tail节点
    		if (h != null && h != tail) {
    			int ws = h.waitStatus;
    			// 若head节点状态为SIGNAL(-1),则自旋CAS将head节点的状态设置为0之后,才可以唤醒head结点的后继节点
    			if (ws == Node.SIGNAL) {
    				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
    					// 执行下一次自旋
    					continue;
    				unparkSuccessor(h);
    			}
    			// 若head节点状态为0,则自旋CAS将节点状态设置为PROPAGATE(-3)
    			else if (ws == 0 &&
    					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
    				// 执行下一次自旋
    				continue;
    		}
    		// head指针在自旋期间未发生移动的话,跳出自旋
    		if (h == head)
    			break;
    	}
    }
    

    为什么最后需要判断(h==head)才跳出自旋?

    想象2种情景:

    • 第1种情景

    线程thread1自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread2,当执行到(h == head)时,假如thread2唤醒后已经将head指向自己了,此时(h == head)返回false,thread1继续自旋获取到新的head节点(thread2);

    thread1自旋CAS将新的head节点(thread2)的状态由SIGNAL修改为0,然后去唤醒thread2的后继线程thread3,当执行到(h == head)时,假如thread3唤醒后已经将head指向自己了,此时(h == head)返回false,thread1继续自旋获取到新的head节点(thread3),

    thread1自旋CAS将新的head节点(thread3)的状态由SIGNAL修改为0,然后去唤醒thread3的后继线程thread4…

    直到某个被唤醒的线程因为获取不到锁(资源被用尽)执行shouldParkAfterFailedAcquire方法被阻塞挂起,head节点才没有发生改变,此时(h == head)返回true,跳出自旋。

    • 第2种情景

    线程thread1自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread2,当执行到(h == head)时,假如thread2唤醒后还未来得及将head指向自己,此时(h == head)返回true,thread1停止自旋;

    thread2唤醒后将执行setHeadAndPropagate方法将head指向自己,并最终进到doReleaseShared方法的自旋中;

    此时,线程thread2自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread3…

    哈哈哈,是不是像thread1一样又面临了2种情景。

    可以看到,整个唤醒后继节点的过程是不断嵌套,螺旋执行的,每个节点的线程都最大程度的尝试唤醒其可以唤醒的节点,而且每个线程都是唤醒的head的后继节点,head指针不断往后推进,则被唤醒尝试获取共享锁的线程越多,而新的线程一旦获取到锁,其又会执行到setHeadAndPropagate–>doReleaseShared的自旋中,加入到唤醒head后继节点的联盟大军中,直到无锁可获。

    所以,整个唤醒后继节点的过程如果一场风暴一样,不得不惊叹这样的设计呀,最大程度的诠释了何为共享,就是"有肉一起吃,有酒一起喝"。

    解锁

    public final boolean releaseShared(int arg) {
    	if (tryReleaseShared(arg)) {
    		doReleaseShared();
    		return true;
    	}
    	return false;
    }
    

    某线程执行tryReleaseShared方法成功后,会释放掉部分资源,然后执行doReleaseShared方法唤醒当前head节点的后继线程,来参与分享资源。

    doReleaseShared方法前面陈述过了,这是个"唤醒风暴",它会唤醒所有可以唤醒的人来参与资源的分享。

    整个获取/释放资源的过程是通过传播完成的,如最开始有10个资源,线程A、B、C分别需要5、4、3个资源。

    • A线程获取到5个资源,其发现资源还剩余5个,则唤醒B线程;
    • B线程获取到4个资源,其发现资源还剩余1个,唤醒C线程;
    • C线程尝试取3个资源,但发现只有1个资源,继续阻塞;
    • A线程释放1个资源,其发现资源还剩余2个,故唤醒C线程;
    • C线程尝试取3个资源,但发现只有2个资源,继续阻塞;
    • B线程释放2个资源,其发现资源还剩余4个,唤醒C线程;
    • C线程获取3个资源,其发现资源还剩1个,继续唤醒后续等待的D线程;

    回顾整个共享锁加锁和解锁的过程,可以发现head指针至关重要,无论是加锁成功后执行setHeadAndPropagate方法进而执行doReleaseShared方法,还是线程解锁时直接执行doReleaseShared方法,其均是直接从当前队列的的head节点的后继节点开始"唤醒",而被唤醒的多个线程也是通过(h == head)判断来决定是否跳出"唤醒自旋"的。

    最后,再次感叹,这个"唤醒风暴"设计得太赞了!!!

    更多相关内容
  • 独占锁是线程独占的,同一时刻只有一个线程能拥有独占锁,AQS里将这个线程放置到exclusiveOwnerThread成员上去。...当然,如果一个线程刚释放了锁,不管是独占锁还是共享锁,都需要唤醒在后面等待的线程。

    前言

    在前面两篇系列文章中,已经讲解了独占锁的获取和释放过程,而共享锁的获取与释放过程也很类似,如果你前面独占锁的内容都看懂了,那么共享锁你也就触类旁通了。

    JUC框架 系列文章目录

    共享锁与独占锁的区别

    共享锁与独占锁最大的区别在于,共享锁的函数名里面都带有一个Shared(抖个机灵,当然不是这个)。

    • 独占锁是线程独占的,同一时刻只有一个线程能拥有独占锁,AQS里将这个线程放置到exclusiveOwnerThread成员上去。
    • 共享锁是线程共享的,同一时刻能有多个线程拥有共享锁,但AQS里并没有用来存储获得共享锁的多个线程的成员。
    • 如果一个线程刚获取了共享锁,那么在其之后等待的线程也很有可能能够获取到锁。但独占锁不会这样做,因为锁是独占的。
    • 当然,如果一个线程刚释放了锁,不管是独占锁还是共享锁,都需要唤醒在后面等待的线程。

    让我们把共享锁与独占锁的函数名都列出来看一下:

    独占锁共享锁
    tryAcquire(int arg)tryAcquireShared(int arg)
    tryAcquireNanos(int arg, long nanosTimeout)tryAcquireSharedNanos(int arg, long nanosTimeout)
    acquire(int arg)acquireShared(int arg)
    acquireQueued(final Node node, int arg)doAcquireShared(int arg)
    acquireInterruptibly(int arg)acquireSharedInterruptibly(int arg)
    doAcquireInterruptibly(int arg)doAcquireSharedInterruptibly(int arg)
    doAcquireNanos(int arg, long nanosTimeout)doAcquireSharedNanos(int arg, long nanosTimeout)
    release(int arg)releaseShared(int arg)
    tryRelease(int arg)tryReleaseShared(int arg)
    -doReleaseShared()

    从上表可以看到,共享锁的函数是和独占锁是一一对应的,而且大部分只是函数名加了个Shared,从逻辑上看也是很相近的。

    doReleaseShared没有对应到独占锁的方法是因为它的逻辑是包含了unparkSuccessor,是建立在unparkSuccessor之上的,你可以简单地认为,doReleaseShared对应到独占锁的方法是unparkSuccessor。最主要的是,它们的使用时机不同:

    • 在独占锁中,释放锁时,会调用unparkSuccessor
    • 在共享锁中,获得锁和释放锁时,都会调用到doReleaseShared。不过获得共享锁时,是在一定条件下调用doReleaseShared

    观察Semaphore的内部类

    为了看到AQS的子类实现部分,我们从Semaphore看起。

        abstract static class Sync extends AbstractQueuedSynchronizer {
            Sync(int permits) {
                setState(permits);
            }
    
            final int nonfairTryAcquireShared(int acquires) {
                for (;;) {
                    int available = getState();
                    int remaining = available - acquires;
                    if (remaining < 0 ||
                        compareAndSetState(available, remaining))
                        return remaining;
                }
            }
    	}
    
        static final class NonfairSync extends Sync {
            protected int tryAcquireShared(int acquires) {
                return nonfairTryAcquireShared(acquires);
            }
        }
    
        static final class FairSync extends Sync {
            protected int tryAcquireShared(int acquires) {
                for (;;) {
                    if (hasQueuedPredecessors())
                        return -1;
                    int available = getState();
                    int remaining = available - acquires;
                    if (remaining < 0 ||
                        compareAndSetState(available, remaining))
                        return remaining;
                }
            }
        }
    
    • 首先看到Sync的构造器,看来参数permits是代表共享锁的数量。
    • 观察tryAcquireShared的公平和非公平锁的逻辑,发现区别只是 公平锁里面每次循环都会判断hasQueuedPredecessors()的返回值。

    这里先给大家讲一下tryAcquireShared
    参数acquires代表这次想要获得的共享锁的数量是多少。
    返回值则有三种情况:

    1. 如果返回值大于0,说明获取共享锁成功,并且后续获取也可能获取成功。
    2. 如果返回值等于0,说明获取共享锁成功,但后续获取可能不会成功。
    3. 如果返回值小于0,说明获取共享锁失败。

    直接看公平版本的tryAcquireShared,上面返回的地方:

    • hasQueuedPredecessors()如果返回了true,说明有线程排在了当前线程之前,现在公平版本又不能插队,所以结束返回-1,代表获取失败。
    • 如果remaining < 0成立,说明想要获取的共享锁数量已经超过了当前已有的数量,那么直接返回一个负数remaining,代表获取失败。
    • 如果remaining < 0不成立,说明想要获取的共享锁数量没有超过了当前已有的数量(等于0代表将会获取剩余所有的共享锁)。且接下来如果compareAndSetState(available, remaining)成功,那么返回一个>=0的数remaining,代表获取成功。

    接下来我们谈谈共享锁的tryAcquireShared和独占锁的tryAcquire的不同之处:

    • tryAcquire的返回值是boolean型,它只代表两种状态(获取成功或失败)。而tryAcquireShared的返回值是int型,如上有三种情况。
    • tryAcquireShared使用了自旋(死循环),但tryAcquire没有自旋。这将导致tryAcquire最多执行一次CAS操作修改同步器状态,但tryAcquireShared可能有多次。tryAcquireShared具体地讲,只要remaining>=0的(remaining < 0不成立),就一定会去尝试CAS设置同步器的状态。使用自旋的原因想必是,锁是共享的,既然还可能获取到(remaining>=0的),就一定要去尝试。
            protected final boolean tryReleaseShared(int releases) {
                for (;;) {
                    int current = getState();
                    int next = current + releases;
                    if (next < current) // overflow
                        throw new Error("Maximum permit count exceeded");
                    if (compareAndSetState(current, next))
                        return true;
                }
            }
    

    最后再看tryReleaseShared的实现,也用到了自旋操作,因为完全有可能多个线程同时释放共享锁,同时调用tryReleaseShared,所以需要用自旋保证 共享锁的释放最终能体现到同步器的状态上去。另外,除非int型溢出,那么此函数只可能返回true。

    共享锁的获取

    上面讲完了Semaphore的内部类,接下来我们就可以尽情地在AQS的源码里畅游了。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    

    acquireShared对应到独占锁的方法是acquire

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    

    咋一看感觉差别有点大,其实我们被迷惑了,后面我们会发现,之所以acquireShared里没有显式调用addWaiterselfInterrupt,是因为这两件事都被放到了doAcquireShared(arg)的逻辑里面了。

    接下来看看doAcquireShared方法的逻辑,它对应到独占锁是acquireQueued,除了上面提到的两件事,它们其实差别很少:

        private void doAcquireShared(int arg) {
            final Node node = addWaiter(Node.SHARED); //这件事放到里面来了
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head) {  //前驱是head时,才尝试获得共享锁
                        int r = tryAcquireShared(arg);
                        if (r >= 0) {  //获取共享锁成功时,才进行善后操作
                            setHeadAndPropagate(node, r);  //独占锁这里调用的是setHead
                            p.next = null; 
                            if (interrupted)
                                selfInterrupt(); //这件事也放到里面来了
                            failed = false;
                            return;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    acquireQueued在获得独占锁成功时,执行的是:

    if (p == head && tryAcquire(arg)) {  // tryAcquire返回true,代表获取独占锁成功
        setHead(node);
        p.next = null; 
        failed = false;
        return interrupted;
    }
    

    所以对比发现,共享锁的doAcquireShared有两处不同:

    1. 创建的节点不同。共享锁使用addWaiter(Node.SHARED),所以会创建出想要获取共享锁的节点。而独占锁使用addWaiter(Node.EXCLUSIVE)
    2. 获取锁成功后的善后操作不同。共享锁使用setHeadAndPropagate(node, r),因为刚获取共享锁成功后,后面的线程也有可能成功获取,所以需要在一定条件唤醒head后继。而独占锁使用setHead(node)
        private void setHeadAndPropagate(Node node, int propagate) {
            Node h = head; 
            setHead(node);
            if (propagate > 0 || h == null || h.waitStatus < 0 ||
                (h = head) == null || h.waitStatus < 0) {
                Node s = node.next;
                if (s == null || s.isShared())
                    doReleaseShared();
            }
        }
    
        private void setHead(Node node) {
            head = node;
            node.thread = null;
            node.prev = null;
        }
    

    setHead函数只是将刚成为将成为head的节点变成一个dummy node。而setHeadAndPropagate里也会调用setHead函数。但是它在一定条件下还可能会调用doReleaseShared,看来这就是单词Propagate的由来了,也就是我们一直说的“如果一个线程刚获取了共享锁,那么在其之后等待的线程也很有可能能够获取到锁”。

    doReleaseShared留到之后讲解,因为共享锁的释放也会用到它。

    关于setHeadAndPropagate的详解请看这篇setHeadAndPropagate源码分析,主要有两张图帮助大家理解setHeadAndPropagate里的这个超长的if判断。

    共享锁的释放

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    

    releaseShared对应到独占锁的方法是release

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    

    可见独占锁的逻辑比较简单,只是在head状态不为0时,就唤醒head后继。

    而共享锁的逻辑则直接调用了doReleaseShared,但在获取共享锁成功时,也可能会调用到doReleaseShared。也就是说,获取共享锁的线程(分为:已经获取到的线程 即执行setHeadAndPropagate中、等待获取中的线程 即阻塞在shouldParkAfterFailedAcquire里)和释放共享锁的线程 可能在同时执行这个doReleaseShared

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
    

    我们来仔细分析下这个函数的逻辑:

    • 逻辑是一个死循环,每次循环中重新读取一次head,然后保存在局部变量h中,再配合if(h == head) break;,这样,循环检测到head没有变化时就会退出循环。注意,head变化一定是因为:acquire thread被唤醒,之后它成功获取锁,然后setHead设置了新head。而且注意,只有通过if(h == head) break;即head不变才能退出循环,不然会执行多次循环。
    • if (h != null && h != tail)判断队列是否至少有两个node,如果队列从来没有初始化过(head为null),或者head就是tail,那么中间逻辑直接不走,直接判断head是否变化了。
    • 如果队列中有两个或以上个node,那么检查局部变量h的状态:
      • 如果状态为SIGNAL,说明h的后继是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)unparkSuccessor(h)绑定在了一起。说明了只要head成功得从SIGNAL修改为0,那么head的后继的代表线程肯定会被唤醒了。
      • 如果状态为0,说明h的后继所代表的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被 被唤醒线程(因为正常来讲,被唤醒线程的前驱,也就是head会被设置为0的,所以被唤醒线程发现head不为0,就会知道自己应该去唤醒自己的后继了) 检测到。
      • 如果状态为PROPAGATE,直接判断head是否变化。
    • 两个continue保证了进入那两个分支后,只有当CAS操作成功后,才可能去执行if(h == head) break;,才可能退出循环。
    • if(h == head) break;保证了,只要在某个循环的过程中有线程刚获取了锁且设置了新head,就会再次循环。目的当然是为了再次执行unparkSuccessor(h),即唤醒队列中第一个等待的线程。

    head状态为0的情况

    • 如果等待队列中只有一个dummy node(它的状态为0),那么head也是tail,且head的状态为0。
    • 等待队列中当前只有一个dummy node(它的状态为0),acquire thread获取锁失败了(无论独占还是共享),将当前线程包装成node放到队列中,此时队列中有两个node,但当前线程还没来得及执行一次shouldParkAfterFailedAcquire
    • 此时队列中有多个node,有线程刚释放了锁,刚执行了unparkSuccessor里的if (ws < 0) compareAndSetWaitStatus(node, ws, 0);把head的状态设置为了0,然后唤醒head后继线程,head后继线程获取锁成功,直到head后继线程将自己设置为AQS的新head的这段时间里,head的状态为0。
      • 具体地讲,如果是共享锁的话,一定是在调用unparkSuccessor之前就把head的状态变成0了,因为if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
      • 上面这种情况还可以继续延伸,在“唤醒head后继线程”后,head后继线程唤醒后第一次循环获取锁失败(你可能会疑问,上面的场景明明是刚有人释放了锁,为什么这里会失败,因为多线程环境下有可能被别的不公平获取方式插队了),调用shouldParkAfterFailedAcquire又将head设置回SIGNAL了,然后第二次循环开始之前(假设head后继线程此时分出去时间片),又有一个释放锁的线程在执行doReleaseShared里面的compareAndSetWaitStatus(h, Node.SIGNAL, 0)成功并且还unpark了处于唤醒状态的head后继线程,然后第二次循环开始(假设head后继线程此时得到时间片),获取锁成功。
        • 注意,如果unpark一个已经唤醒的线程,它的副作用是下一次park这个线程,线程不会阻塞。下下次park线程,才会阻塞。

    总结:

    • head状态为0的情况,属于一种中间状态。
    • 这种中间状态将变化为,head状态为SIGNAL,不管acquire thread接下来是获取锁成功还是失败。不过获取锁成功这种情况,需要考虑head后继(也就是包装acquire thread的那个node)不是队尾,如果是队尾,那么新head的状态也是为0的了。

    同时执行doReleaseShared

    这个函数的难点在于,很可能有多个线程同时在同时运行它。比如你创建了一个Semaphore(0),让N个线程执行acquire(),自然这多个线程都会阻塞在acquire()这里,然后你让另一个线程执行release(N)

    • 此时 释放共享锁的线程,肯定在执行doReleaseShared。
    • 由于 上面这个线程的unparkSuccessor,head后继的代表线程也会唤醒,进而执行doReleaseShared。
    • 重复第二步,获取共享锁的线程 又会唤醒 新head后继的代表线程。

    观察上面过程,有的线程 因为CAS操作失败,或head变化(主要是因为这个),会一直退不出循环。进而,可能会有多个线程都在运行该函数。doReleaseShared源码分析中的图解举例了一种循环继续的例子,当然,循环继续的情况有很多。

    总结

    • 共享锁与独占锁的最大不同,是共享锁可以同时被多个线程持有,虽然AQS里面没有成员用来保存持有共享锁的线程们。
    • 由于共享锁在获取锁和释放锁时,都需要唤醒head后继,所以将其逻辑抽取成一个doReleaseShared的逻辑了。
    展开全文
  • 详细介绍了AQS中的同步状态的共享式获取、释放的原理,以及独占锁和共享锁的简单实现。

    详细介绍了AQS中的同步状态的共享式获取、释放的原理,以及独占锁和共享锁的简单实现。

    AQS相关文章:

    AQS(AbstractQueuedSynchronizer)源码深度解析(1)—AQS的设计与总体结构

    AQS(AbstractQueuedSynchronizer)源码深度解析(2)—Lock接口以及自定义锁的实现

    AQS(AbstractQueuedSynchronizer)源码深度解析(3)—同步队列以及独占式获取锁、释放锁的原理【一万字】

    AQS(AbstractQueuedSynchronizer)源码深度解析(4)—共享式获取锁、释放锁的原理【一万字】

    AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】

    上篇文章中我们介绍了同步队列以及独占式获取锁、释放锁的原理,下面我们来看看共享式获取锁、释放锁的原理,以及如何实现一个更加高级的自定义的独占锁和共享锁。

    1 acquireShared共享式获取锁

    共享式获取与独占式获取的区别就是同一时刻是否可以多个线程同时获取到锁。

    在独占锁的实现中会使用一个exclusiveOwnerThread属性,用来记录当前持有锁的线程。当独占锁已经被某个线程持有时,其他线程只能等待它被释放后,才能去争锁,并且同一时刻只有一个线程能争锁成功。

    对于共享锁来说,如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,并且极有可能获取成功。基于共享式实现的组件有CountDownLatch、Semaphore等。

    通过调用AQS的acquireShared模版方法方法可以共享式地获取锁,同样该方法不响应中断。实际上如果看懂了独占式获取锁的源码,那么看共享式获取锁的源码就非常简单了。

    大概步骤如下:

    1. 首先使用tryAcquireShared尝试获取锁,获取成功(返回值大于等于0)则直接返回;
    2. 否则,调用doAcquireShared将当前线程封装为Node.SHARED模式的Node 结点后加入到AQS 同步队列的尾部,然后"自旋"尝试获取锁,如果还是获取不到,那么最终使用park方法挂起自己等待被唤醒。
    /**
     * 共享式获取锁的模版方法,不响应中断
     *
     * @param arg 参数
     */
    public final void acquireShared(int arg) {
        //尝试调用tryAcquireShared方法获取锁
        //获取成功(返回值大于等于0)则直接返回;
        if (tryAcquireShared(arg) < 0)
            //失败则调用doAcquireShared方法将当前线程封装为Node.SHARED类型的Node 结点后加入到AQS 同步队列的尾部,
            //然后"自旋"尝试获取同步状态,如果还是获取不到,那么最终使用park方法挂起自己。
            doAcquireShared(arg);
    }
    

    1.1 tryAcquireShared尝试获取共享锁

    熟悉的tryAcquireShared方法,这个方法我们在最开头讲“AQS的设计”时就提到过,该方法是AQS的子类即我们自己实现的,用于尝试获取共享锁,一般来说就是对state的改变、或者重入锁的检查等等,不同的锁有自己相应的逻辑判断,这里不多讲,后面讲具体锁的实现的时候(比如CountDownLatch)会讲到。

    返回int类型的值(比如返回剩余的state状态值-资源数量),一般的理解为:

    1. 如果返回值小于0,表示当前线程共享锁失败;
    2. 如果返回值大于0,表示当前线程共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功;
    3. 如果返回值等于0,表示当前线程共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败。
      1. 实际上在AQS的实际实现中,即使某时刻返回值等于0,接下来其他线程尝试获取共享锁的行为也可能会成功。即某线程获取锁并且返回值等于0之后,马上又有线程释放了锁,导致实际上可获取锁数量大于0,此时后继还是可以尝试获取锁的。

    在AQS的中tryAcquireShared的实现为抛出异常,因此需要子类重写:

    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    

    1.2 doAcquireShared自旋获取共享锁

    首次调用tryAcquireShared方法获取锁失败之后,会调用doAcquireShared方法。类似于独占式获取锁acquire方法中的addWaiter和acquireQueued方法的组合版本!

    大概步骤如下:

    1. 调用addWaiter方法,将当前线程封装为Node.SHARED模式的Node结点后加入到AQS 同步队列的尾部,即表示共享模式。
    2. 后面就是类似于acquireQueued方法的逻辑,结点自旋尝试获取共享锁。如果还是获取不到,那么最终使用park方法挂起自己等待被唤醒。

    每个结点可以尝试获取锁的要求是前驱结点是头结点,那么它本身就是整个队列中的第二个结点,每个获得锁的结点都一定是成为过头结点。那么如果某第二个结点因为不满足条件没有获取到共享锁而被挂起,那么即使后续结点满足条件也一定不能获取到共享锁。

    /**
     * 自旋尝试共享式获取锁,一段时间后可能会挂起
     * 和独占式获取的区别:
     * 1 以共享模式Node.SHARED添加结点
     * 2 获取到锁之后,修改当前的头结点,并将信息传播到后续的结点队列中
     *
     * @param arg 参数
     */
    private void doAcquireShared(int arg) {
        /*1 addWaiter方法逻辑,和独占式获取的区别1 :以共享模式Node.SHARED添加结点*/
        final Node node = addWaiter(Node.SHARED);
        /*2 下面就是类似于acquireQueued方法的逻辑
         * 区别在于获取到锁之后acquireQueued调用setHead方法,这里调用setHeadAndPropagate方法
         *  */
        //当前线程获取锁失败的标志
        boolean failed = true;
        try {
            //当前线程的中断标志
            boolean interrupted = false;
            for (; ; ) {
                //获取前驱结点
                final Node p = node.predecessor();
                /*当前驱结点是头结点的时候就会以共享的方式去尝试获取锁*/
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    /*返回值如果大于等于0,则表示获取到了锁*/
                    if (r >= 0) {
                        /*和独占式获取的区别2 :修改当前的头结点,根据传播状态判断是否要唤醒后继结点。*/
                        setHeadAndPropagate(node, r);
                        // 释放掉已经获取到锁的前驱结点
                        p.next = null;
                        /*检查设置中断标志*/
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                /*判断是否应该挂起,以及挂起的方法,和acquireQueued方法的逻辑完全一致,不会响应中断*/
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    

    从源码可以看出,和独占式获取的主要区别为:

    1. addWaiter以共享模式Node.SHARED添加结点。
    2. 获取到锁之后,调用setHeadAndPropagate设置行head结点,然后根据传播状态判断是否要唤醒后继结点。

    1.2.1 setHeadAndPropagat设置结点并传播信息

    在结点线程获取共享锁成功之后会调用setHeadAndPropagat方法,相比于setHead方法,在设置head之后多执行了一步propagate操作:

    1. 和setHead方法一样设置新head结点信息
    2. 根据传播状态判断是否要唤醒后继结点。

    1.2.1.1 doReleaseShared唤醒后继结点

    doReleaseShared用于在共享模式下唤醒后继结点。

    关于Node.PROPAGATE的分析,将在下面总结部分列出!

    /**
     * 共享式获取锁的核心方法,尝试唤醒一个后继线程,被唤醒的线程会尝试获取共享锁,如果成功之后,则又会有可能调用setHeadAndPropagate,将唤醒传播下去。
     * 独占锁只有在一个线程释放所之后才会唤醒下一个线程,而共享锁在一个线程在获取到锁和释放掉锁锁之后,都可能会调用这个方法唤醒下一个线程
     * 因为在共享锁模式下,锁可以被多个线程所共同持有,既然当前线程已经拿到共享锁了,那么就可以直接通知后继结点来获取锁,而不必等待锁被释放的时候再通知。
     */
    private void doReleaseShared() {
        /*一个死循环,跳出循环的条件就是最下面的break*/
        for (; ; ) {
            //获取当前的head,每次循环读取最新的head
            Node h = head;
            //如果h不为null且h不为tail,表示队列至少有两个结点,那么尝试唤醒head后继结点线程
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //如果头结点的状态为SIGNAL,那么表示后继结点需要被唤醒
                if (ws == Node.SIGNAL) {
                    //尝试CAS设置h的状态从Node.SIGNAL变成0
                    //可能存在多线程操作,但是只会有一条成功
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        //失败的线程结束本次循环,继续下一次循环
                        continue;            // loop to recheck cases
                    //成功的那一条线程会调用unparkSuccessor方法唤醒head的一个没有取消的后继结点
                    //对于一个head,只需要一条线程去唤醒该head的后继就行了。上面的CAS就是保证unparkSuccessor方法对于一个head只执行一次
                    unparkSuccessor(h);
                }
                /*
                 * 如果h状态为0,那说明后继结点线程已经是唤醒状态了或者将会被唤醒,不需要该线程来唤醒
                 * 那么尝试设置h状态从0变成PROPAGATE,如果失败则继续下一次循环,此时设置PROPAGATE状态能保证唤醒操作能够传播下去
                 * 因为后继结点成为头结点时,在setHeadAndPropagate方法中能够读取到原head结点的PROPAGATE状态<0,从而让它可以尝试唤醒后继结点(如果存在)
                 * */
                else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    //失败的线程结束本次循环,继续下一次循环
                    continue;                // loop on failed CAS
            }
            // 执行到这一步说明在上面的判断中队列可能只有一个结点,或者unparkSuccessor方法调用完毕,或h状态为PROPAGATE(不需要继续唤醒后继)
            // 再次检查h是否仍然是最新的head,如果不是的话需要再进行循环;如果是的话说明head没有变化,退出循环
            if (h == head)                   // loop if head changed
                break;
        }
    }
    

    2 reaseShared共享式释放锁

    共享锁的释放是通过调用releaseShared模版方法来实现的。大概步骤为:

    1. 调用tryReleaseShared尝试释放共享锁,这里必须实现为线程安全。
    2. 如果释放了锁,那么调用doReleaseShared方法唤醒后继结点,实现唤醒的传播。

    对于支持共享式的同步组件(即多个线程同时访问),它们和独占式的主要区别就是tryReleaseShared方法必须确保锁的释放是线程安全的(因为既然是多个线程能够访问,那么释放的时候也会是多个线程的,就需要保证释放时候的线程安全)。

    由于tryReleaseShared方法也是我们自己实现的,因此需要我们自己实现线程安全,所以常常采用CAS的方式来释放同步状态。

    /**
     * 共享模式下释放锁的模版方法。
     * ,如果成功释放则会调用
     */
    public final boolean releaseShared(int arg) {
        //tryReleaseShared释放锁资源,该方法由子类自己实现
        if (tryReleaseShared(arg)) {
            //释放成功,必定调用doReleaseShared尝试唤醒后继结点
            doReleaseShared(); 
            return true;
        }
        return false;
    }
    

    3 acquireSharedInterruptibly共享式可中断获取锁

    上面分析的独占式获取锁的方法acquireShared是不会响应中断的。但是AQS提供了另外一个acquireSharedInterruptibly模版方法,调用该方法的线程在等待获取锁时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。

    /**
     * 共享式可中断获取锁模版方法
     *
     * @param arg 参数
     * @throws InterruptedException 线程处于中断状态,抛出此异常
     */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取锁
        if (tryAcquireShared(arg) < 0)
            //获取不到就执行doAcquireSharedInterruptibly方法
            doAcquireSharedInterruptibly(arg);
    }
    

    3.1 doAcquireSharedInterruptibly共享式可中断获取锁

    该方法内部操作和doAcquireShared差不多,都是自旋获取共享锁,有些许区别,就是在后续挂起的线程因为线程被中断而返回时的处理方式不一样。

    共享式不可中断获取锁仅仅是记录该状态,interrupted = true,紧接着又继续循环获取锁;共享式可中断获取锁则直接抛出异常,因此会直接跳出循环去执行finally代码块。

    /**
     * 以共享可中断模式获取。
     *
     * @param arg 参数
     */
    private void doAcquireSharedInterruptibly(int arg)
            throws InterruptedException {
        /*内部操作和doAcquireShared差不多,都是自旋获取共享锁,有些许区别*/
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (; ; ) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    /*
                     * 这里就是区别所在,共享不可中断式方法doAcquireShared中
                     * 如果线程被中断,此处仅仅会记录该状态,interrupted = true,紧接着又继续循环获取锁
                     *
                     * 但是在该共享可中断式的锁获取方法中
                     * 如果线程被中断,此处直接抛出异常,因此会直接跳出循环去执行finally代码块
                     * */
                    throw new InterruptedException();
            }
        }
        /*获取到锁或者抛出异常都会执行finally代码块*/
        finally {
            /*如果获取锁失败。那么是发生异常的情况,可能就是线程被中断了,执行cancelAcquire方法取消该结点对锁的请求,该线程结束*/
            if (failed)
                cancelAcquire(node);
        }
    }
    

    4 tryAcquireSharedNanos共享式超时获取锁

    共享式超时获取锁tryAcquireSharedNanos模版方法可以被视作共享式响应中断获取锁acquireSharedInterruptibly方法的“增强版”,支持中断,支持超时时间!

    /**
     * 共享式超时获取锁,支持中断
     *
     * @param arg          参数
     * @param nanosTimeout 超时时间,纳秒
     * @return 是否获取锁成功
     * @throws InterruptedException 如果被中断,则抛出InterruptedException异常
     */
    
    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //下面是一个||运算进行短路连接的代码
        //tryAcquireShared尝试获取锁,获取到了直接返回true
        //获取不到(左边表达式为false) 就执行doAcquireSharedNanos方法
        return tryAcquireShared(arg) >= 0 ||
                doAcquireSharedNanos(arg, nanosTimeout);
    }
    

    4.1 doAcquireSharedNanos共享式超时获取锁

    doAcquireSharedNanos (int arg,long nanosTimeout)方法在支持响应中断的基础上, 增加了超时获取的特性。

    该方法在自旋过程中,当结点的前驱结点为头结点时尝试获取锁,如果获取成功则从该方法返回,这个过程和共享式式同步获取的过程类似,但是在锁获取失败的处理上有所不同。

    如果当前线程获取锁失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。

    如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。

    因此,在超时非常短的场景下,AQS会进入无条件的快速自旋而不是挂起线程。

    static final long spinForTimeoutThreshold = 1000L;
    
    /**
     * 以共享超时模式获取。
     *
     * @param arg          参数
     * @param nanosTimeout 剩余超时时间,纳秒
     * @return true 成功 ;false 失败
     * @throws InterruptedException 如果被中断,则抛出InterruptedException异常
     */
    private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //剩余超时时间小于等于0的,直接返回
        if (nanosTimeout <= 0L)
            return false;
        //能够等待获取的最后纳秒时间
        final long deadline = System.nanoTime() + nanosTimeout;
        //同样调用addWaiter将当前线程构造成结点加入到同步队列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            /*和共享式式不可中断方法doAcquireShared一样,自旋获取锁*/
            for (; ; ) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return true;
                    }
                }
                /*这里就是区别所在*/
                //如果新的剩余超时时间小于0,则退出循环,返回false,表示没获取到锁
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                //如果需要挂起 并且 剩余nanosTimeout大于spinForTimeoutThreshold,即大于1000纳秒
                if (shouldParkAfterFailedAcquire(p, node) &&
                        nanosTimeout > spinForTimeoutThreshold)
                    //那么调用LockSupport.parkNanos方法将当前线程挂起nanosTimeout
                    LockSupport.parkNanos(this, nanosTimeout);
                //如果线程被中断了,那么直接抛出异常
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        }
        /*获取到锁、超时时间到了、抛出异常都会执行finally代码块*/
        finally {
            /*如果获取锁失败。可能就是线程被中断了,那么执行cancelAcquire方法取消该结点对锁的请求,该线程结束
             * 或者是超时时间到了,那么执行cancelAcquire方法取消该结点对锁的请求,将返回false
             * */
            if (failed)
                cancelAcquire(node);
        }
    }
    

    5 共享式获取/释放锁总结

    我们可以调用acquireShared 模版方法来获取不可中断的共享锁,可以调用acquireSharedInterruptibly模版方法来可中断的获取共享锁,可以调用tryAcquireSharedNanos模版方法来可中断可超时的获取共享锁,在此之前需要重写tryAcquireShared方法;还可以调用releaseShared模版方法来释放共享锁,在此之前需要重写tryReleaseShared方法。

    对于共享锁来说,由于锁是可以多个线程同时获取的。那么如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,并且极有可能获取成功。因此在一个结点线程释放共享锁成功时,必定调用doReleaseShared尝试唤醒后继结点,而在一个结点线程获取共享锁成功时,也可能会调用doReleaseShared尝试唤醒后继结点。

    基于共享式实现的组件有CountDownLatch、Semaphore、ReentrantReadWriteLock等。

    5.1. Node.PROPAGATE简析

    5.1.1 出现时机

    doReleaseShared方法在线程获取共享锁成功之后可能执行,在线程释放共享锁成功之后必定执行。

    在doReleaseShared方法中,可能会存在将线程状态设置为Node.PROPAGATE的情况,然而,整个AQS类中也只有这一处直接涉及到Node.PROPAGATE状态,并且仅仅是设置,在其他地方却再也没见到对该状态的直接使用。由于该状态值为-3,因此可能是在其他方法中对waitStatus大小范围的判断的时候将这种情况包括进去了(猜测)!

    关于Node.PROPAGATE的直接代码如下:

    else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;                // loop on failed CAS
    

    首先是需要进入到else if分支,然后需要此时ws(源码中最开始获取的head结点的引用的状态—不一定是最新的)状态为0,然后尝试CAS设置该结点的状态为Node.PROPAGATE,并且可能失败,失败之后直接continue继续下一次循环。

    对于这个Node.PROPAGATE状态的作用,众说纷纭,笔者看了很多文章,很多看起来都有道理,但是仔细想想又有些差错,在此,笔者不做过多个人分析,首先来看看进入else if分支并且ws为0的情况有哪些!

    初始情况

    假设某个共享锁的实现允许最多三个线程持有锁,此时有线程A、B、C均获取到了锁,同步队列中还有一个被挂起的结点线程D在等待锁的释放,此时队列结构如下:

    在这里插入图片描述

    如果此时线程A释放了锁,那么A将会调用doReleaseShared方法,但是明显A将会进入if代码块中,将head的状态改为0,同时调用unparkSuccessor唤醒一个后继线程,这里明显是D。

    此时同步队列结构为:

    在这里插入图片描述

    情形1

    如果此时线程B、C都释放了锁,那么B、C都将会调用doReleaseShared方法,假设它们执行速度差不多,那么它们都将会进入到else if中,因为此时head的状态变成了0,然后它们都会调用CAS将0改成Node.PROPAGATE,此时只会有一条线程成功,另一条会失败。

    这就是 释放锁时,进入到else if的一种情况。即多个释放锁的结点操作同一个head,那么最终只有一个结点能够在if中成功调用unparkSuccessor唤醒后继,另外的结点都将失败并最终都会走到else if中去。同理,获取锁时也可能由于上面的原因而进入到else if。

    情形2

    如果此时又来了一个新结点E,由于同样没有获取到锁那么会调用addWaiter添加到D结点后面成为新tail结点。

    然后结点E会在shouldParkAfterFailedAcquire方法中尝试将没取消的前驱D的waitStatus修改为Node.SIGNAL,然后挂起。

    那么在新结点E执行addWaiter之后,执行shouldParkAfterFailedAcquire之前,此时同步队列结构为:

    在这里插入图片描述

    由于A释放了锁,那么线程D会被唤醒,并调用tryAcquireShared获取了锁,那么将会返回0(常见的共享锁获取锁的实现是使用state减去需要获取的资源数量,这里A释放了一把锁,D又获取一把锁,此时剩余资源—锁数量剩余0)。

    此时,如果B再释放锁,这就出现了“即使某时刻返回值等于0,接下来其他线程尝试获取共享锁的行为也可能会成功”的情况。即 某线程获取共享锁并且返回值等于0之后,马上又有其他持有锁的线程释放了锁,导致实际上可获取锁数量大于0,此时后继还是可以尝试获取锁的。

    上面的是题外话,我们回到正文。如果此时B释放了锁,那么肯定还是会走doReleaseShared方法,由于在初始情形中,head的状态已经被A修改为0,此时B还是会走else if ,将状态改为Node.PROPAGATE。

    在这里插入图片描述

    我们回到线程D,此时线程D获取锁之后会走到setHeadAndPropagate方法中,在进行sheHead方法调用之后,此时结构如下(假设线程E由于资源分配的原因,在此期间效率低下,还没有将前驱D的状态改为-1,或者由于单核CPU线程切换导致线程E一直没有分配到时间片):

    在这里插入图片描述

    sheHead之后,就会判断是否需要调用doReleaseShared方法唤醒后继线程,这里的判断条件是:

    propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || 
    h.waitStatus < 0
    

    根据结构,只有第三个条件h.waitStatus<0满足,此时线程D就可以调用doReleaseShared唤醒后继结点,在这个过程中,关键的就是线程B将老head的状态设置为Node.PROPAGATE,即-2,小于0,此时可以将唤醒传播下去,否则被唤醒的线程A将因为不满足条件而不会调用doReleaseShared方法!

    或许这就是所谓的Node.PROPAGATE可能将唤醒传播下去的考虑到的情况之一?

    而在此时获取锁的线程D调用doReleaseShared方法时,由于此时head状态本来就是0,因此直接进入else if将状态改为Node.PROPAGATE,表示此时后继结点不需要唤醒,但是需要将唤醒操作继续传播下去。

    这也是在获取锁时,在doReleaseShared方法中第一次出现某结点作为head就直接进入到else if的一种情况。

    情形3

    由于A释放了锁,那么如果D的获取了锁,并且方法执行完毕,那么此时同步队列结构如下:

    在这里插入图片描述

    此时又来了一个新结点E,由于同样没有获取到锁那么会调用addWaiter添加到head结点后面成为新tail结点。

    然后结点E会在shouldParkAfterFailedAcquire方法中尝试将没取消的前驱head的waitStatus修改为Node.SIGNAL,然后挂起。

    那么在新结点E执行addWaiter之后,执行shouldParkAfterFailedAcquire之前,此时同步队列结构为:

    在这里插入图片描述

    此时线程A尝试释放锁,释放锁成功后一定会都调用doReleaseShared方法时,由于此时head状态本来就是0,因此直接进入else if将状态改为Node.PROPAGATE,表示此时后继结点不需要唤醒,但是需要将唤醒操作继续传播下去。

    这也是在释放锁的时候,在doReleaseShared方法中第一次出现某结点作为head就直接进入到else if的一种情况。

    5.1.2 总结

    下面总结了会走到else if的几种情况,可能还有更多情形这里分有分析出来:

    1. 多线程并发的在doReleaseShared方法中操作同一个head,并且这段时间head没发生改变。那么先进来的一条线程能够将if执行成功,即将状态置为0,然后调用unparkSuccessor唤醒后,后续进来的线程由于状态为0,那么只能执行else if。这种情况对于获取锁或者释放锁的doReleaseShared方法都可能存在!这种情况发生时,在doReleaseShared方法中第一次出现某结点作为head时,不会进入else if,一定是后续其他线程以同样的结点作为头结点时,才会进入else if!
    2. 对于获取锁的doReleaseShared方法,有一种在doReleaseShared方法中第一次出现某结点作为head就直接进入到else if的一种情况。设结点D作为原队列的尾结点,状态值为0,然后又来了新结点E,在新结点E的线程调用addWaiter之后(加入队列成为新tail),shouldParkAfterFailedAcquire之前(没来得及修改前驱D的状态为-1)的这段特殊时间范围之内,此时结点D的线程获取到了锁成为新头结点,并且原头结点状态值小于0,那么就会出现 在获取锁时调用doReleaseShared并直接进入else if的情况,这种情况的要求极为苛刻。或许本就不存在,只是本人哪里的分析出问题了?
    3. 对于释放锁的doReleaseShared方法,有一种在doReleaseShared方法中第一次出现结点某作为head就直接进入到else if的一种情况。设结点D作为原队列的尾结点,此时状态值为0,并且已经获取到了锁;然后又来了新结点E,在新结点E的线程调用addWaiter之后(加入队列成为新tail),shouldParkAfterFailedAcquire之前(没来得及修改前驱D的状态为-1)的这段特殊时间范围之内,此时结点D的线程释放了锁,那么就会出现 在释放锁时调用doReleaseShared并直接进入else if的情况,这种情况的要求极为苛刻。或许本就不存在,只是本人哪里的分析出问题了?

    那么根据上面的情况来看,就算没有else if这个判断或者如果没有Node.PROPAGATE这个状态的设置,最终对于后续结点的唤醒并没有什么大的问题,也并不会导致队列失活。

    加上Node.PROPAGATE这个状态的设置,导致的直接结果是可能会增加doReleaseShared方法调用的次数,但是也会增加无效、无意义唤醒的次数。 在setHeadAndPropagate方法中,判断是否需要唤醒后继的源码注释中我们能找到这样的描述:

    The conservatism in both of these checks may cause unnecessary wake-ups, but only when there are multiple racing acquires/releases, so most need signals now or soon anyway.

    意思就是,这些判断可能会造成无意义的唤醒,但如果doReleaseShared方法调用的次数比较多的话,相当于多线程争抢着去唤醒后继线程,或许可以提升锁的获取速度?或者这里的代码只是一种更加通用的保证正确的做法?实际上AQS中还有许多这样可能会造成无意义调用的代码!

    6 锁的简单实现

    6.1 可重入独占锁的实现

    在最开始我们实现了简单的不可重入独占锁,现在我们尝试实现可重入的独占锁,实际上也比较简单!

    AQS 的state 状态值表示线程获取该锁的重入次数, 在默认情况下,state的值为0 表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state 的值为l ,如果CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是重入次数为2。在该线程释放该锁时,会尝试使用CAS 让状态值减1,如果减l 后状态值为0,则当前线程释放该锁。

    对于可重入独占锁,获取了几次锁就需要释放几次锁,否则由于锁释放不完全而阻塞其他线程!

    /**
     * @author lx
     */
    public class ReentrantExclusiveLock implements Lock {
        /**
         * 将AQS的实现组合到锁的实现内部
         */
        private static class Sync extends AbstractQueuedSynchronizer {
            /**
             * 重写isHeldExclusively方法
             *
             * @return 是否处于锁占用状态
             */
            @Override
            protected boolean isHeldExclusively() {
                //state是否等于1
                return getState() == 1;
            }
    
            /**
             * 重写tryAcquire方法,可重入的尝试获取锁
             *
             * @param acquires 参数,这里我们没用到
             * @return 获取成功返回true,失败返回false
             */
            @Override
            public boolean tryAcquire(int acquires) {
                /*尝试获取锁*/
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                /*获取失败,判断当前获取锁的线程是不是本线程*/
                else if (getExclusiveOwnerThread() == Thread.currentThread()) {
                    //如果是,那么state+1,表示锁重入了
                    setState(getState() + 1);
                    return true;
                }
                return false;
            }
    
            /**
             * 重写tryRelease方法,可重入的尝试释放锁
             *
             * @param releases 参数,这里我们没用到
             * @return 释放成功返回true,失败返回false
             */
            @Override
            protected boolean tryRelease(int releases) {
                //如果尝试解锁的线程不是加锁的线程,那么抛出异常
                if (Thread.currentThread() != getExclusiveOwnerThread()) {
                    throw new IllegalMonitorStateException();
                }
                boolean flag = false;
                int oldState = getState();
                int newState = oldState - 1;
                //如果state变成0,设置当前拥有独占访问权限的线程为null,返回true
                if (newState == 0) {
                    setExclusiveOwnerThread(null);
                    flag = true;
                }
                //重入锁的释放,释放一次state减去1
                setState(newState);
                return flag;
            }
    
            /**
             * 返回一个Condition,每个condition都包含了一个condition队列
             * 用于实现线程在指定条件队列上的主动等待和唤醒
             *
             * @return 每次调用返回一个新的ConditionObject
             */
            Condition newCondition() {
                return new ConditionObject();
            }
        }
    
        /**
         * 仅需要将操作代理到Sync实例上即可
         */
        private final Sync sync = new Sync();
    
        /**
         * lock接口的lock方法
         */
        @Override
        public void lock() {
            sync.acquire(1);
        }
    
        /**
         * lock接口的tryLock方法
         */
        @Override
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
    
        @Override
        public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
    
        /**
         * lock接口的unlock方法
         */
        @Override
        public void unlock() {
            sync.release(1);
        }
    
        /**
         * lock接口的newCondition方法
         */
        @Override
        public Condition newCondition() {
            return sync.newCondition();
        }
    
        public boolean isLocked() {
            return sync.isHeldExclusively();
        }
    
        public boolean hasQueuedThreads() {
            return sync.hasQueuedThreads();
        }
    }
    

    6.1.1 测试

    /**
     * @author lx
     */
    public class ReentrantExclusiveLockTest {
        /**
         * 创建锁
         */
        static ReentrantExclusiveLock reentrantExclusiveLock = new ReentrantExclusiveLock();
        /**
         * 自增变量
         */
        static int i;
    
        public static void main(String[] args) throws InterruptedException {
            //三条线程
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.MINUTES,
                    new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
            Runa runa = new Runa();
            for (int i1 = 0; i1 < 3; i1++) {
                threadPoolExecutor.execute(runa);
            }
            threadPoolExecutor.shutdown();
            while (!threadPoolExecutor.isTerminated()) {
            }
            //三条线程执行完毕,输出最终结果
            System.out.println(i);
    
        }
    
    
        /**
         * 线程任务,循环50000次,每次i自增1
         */
        public static class Runa implements Runnable {
            @Override
            public void run() {
                // lock与unlock注释时,可能会得到错误的结果
                // 开启时每次都会得到正确的结果150000
                //支持多次获取锁(重入)
                reentrantExclusiveLock.lock();
                reentrantExclusiveLock.lock();
                for (int i1 = 0; i1 < 50000; i1++) {
                    i++;
                }
                //获取了多少次必须释放多少次
                reentrantExclusiveLock.unlock();
                reentrantExclusiveLock.unlock();
            }
        }
    }
    

    6.2 可重入共享锁的实现

    自定义一个共享锁,共享锁的数量可以自己指定。默认构造情况下,在同一时刻,最多允许三条线程同时获取锁,超过三个线程的访问将被阻塞。

    我们必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法。由于是共享式的获取,那么在对同步状态state更新时,两个方法中都需要使用CAS方法compareAndSet(int expect,int update)做原子性保障。

    假设一条线程一次只需要获取一个资源即表示获取到锁。由于同一时刻允许至多三个线程的同时访问,表明同步资源数为3,这样可以设置初始状态state为3来代表同步资源,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0、1和2,其中0表示当前已经有两个线程获取了同步资源,此时再有其他线程对同步状态进行获取,该线程可能会被阻塞。

    最后,将自定义的AQS实现通过内部类的方法聚合到自定义锁中,自定义锁还需要实现Lock接口,外部方法的内部实现直接调用对应的模版方法即可。

    这里一条线程可以获取多次共享锁,但是同时必须释放多次共享锁,否则可能由于锁资源的减少,导致效率低下甚至死锁(可以使用tryLock避免)!

    public class ShareLock implements Lock {
        /**
         * 默认构造器,默认共享资源3个
         */
        public ShareLock() {
            sync = new Sync(3);
        }
    
        /**
         * 指定资源数量的构造器
         */
        public ShareLock(int num) {
            sync = new Sync(num);
        }
    
        private static class Sync extends AbstractQueuedSynchronizer {
            Sync(int num) {
                if (num <= 0) {
                    throw new RuntimeException("锁资源数量需要大于0");
                }
                setState(num);
            }
    
            /**
             * 重写tryAcquireShared获取共享锁
             */
            @Override
            protected int tryAcquireShared(int arg) {
                /*一般的思想*/
                /*//获取此时state
                int currentState = getState();
                //获取剩余state
                int newState = currentState - arg;
                //如果剩余state小于0则直接返回负数
                //否则尝试更新state,更新成功就说明获取成功,返回大于等于0的数
                return newState < 0 ? newState : compareAndSetState(currentState, newState) ? newState : -1;*/
    
                /*更好的思想
                 * 在上面的实现中,如果剩余state值大于0,那么只尝试CAS一次,如果失败就算没有获取到锁,此时该线程会进入同步队列
                 * 在下面的实现中,如果剩余state值大于0,那么如果尝试CAS更新不成功,会在for循环中重试,直到剩余state值小于0或者更新成功
                 *
                 * 两种方法的不同之处在于,对CAS操作是否进行重试,这里建议第二种
                 * 因为可能会有多个线程同时获取多把锁,但是由于CAS只能保证一次只有一个线程成功,因此其他线程必定失败
                 * 但此时,实际上还是存在剩余的锁没有被获取完毕的,因此让其他线程重试,相比于直接加入到同步队列中,对于锁的利用率更高!
                 * */
                for (; ; ) {
                    int currentState = getState();
                    int newState = currentState - arg;
                    if (newState < 0 || compareAndSetState(currentState, newState)) {
                        return newState;
                    }
                }
    
            }
    
            /**
             * 重写tryReleaseShared释放共享锁
             *
             * @param arg 参数
             * @return 成功返回true 失败返回false
             */
            @Override
            protected boolean tryReleaseShared(int arg) {
                //只能成功
                for (; ; ) {
                    int currentState = getState();
                    int newState = currentState + arg;
                    if (compareAndSetState(currentState, newState)) {
                        return true;
                    }
                }
            }
        }
    
        /**
         * 内部初始化一个sync对象,此后仅需要将操作代理到这个Sync对象上即可
         */
        private final Sync sync;
    
        /*下面都是调用模版方法*/
        
        @Override
        public void lock() {
            sync.acquireShared(1);
        }
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
    
        @Override
        public boolean tryLock() {
            return sync.tryAcquireShared(1) >= 0;
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
        }
    
        @Override
        public void unlock() {
            sync.releaseShared(1);
        }
    
    
        /**
         * @return 没有实现自定义Condition,单纯依靠原始Condition实现是不支持共享锁的
         */
        @Override
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }
    

    6.2.1 测试

    public class ShareLockTest {
    
        static final ShareLock lock = new ShareLock();
    
        public static void main(String[] args) {
            /*启动10个线程*/
            for (int i = 0; i < 10; i++) {
                Worker w = new Worker();
                w.setDaemon(true);
                w.start();
            }
            ShareLockTest.sleep(20);
        }
    
        /**
         * 睡眠
         *
         * @param seconds 时间,秒
         */
        public static void sleep(long seconds) {
            try {
                TimeUnit.SECONDS.sleep(seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        static class Worker extends Thread {
            @Override
            public void run() {
                /*不停的获取锁,释放锁
                 * 最开始获取了几个锁,那么最后必须释放几个锁
                 * 否则可能由于锁资源的减少,导致效率低下甚至死锁(可以使用tryLock避免)!
                 * */
                while (true) {
                    /*tryLock测试*/
                    if (lock.tryLock()) {
                        System.out.println(Thread.currentThread().getName());
                        /*获得锁之后都会休眠2秒
                        那么可以想象,控制台将很有可能会出现连续三个一起输出,然后等待2秒,再连续三个一起输出,然后2秒……*/
                        ShareLockTest.sleep(2);
                        lock.unlock();
                    }
    
                    /*lock测试,或许总会出现固定线程获取锁,因为AQS默认是实现是非公平锁*/
                    /*lock.lock();
                    System.out.println(Thread.currentThread().getName());
                    ShareLockTest.sleep(2);
                    lock.unlock();*/
                }
            }
        }
    }
    

    如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

    展开全文
  • 分布式是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干...

    分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

    如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁了。

    在平时的实际项目开发中,我们往往很少会去在意分布式锁,而是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥。这确实是一种非常简便且被广泛使用的分布式锁实现方式。

    然而有一个不争的事实是,目前绝大多数大型分布式系统的性能瓶颈都集中在数据库操作上。因此,如果上层业务再给数据库添加一些额外的锁,例如行锁、表锁甚至是繁重的事务处理,那么就会让数据库更加不堪重负。

    下面我们来看看使用ZooKeeper如何实现分布式锁,这里主要讲解排他锁和共享锁两类分布式锁。

    排他锁

    排他锁(Exclusive Locks,简称 X 锁),又称为写锁或独占锁,是一种基本的锁类型。如果事务 T1对数据对象 O1加上了排他锁,那么在整个加锁期间,只允许事务 T1对 O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁。

    从上面讲解的排他锁的基本概念中,我们可以看到,排他锁的核心是如何保证当前有且仅有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到。

    下面我们就来看看如何借助ZooKeeper实现排他锁:

    ① 定义锁

    在通常的Java开发编程中,有两种常见的方式可以用来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。然而,在ZooKeeper中,没有类似于这样的API可以直接使用,而是通过 ZooKeeper 上的数据节点来表示一个锁,例如/exclusive_lock/lock节点就可以被定义为一个锁,如图:

    ② 获取锁

    在需要获取排他锁时,所有的客户端都会试图通过调用 create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。在前面,我们也介绍了,ZooKeeper 会保证在所有的客户端中,最终只有一个客户端能够创建成功,那么就可以认为该客户端获取了锁。

    同时,所有没有获取到锁的客户端就需要到/exclusive_lock 节点上注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。

    ③释放锁

    在“定义锁”部分,我们已经提到,/exclusive_lock/lock 是一个临时节点,因此在以下两种情况下,都有可能释放锁。· 当前获取锁的客户端机器发生宕机,那么ZooKeeper上的这个临时节点就会被移除。· 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除。无论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。整个排他锁的获取和释放流程。

    如下图:


    共享锁

    共享锁(Shared Locks,简称S锁),又称为读锁,同样是一种基本的锁类型。

    如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。

    共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对一个事务可见,而加上共享锁后,数据对所有事务都可见。

    下面我们就来看看如何借助ZooKeeper来实现共享锁:

    ① 定义锁
    和排他锁一样,同样是通过 ZooKeeper 上的数据节点来表示一个锁,是一个类似于“/shared_lock/[Hostname]-请求类型-序号”的临时顺序节点,例如/shared_lock/host1-R-0000000001,那么,这个节点就代表了一个共享锁。

    如图所示:

    ② 获取锁

    在需要获取共享锁时,所有客户端都会到/shared_lock 这个节点下面创建一个临时顺序节点,如果当前是读请求,那么就创建例如/shared_lock/host1-R-0000000001的节点;如果是写请求,那么就创建例如/shared_lock/host2-W-0000000002的节点。

    判断读写顺序

    通过Zookeeper来确定分布式读写顺序,大致分为四步

    (1)创建完节点后,获取/shared_lock节点下所有子节点,并对该节点变更注册监听。

    (2)确定自己的节点序号在所有子节点中的顺序。

    (3)对于读请求:若没有比自己序号小的子节点或所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取到共享锁,同时开始执行读取逻辑,若有写请求,则需要等待。对于写请求:若自己不是序号最小的子节点,那么需要等待。

    (4)接收到Watcher通知后,重复步骤1

    ③ 释放锁,其释放锁的流程与独占锁一致。

    觉得有用的小伙伴可以分享出去

    给更多需要的人看到哦~

    END

    黑马程序员13个热门学科

    火热开班中

    598元 基础班课程

    免费领学费抵扣券

    现只需28元立刻学习

    立即扫码咨询

    展开全文
  • MySQL的共享锁、排他锁、意向锁

    千次阅读 2022-04-10 10:32:01
    目录一、共享锁(Shared Lock)二、排他锁(EXclusive Lock)三、意向锁(Intention Lock) MySQL锁的模式有三种:共享锁、排他锁、意向锁(意向共享锁、意向排他锁)。 一、共享锁(Shared Lock) 共享锁(Shared ...
  • 共享锁 什么共享锁? 就是指同一时间可以被多个线程占有,像java自带的ReadWriteLock、Semaphore,他们可以设置自己共享的数量。 举个栗子:共享锁相当于一个大型公共澡堂,一开始就设定了能进去洗澡人的数量,...
  • 独占锁与共享锁

    千次阅读 多人点赞 2019-08-09 19:38:11
    独占锁与共享锁前言概念引入独占锁概念共享锁概念源码分析ReentrantReadWriteLock源码读锁和写锁的具体加锁方式有什么区别 前言 独占锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和...
  • 想要学好 mysql 数据库,机制的理解才是打开通往更深层次大门的钥匙
  • Mysql共享锁和排他锁

    万次阅读 多人点赞 2019-05-27 15:10:41
    不知道图片能不能正常显示 mysql锁机制分为表级锁和行级锁,...共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。 排他锁又称为写锁...
  • 共享锁和排它锁

    千次阅读 2020-06-04 23:42:40
    什么共享锁和排它锁 排它锁,又称独占锁,独享锁 synchronized就是一个排它锁 共享锁,又称为读锁,获得共享锁后,可以查看,但无法删除和修改数 据, 其他线程此时业获取到共享锁,也可以查看但是 无法 ...
  • 由于等待一个锁定线程只有在获得这把之后,才能恢复运行,所以让持有的线程在不需要时候及时释放锁是很重要的。在以下情况下,持有的线程会释放锁: 1. 执行完同步代码块。 2. 在执行同步代码块的过程中...
  • 一、InnoDB表级锁 二、意向共享锁和意向排他锁 三、死锁 1. 数据库中的死锁 2. 死锁场景以及解决办法 3. 操作 四、锁的优化建议
  • 什么要引入多个用户同时对数据库的并发操作时会带来以下数据不一致的问题:丢失更新A,B两个用户读同一数据并进行修改,其中一个用户的修改结果破坏了另一个修改的结果,比如订票系统脏读A用户修改了数据,随后B用户...
  • MySQL数据库的共享锁和排他锁

    千次阅读 2021-10-25 17:30:40
    其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。 如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读...
  • 一、共享锁(S锁)又称读锁, 若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A。 比如SELECT语句。 其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S 锁。 这保证了其他事务可以读A,但在事务T释放A...
  • 当T1为数据对象A加上共享锁后,可以对A进行读操作,但不能进行写操作,并且T2可以再次对A加共享锁,大家都可以正常地读A,但是在A上的共享锁释放之前,任何事务不可以对A进行写操作。 例1: T1:select * from tab.....
  • Redis实现分布式共享锁

    千次阅读 2018-03-02 21:41:12
    走第一种情况的问题是,正常的加锁逻辑应该走SETNX,而现在当被解除后,走的是GETST,如果判断条件不当,就会引起死锁,很悲催,我在做的时候就碰到了,具体怎么碰到的看下面的问题 GETSET返回nil时应该怎么...
  • 重要声明:本人之前对java中的读写锁也不是非常了解,用的也不是很多,尤其在读写锁的策略原理一块没有深究过,本篇文章是在学习【玩...在【ReentrantLock锁详解】一文中讲到了java中锁的划分,本篇主要讲述共享锁和...
  • 1. ReentrantReadWriteLock中PROPAGATE只是一个中间状态,共享锁的传播性由setHeadAndPropagate完成。 2. 对于有资源概念的Semaphore,PROPAGATE和setHeadAndPropagate组合完成共享锁的传播性。 3. 共享锁的传播性...
  • 注意,共享锁的特点是不一定要显式释放,一个会话A加了共享锁,会话B也可以加共享锁,那么共享锁就转移到了会话B,相当于会话A的共享锁已经释放。锁是只有一把的。 2、排他锁 加上排他锁,其他会话不能再加排他锁,...
  • mysql共享锁和排它锁

    千次阅读 2019-04-09 14:43:14
    |--共享锁(S锁,MyISAM 叫做读锁) |--排他锁(X锁,MyISAM 叫做写锁) |--悲观锁(抽象性,不真实存在这个锁) |--乐观锁(抽象性,不真实存在这个锁)   二、InnoDB与MyISAM Mysql 在5.5之前默认使用 MyISAM...
  • 非公平锁-羊群效应 公平锁 共享锁
  • Semaphore共享锁的使用 信号量(Semaphore),又被称为信号灯,在多线程环境下用于协调各个线程, 以保证它们能够正确、合理的使用公共资源。信号量维护了一个许可集,我们在初始化Semaphore时需要为这个许可集传入一个...
  • AQS共享锁的实现原理

    千次阅读 2017-10-20 00:44:45
    一、AQS共享锁的实现原理前面的文章Lock的实现中分析了AQS独占锁的实现原理,那么接下来就分析下AQS是如何实现共享锁的。共享锁的介绍共享锁:同一时刻有多个线程能够获取到同步状态。那么它是如何做到让多个线程...
  • Oracle的共享锁和排它锁

    千次阅读 2018-08-28 15:00:02
    oracle有两种模式的锁:排他锁(exclusive lock,即X锁)和共享锁(share lock,即S锁)。 共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能...
  • 数据库-mysql-什么时候锁

    千次阅读 2021-01-19 05:24:32
    说在前面:基于innodb讨论1.insert时全表锁,update是行级锁(非绝对-成功使用...InnoDB支持行级锁(分为共享锁与排它锁),可以使用表锁;当前读(除显式in share外,包括update都是拍他锁)4)innodb一般情况下走索引或...
  • mysql中的共享锁与排他锁

    千次阅读 2017-02-16 17:08:27
    原文出处:MySQL中的共享锁与排他锁 在 MySQL中的行级锁,表级锁,页级锁中介绍过,行级锁是Mysql中锁定粒度最细的一种锁,行级锁能大大减少数据库操作的冲突。行级锁分为共享锁和排他锁两种,本文将详细介绍...
  • AQS共享锁与独占锁对比

    千次阅读 2020-03-05 23:23:28
    总的来说AQS框架分为五层从对外暴露的API层到底层的数据层,当自定义同步器时候,只需要实现API的方法即可,不需要关注底层的具体实现逻辑。 共享模式和独占模式流程图对比 独占模式流程图 共享模式流程图 ...
  • 1.共享锁(S)(读锁):一旦事务给数据或记录加上了共享锁(S),则只能读数据不能写数据,其他事务也只能加共享锁(S)知道上一个事务释放了锁 2.排它锁(X)(写锁):一旦被上上排它锁(X),该事务可进行读写...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 235,007
精华内容 94,002
关键字:

共享锁什么时候释放