四十一、并发工具之Semaphore

37 阅读8分钟

Semaphore

Semaphore概述

板书栏:

Semaphore是信号量,指定一定数量的资源,一个线程可以获取一个或者多个资源,当资源为0时,其他线程挂起,直到有线程归还资源,才会唤醒挂起的线程。

Semaphore是基于AQS实现的。资源数量是通过AQS中的state属性记录,每有一个线程来申请资源,state就减相应的数量,有线程归还资源,state就加上相应的数量。state值为0时,来申请资源的线程被挂起。有线程归还资源时,才会唤醒挂起的线程。

要点栏:

要点1:

Semaphore用来进行资源管理,有空闲资源时,会将资源给申请的线程,没有空闲资源时,挂起申请的线程。

Semaphore应用

板书栏:

public class Test {

    public static void main(String[] args) throws InterruptedException {

        Semaphore semaphore = new Semaphore(10);

        new Thread(() -> {
            System.out.println("一家三口要进来");
            try {
                semaphore.acquire(3);
                System.out.println("一家三口进来了");
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(3);
                System.out.println("一家三口离开了");
            }
        }).start();

        for (int i=0; i<7; i++) {
            int j = i;
            new Thread(()->{
                System.out.println(j + "要进来");
                try {
                    semaphore.acquire();
                    System.out.println(j + "进来了");
                    Thread.sleep(6000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                    System.out.println(j + "离开了");
                }
            }).start();
        }

        Thread.sleep(10);

        System.out.println("main要进来");
        if (semaphore.tryAcquire()) {
            System.out.println("main进来了");
            semaphore.release();
            System.out.println("main离开了");
        } else {
            System.out.println("资源不够,main进不来");
        }

        Thread.sleep(7000);
        if (semaphore.tryAcquire()) {
            System.out.println("main进来了");
        } else {
            System.out.println("资源不够,main进不来");
        }

        semaphore.release();
        System.out.println("main离开了");
    }
}

要点栏:

要点1:

获取资源的方法:

  • acquire():获取一个资源;响应中断,抛出异常
  • acquire(int):获取指定个数资源;响应中断,抛出异常
  • tryAcquire():获取一个资源,立即返回
  • tryAcquire(time, unit):获取一个资源,等待一定时间;响应中断,抛出异常
  • tryAcquire(int):获取指定个数资源,立即返回
  • tryAcquire(int,time,unit):获取指定个数资源,等待一定时间;响应中断 ,抛出异常
  • acquireUninterruptibly():获取一个资源,不响应中断
  • acquireUninterruptibly(int):获取指定个数资源,不响应中断

释放资源的方法:

  • release():释放一个资源
  • release(int):释放指定个数资源

要点2:

获取资源后,一定要释放响应数量的资源,否则最后资源会被耗尽。

Semaphore源码

Semaphore基本属性

板书栏:

public class Semaphore implements java.io.Serializable {
    private static final long serialVersionUID = -3222578661600680210L;
  
    private final Sync sync;

	// 抽象内部类
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        Sync(int permits) {
			// 将资源个数赋值给state
            setState(permits);
        }

        final int getPermits() {
            return getState();
        }

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

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

        final void reducePermits(int reductions) {
            for (;;) {
                int current = getState();
                int next = current - reductions;
                if (next > current) // underflow
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next))
                    return;
            }
        }

        final int drainPermits() {
            for (;;) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }

	// 非公平
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

	// 公平
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        FairSync(int permits) {
            super(permits);
        }

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

要点栏:

要点1:

Semaphore有三个内部类,其中Sync是抽象内部类,NonfairSync和FairSync继承了Sync

要点2:

Sync将资源数量通过setState方法赋值给了AQS中的state属性

要点3:

NonfairSync和FairSync的核心区别在于tryAcquireShared方法实现不同

Semaphore基本方法

构造方法

板书栏:

public Semaphore(int permits) {
	sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
	sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

要点栏:

要点1:

无参构造方法默认使用的是非公平方法

要点2:

有参构造方法可以指定使用非公平还是公平方法

acquire无参方法

背景栏:

非公平的实现

板书栏:

public void acquire() throws InterruptedException {
	sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
		throws InterruptedException {
	if (Thread.interrupted())
		// 响应中断,抛出中断异常
		throw new InterruptedException();

	// tryAcquireShared返回值小于0,说明没有资源了,当前线程要被挂起
	// tryAcquireShared返回值大于等于0,说明当前线程获取到资源,正常执行业务代码
	if (tryAcquireShared(arg) < 0)
		doAcquireSharedInterruptibly(arg);
}

protected int tryAcquireShared(int acquires) {
	return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
	for (;;) {
		// 获取state值
		int available = getState();
		// 获取剩余的资源
		int remaining = available - acquires;
		if (remaining < 0 ||
			compareAndSetState(available, remaining))
			// 返回剩余资源
			return remaining;
	}
}
private void doAcquireSharedInterruptibly(int arg)
	throws InterruptedException {
	// 将当前线程封装成Node,添加到双向链表中
	final Node node = addWaiter(Node.SHARED);
	boolean failed = true;
	try {
		for (;;) {
			// 获取前一个节点
			final Node p = node.predecessor();
			if (p == head) {
				// 前一个节点是head节点,再次尝试获取资源
				int r = tryAcquireShared(arg);
				if (r >= 0) {
					// 获取资源成功
					// 当前节点设置为head节点
                    // 唤醒后面的节点线程
					setHeadAndPropagate(node, r);
					p.next = null; 
					failed = false;
					return;
				}
			}
			// 获取资源失败
			// 挂起当前线程,并且将前一个节点状态改成-1
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				throw new InterruptedException();
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}
// node:当前线程封装的节点
// propagate:当前线程拿到资源后,剩余的资源数量
private void setHeadAndPropagate(Node node, int propagate) {
	// h赋值为head节点
	Node h = head;
	// 将当前线程节点设置为head节点
	setHead(node);

	// 下面的if判断中有很多条件满足后,都去尝试唤醒线程获取资源
	// 这样做是为了消除jdk1.5的BUG:有资源,但是存在线程没有被唤醒
	// 所以jdk1.8中这里就很轻易去唤醒线程去尝试获取一下资源
	if (
	// 判断剩余的资源数量是不是大于0
	// 如果大于0,唤醒当前线程节点后面等待的线程
	propagate > 0 || 
	// 判断之前head节点是否已经被gc释放
	// 如果被释放,不知道现在有没有新的线程释放资源
	// 唤醒当前线程节点后面等待的线程尝试获取资源
	h == null || 
	// 之前的head节点还没有被释放
	// 判断head节点的状态是否小于0
	// 小于0说明双向链表中还有其他线程挂起,唤醒线程尝试获取资源
	h.waitStatus < 0 ||
	// 判断当前线程作为head节点的状态是否小于0
	// 小于0说明双向链表中还有其他线程挂起,唤醒线程尝试获取资源
	(h = head) == null || h.waitStatus < 0) {
		Node s = node.next;
		if (s == null || s.isShared())
			// 唤醒后面的节点
			doReleaseShared();
	}
}
private void doReleaseShared() {
	for (;;) {
		// h赋值为head节点
		Node h = head;
		// 判断head节点不为null,并且双向链表中有节点
		if (h != null && h != tail) {
			// ws赋值为head节点的状态
			int ws = h.waitStatus;
			if (ws == Node.SIGNAL) {
				// head节点后面有节点
				// 将head节点状态设置为0,防止重复唤醒
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					continue;  
				// 唤醒后面的节点
				unparkSuccessor(h);
			}
			// head节点的状态为0,将其改成-3,表示后面有节点可以唤醒
			else if (ws == 0 &&
					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;   
		}
		// head节点没有变化,没有并发情况发生
		if (h == head)  
			break;
	}
}

要点栏:

要点1:

acquire方法是响应中断的,所以方法一开始就对线程是否中断做了判断

要点2:

如果state属性值大于等于当前线程申请的资源数,那么线程正常执行业务代码;否则线程会被挂起

要点3:

不带有时间参数的tryAcquire方法中,只使用了非公平的实现,因为只有非公平的情况下,才有可能在有线程排队的时候获取资源。公平情况下没有意义。

要点4:

带有时间参数的tryAcquire方法是执行AQS提供的acquire方法。因为这个方法排队的情况下,即使是公平的情况下也有可能获取到资源。

要点5:

acquireUninterruptibly的无参和有参方法是在挂起线程后,不会因为线程的中断而抛出异常,而是继续尝试获取资源,资源不够就挂起。

要点6:

公平情况下,acquire方法中只有一个地方与非公平情况下实现不同,就是tryAcquireShared方法

protected int tryAcquireShared(int acquires) {
	for (;;) {
		// 如果双向链表中没有节点,不走下面的if语句
		// 如果当前线程节点是head节点的next节点,也不走下面的if语句
		if (hasQueuedPredecessors())
			return -1;
		// 尝试获取资源
		int available = getState();
		int remaining = available - acquires;
		if (remaining < 0 ||
			compareAndSetState(available, remaining))
			return remaining;
	}
}

公平情况下,当前线程在获取资源前,需要对双向链表中的等待节点的情况做判断。如果有线程在等待,当前线程需要去排队,否则尝试获取资源。

release方法

板书栏:

public void release() {
	sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
	if (tryReleaseShared(arg)) {
		// 释放资源成功,执行doReleaseShared方法
		doReleaseShared();
		return true;
	}
	return false;
}

protected final boolean tryReleaseShared(int releases) {
	for (;;) {
		// 获取资源数
		int current = getState();
		// 资源数加上释放的资源数
		int next = current + releases;
		// 对资源数做健壮性判断
		if (next < current) 
			throw new Error("Maximum permit count exceeded");
		// 通过CAS将新的资源数赋值给state
		if (compareAndSetState(current, next))
			return true;
	}
}
private void doReleaseShared() {
	for (;;) {
		// h赋值为head节点
		Node h = head;
		// 判断head节点不为null,并且双向链表中有节点
		if (h != null && h != tail) {
			// ws赋值为head节点的状态
			int ws = h.waitStatus;
			if (ws == Node.SIGNAL) {
				// head节点后面有节点
				// 将head节点状态设置为0,防止重复唤醒
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					continue;  
				// 唤醒后面的节点
				unparkSuccessor(h);
			}
			// head节点的状态为0,将其改成-3,表示后面有节点可以唤醒
			else if (ws == 0 &&
					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;   
		}
		// head节点没有变化,没有并发情况发生
		if (h == head)  
			break;
	}
}

要点栏:

要点1:

release方法只有一种实现,不分公平还是非公平

要点2:

release方法释放资源就是将state值加上释放的资源数量