java多线程:5.4 锁-Lock

java lock包

由于Synchronized存在不少缺陷,因此jdk1.5之后,提供了Locks包,有ReentrantLock(可重入锁),ReadWriteLock(读写锁,可以获取读锁和写锁)。

简单示例

public class ReentrantLockkTest {
	public static void main(String[] args) {
		final ResourceReentrantLock  res = new ResourceReentrantLock();
		//取数据
		for(int i=0;i<10;i++){
			new Thread(new Runnable() {
				public void run() {
					res.get();	
				}
			}).start();
		}
		//放数据
		for(int i=0;i<10;i++){
			new Thread(new Runnable() {
				public void run() {
					res.put("jkf");
				}
			}).start();
		}	
	}
}
//这是关键: 资源,内部通过ReentrantLock对get和put进行限制,如果put可以放在list的尾,get获取头,这就是一个FIFO队列的实现了,当然了这种实现还是有问题的,例如:取的时候不能放,放的时候不能取,
class ResourceReentrantLock{
	int index = 0;
	private List<String> list = new ArrayList<String>();
	private ReentrantLock lock = new ReentrantLock();
	public void put(String name){
		//消耗时间,防止set一直获取锁
		int i=0;
		int max = new Random().nextInt(10000);
		while(i<max){
			i++;
		}
		
		lock.lock();
		try{
			name = name+(index++);
			list.add(name);
			System.out.println(Thread.currentThread().getName()+" put "+name);
		}finally{
			lock.unlock();
		}
	}
	public String get(){
		//消耗时间,防止get一直获取锁
		int i=0;
		int max = new Random().nextInt(10000);
		while(i<max){
			i++;
		}
		
		lock.lock();
		try{
			if(list.size()>0){
				System.out.println(Thread.currentThread().getName()+" get "+list.get(0));
				return list.remove(0);
			}
			System.out.println(Thread.currentThread().getName()+" get ");
			return "";
		}finally{
			lock.unlock();
		}
	}
}

AQS(AbstractQueuedSynchronizer)

查看ReentrantLock源码,就会发现ReentrantLock的核心就是抽象的队列式的同步器(AbstractQueueSynchronizer):主要提供通过队列形式,处理线程的等待、唤醒、顺序等待问题,子类只需要关心什么情况下可以获取锁(加锁)、什么情况下释放锁(解锁),而加锁失败、释放锁之后的处理都不需要关心,统统由AQS来处理。

通过上述分析,就可以明白了AQS的原理,就是处理未获取锁线程的排队等待和已释放锁后唤醒什么线程,因此我们可以写出一个类似的同步器。如果有看过以前的文章,会发现在自旋锁中的CLH锁和MCS锁中,已经实现了类似的AQS,例如:通过队列实现获取锁失败后的线程顺序问题;通过自选处理线程的等待;释放锁之后,某一个自旋状态的线程就可以获取锁了。

MCS锁源码

package lock.review.lock;

import java.util.concurrent.atomic.AtomicReference;

public class MSCLock {
	
	public AtomicReference<MCSNode> tail;//尾指针
	public Thread cureentThread;
	
	public ThreadLocal<MCSNode> currentThreadLocal = new ThreadLocal<MCSNode>();
	
	
	public void lock(){
		MCSNode node = new MCSNode(false, null);//当前节点的初始状态:不可以获取锁
		MCSNode preNode = tail.getAndSet(node);
		if(preNode != null){
			// 加锁失败,进入等待队列中
			preNode.nextNode = node;//设置前尾节点指向node的nextNode为当前节点,建立显式链表。
			currentThreadLocal.set(node);
			// 加入等待队列后,线程自选
			while(node.lock);
			cureentThread = Thread.currentThread();
		}else{
			// 加锁成功,啥也不说了,直接干
			cureentThread = Thread.currentThread();
		}	
	}
	
	
	public void unlock(){
		// 是否可以释放锁
		if(currentThread != Thread.currentThread()){
			return;
		}
		// 可以释放,但是如何传递锁
		MCSNode node = currentThreadLocal.get();
		if(node.nextNode == null){
			//当前线程是最后一个线程,为了让锁传递下去,而当前线程也不用一直循环等待,设置tail为null
			if(tail.compareAndSet(node, null)){				
				node.nextNode=null;
				return;
			}else{				
				while(node.nextNode != null){
					node.nextNode.lock=true;
					node.nextNode=null;
				}
			}
		}
	}
}

//节点
class MCSNode{
	public volatile boolean lock;//true 可以获取锁,false 不可以获取锁,volatile 保证修改的立刻可见
	/**当前节点的下一节点,通过nextNode进行显示的链表表示多线程的顺序,
	 * 同时当线程释放锁时,需要通过nextNode获取下一节点,修改其lock状态
	 */
	public MCSNode nextNode;
	
	public MCSNode(boolean lock,MCSNode preNode){
		this.lock = lock;
		this.nextNode = preNode;
	}
}

通过上述锁的分析,我们可以直接进入AbstractQueuedSynchronizer查看源码。

AbstractQueuedSynchronizer 源码分析

1. 子类如何告知AQS加锁的结果(成功或者失败)

通过ReentrantLock的lock方法,我们发现如下入口:

AQS的入口

通过上述可以看出,如果我们需要自定义一个锁,只需要继承AQS,同时重写方法tryAcquire(),方法中明确什么情况下加锁失败即可,这样就简简单单的自定义锁。

2. 加锁失败如何处理

通过AQS方法,我们细致分析加锁失败后,如何处理失败的线程。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  1. !tryAcquire(arg) :调用子类的方法,加锁是否成功,如果成功,AQS就什么都不做,如果加锁失败,就进入:acquireQueued(addWaiter(Node.EXCLUSIVE,arg))方法。

  2. addWaiter(Node.EXCLUSIVE):把当前节点加入等待队列中。

    private Node addWaiter(Node mode) {
            Node node = new Node(Thread.currentThread(), mode);
            // Try the fast path of enq; backup to full enq on failure
            Node pred = tail;
            // 先搞个简单一次的把当前节点加入等待队列中
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            // 无法简单,就只能循环处理
            enq(node);
            return node;
        }
    

    Node 通过next(当前节点的下一个节点)、prev(当前节点的前一个节点)指针,串联整个等待队列。通过addWatiter把当前线程的节点加入等待线程节点中,请注意当前节点的waitStatus是0,初始值。

  3. acquireQueued()

    /**
         * Acquires in exclusive uninterruptible mode for thread already in
         * queue. Used by condition wait methods as well as acquire.
         *
         * @param node the node
         * @param arg the acquire argument
         * @return {@code true} if interrupted while waiting
         */
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    // 如果前节点是头结点,尝试获取锁
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    // 再次尝试获取锁失败
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    shouldParkAfterFailedAcquire:当前节点等待前,判断该节点的前一个节点是否可以值得等待:
    1 如果前节点取消了,就不能直接在这个前节点等待,因为该节点的被取消了,永远不会获取锁,也就谈不上释放锁之后唤醒下一节点,所以必须找到一个非取消的节点。
    2 在寻找前一个非取消节点时,如果节点没有设置状态,顺手设置状态为SIGNAL状态。

  4. parkAndCheckInterrupt 当前线程等待,内部调用LockSupport.park方法

    /**
         * Disables the current thread for thread scheduling purposes unless the
         * permit is available.
         *
         * <p>If the permit is available then it is consumed and the call returns
         * immediately; otherwise
         * the current thread becomes disabled for thread scheduling
         * purposes and lies dormant until one of three things happens:
         *
         * <ul>
         * <li>Some other thread invokes {@link #unpark unpark} with the
         * current thread as the target; or
         *
         * <li>Some other thread {@linkplain Thread#interrupt interrupts}
         * the current thread; or
         *
         * <li>The call spuriously (that is, for no reason) returns.
         * </ul>
         *
         * <p>This method does <em>not</em> report which of these caused the
         * method to return. Callers should re-check the conditions which caused
         * the thread to park in the first place. Callers may also determine,
         * for example, the interrupt status of the thread upon return.
         *
         * @param blocker the synchronization object responsible for this
         *        thread parking
         * @since 1.6
         */
        public static void park(Object blocker) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, 0L);
            setBlocker(t, null);
        }
    

    LockSupport.lock:当前线程阻塞,结束阻塞的方式如下:
    1.调用unpark方法,释放该线程的许可,这就是AQS唤醒下一线程使用的方式。
    2.该线程被中断,当前线程被中断后,也可以结束阻塞。
    3.到期时间,这个在AQS中,没有设置阻塞时间,暂时不会使用这个方法。
    通过上述方法,获取锁失败的线程,就正式的阻塞了,知道取消阻塞,注意哦:当线程被打断后,先需要把当前的等待线程结束掉,才能响应打断。

释放锁成功

当判断可以释放锁了,那么AQS会如何做呢?
可以看到AQS中,有release方法,用来处理释放锁之后的事情:激活下一节点。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            // 加一些判断,防止先调用unlock
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

子类重写tryRelease方法,告诉AQS释放锁是否成功,如果成功,那么剩下的工作就交给AQS。

  1. unparkSuccessor 把下一节点唤醒
/**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        // 当前节点的下一节点存在,并且非取消,唤醒它
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 特殊情况:从后往前找一个非取消的。
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

上述就是AQS的抽象队列同步器的大致原理,如果我们想自己写个特殊的锁,只需要写个AQS的子类,重写tryAcquire和tryRealse方法,就可以实现我们的目的,忽略获取锁失败和释放锁成功的后续操作。
为什么释放锁时,不按照当前节点的下一个节点直接唤醒,而是从

ReetrantLock 公平锁和非公平锁

通过源码发现,非公平锁的不公平表现在如下两个方面:

  1. lock时的不公平

    // 非公平锁lock
    final void lock() {
       if (compareAndSetState(0, 1))
               setExclusiveOwnerThread(Thread.currentThread());
           else
               acquire(1);
       }
    // 公平锁lock
    final void lock() {
                acquire(1);
            }
    

    非公平锁进入后,不会通过AQS,先去偷偷获取锁。

  2. tryAcquire的非公平

// 非公平锁
protected final boolean tryAcquire(int acquires) {
   return nonfairTryAcquire(acquires);
}
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;
        }

tryAcquire:公平锁,当有人排队时,我就直接去排队;非公平锁我才不管又没人排队,我先试试再说。

ReetrantLock的重入

其实在ReetrantLock的公平和非公平源码中,已经有提现了

无论公平锁和非公平锁,tryAcquire都有如下判断:

else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }

就是判断当前锁的独占线程是否是当前线程,如果是,就state+1

猜你喜欢

转载自blog.csdn.net/u010652576/article/details/84728138