深入浅出Java并发包—Semaphore原理分析 (转载)
转载地址:http://yhjhappy234.blog.163.com/blog/static/3163283220135158415331/?suggestedreading&wumii
Semaphore在实际应用中经常被称之为“信号量”、“许可”等,它维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。缓存池、连接池、对象池等经常使用。下面我们来看一个数据库连接池的模拟示例。
|
package com.yhj.semaphore;
import java.util.Date; import java.util.Random; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock;
//连接 class Connection{ private String url; private String username; private String password;
public Connection(String url, String username, String password) { this.url = url; this.username = username; this.password = password; }
public Connection(Connection template) { this.url = template.getUrl(); this.username = template.getUsername(); this.password = template.getPassword(); }
public String getUrl() { return url; } public String getUsername() { return username; } public String getPassword() { return password; }
} //连接池 class ConnectionPool{
private int initSize;//初始容量 private int maxSize;//最大容量 private Semaphore semaphore;//许可证 private Connection template;//连接模版 private class Node{//链表节点 Connection conn;//当前节点的连接对象 Node next;//下一个节点 public Node(Connection conn) { this.conn = conn; } } private Node head;//头结点 private Node tail;//尾节点 private ReentrantLock lock = new ReentrantLock();//操作锁
//默认构造函数 public ConnectionPool(String url, String username, String password) { this(5,10,url,username,password); }
//构造函数 public ConnectionPool(int initSize,int maxSize,String url, String username, String password) { this.initSize = initSize; this.maxSize = maxSize; this.template = new Connection(url, username, password); initPool(); semaphore = new Semaphore(maxSize);//许可的连接数为最大连接数 }
//初始化连接池 public void initPool(){ if(initSize<=0||maxSize<initSize) throw new IllegalArgumentException(); Node cur = new Node(new Connection(template)); tail = head = cur; for(int i=1;i<initSize;++i){ Node newNode = new Node(new Connection(template)); cur.next = newNode; cur = cur.next; } tail = cur; }
//获取连接 public Connection getConnection() throws InterruptedException{ semaphore.acquire();//先看是否许可执行,不许可则阻塞等待 lock.lock();//非原子操作,为保证数据一致性,需加锁 try { if(head==null){//超过了初始限定的连接数,需扩容 return new Node(new Connection(template)).conn; }else{ Node ret = head; head = head.next; if(head == null)//队列中没有元素了 tail = head = null; return ret.conn; } }finally{ lock.unlock(); } }
//释放连接 public void closeConnection(Connection conn){ lock.lock(); try { Node ret = new Node(conn); if(tail == null){//队列中没有元素 head = tail = ret; }else{ tail.next = ret; tail = tail.next; } }finally{ lock.unlock(); } semaphore.release();//结束后释放连接 } } //Semaphore测试用例 public class SemaphoreTestCase {
public static void main(String[] args) { final ConnectionPool pool = newConnectionPool(1,2,"http://yhjhappy234.blog.163.com/", "root", "******"); final Random random = new Random(); for(int i=0;i<10;++i){//启用10个线程 去获取连接 final String threadNo ="YHJ"+i; new Thread(){
public void run() { try { System.out.println(new Date()+" - "+threadNo+": 等待连接..."); Connection conn = pool.getConnection(); System.out.println(new Date()+" - "+threadNo+": 获取连接"+conn); long sleepTime = random.nextInt(1000); Thread.sleep(sleepTime); System.out.println(new Date()+" - "+threadNo+": 持有连接"+sleepTime/1000.0+ "s,持有数据"+ conn); pool.closeConnection(conn); System.out.println(new Date()+" - "+threadNo+": 释放连接"+conn);
} catch (InterruptedException e) { e.printStackTrace(); }
};
}.start(); } } } |
这段代码也比较简单,就模拟了一个连接池的过程,其中使用一个链表来存储有效的可被许可的连接。
获取连接时,首先使用Semaphore获取一个许可(这里的许可数是最大的连接数maxSize),如果没有获取到许可则进行等待,获得许可后从池子中拿取一个连接(这里特别说明的是信号量只是在信号不够的时候挂起线程,但是并不能保证信号量足够的时候获取对象和返还对象是线程安全的,所以还需要一把锁来保证数据的一致性),释放资源的时候先将数据放置到池子中,然后再释放许可。整个过程中Semaphore扮演的只是一个许可的计数器,并不参与许可对象。
当Semaphore的许可资源为一时,Semaphore最多只有一个许可,形似上类似于排它锁(也可以作为开关,可参考CountDownLauch),这通常也称为二进制信号量,因为它只能有两种状态:一个可用的许可,或零个可用的许可。按此方式使用时,二进制信号量具有某种属性(与很多 Lock 实现不同),即可以由线程释放“锁”,而不是由所有者(因为信号量没有所有权的概念)。
另外同公平锁非公平锁一样,信号量也有公平性。如果一个信号量是公平的表示线程在获取信号量时按FIFO的顺序得到许可,也就是按照请求的顺序得到释放。这里特别说明的是:所谓请求的顺序是指在请求信号量而进入FIFO队列的顺序,有可能某个线程先请求信号而后进去请求队列,那么次线程获取信号量的顺序就会晚于其后请求但是先进入请求队列的线程。
说了这么多,那它是怎么实现的呢?我们一起来看下他的源代码:
Semaphore内部也实现了AQS的同步器,同时支持公平锁和非公平锁。默认为非公平锁。
|
abstract static class Sync extends AbstractQueuedSynchronizer final static class NonfairSync extends Sync final static class FairSync extends Sync public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = (fair)? new FairSync(permits) : new NonfairSync(permits); } |
我们知道导致Semaphore的是acquire方法,其内部是调用AQS的acquireSharedInterruptibly(1)方法
|
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } |
Semaphore试图通过AQS申请一把共享锁,这段在前面锁机制中已经提到了他的原理,这里就不多说了,可参考共享锁的原理分析。
|
public final void acquireSharedInterruptibly(int arg) throwsInterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } |
我们来看下Semaphore内部类实现的Sync的重写部分
非公平信号实现
|
rotected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); } 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 p = getState(); if (compareAndSetState(p, p + releases)) return true; } } |
有了我们之前AQS的基础,分析这段代码显得很简单,当信号量不足的时候返回的是负数(0-需要的信号数据),AQS检测到返回的是负数则将当前线程送入CLH队列阻塞。释放的时候也类似,通过CAS返还信号成功则唤醒其他线程重新进行许可申请。
同样的,公平信号的实现和AQS类似,也只是加了一个是否是队头元素的判断,如果不是队头的线程则直接返回-1.
|
protected int tryAcquireShared(int acquires) { Thread current = Thread.currentThread(); for (;;) { Thread first = getFirstQueuedThread(); if (first != null && first != current) return -1; int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } } |
当然非公平信号和公平信号还有一个很大的异同点,就是公平锁每次都需要进入AQS的队列,如果不是队头的元素就需要排队等待,而非公平锁只有在可用信号不足的时候才进行排队,所以非公平信号要比公平信号吞吐量大很多。
Semaphore其实就是一个共享锁的实现,前面我们已经讲过,因此这里就显得比较简单,下面我们再来写一个实例了解一下Semaphore代替锁的一些用途。
前面我们已经通过锁的Condition实现了一个生产者消费者模型。今天我们用Semaphore信号/许可再来实现一遍。废话不多说,看代码:
|
package com.yhj.semaphore;
import java.util.Date; import java.util.concurrent.Semaphore; //消息 class Msg{}
/** * @Described 生产者消费者模型 * @Author YHJ create at 2013-6-15 下午09:15:27 */ public class ProductQueue<T> {
private final T[] items; //队列存储区
private Semaphore coreSign = new Semaphore(1); //独占信号 模拟独占锁
private Semaphore notFull; //队列非满信号 因队列大小未知,需要在队列构建的时候初始化
private Semaphore notEmpty = new Semaphore(0); //队列非空信号
private int head, tail, count; //下标
@SuppressWarnings("unchecked") public ProductQueue(int maxSize) { items = (T[]) new Object[maxSize]; notFull = new Semaphore(maxSize);///初始化信号 }
/** * 默认10个元素 * @Constructors * @Author YHJ create at 2013-6-15 下午09:15:21 */ public ProductQueue() { this(10); }
/** * 放置数据 * @param t * @throws InterruptedException * @Author YHJ create at 2013-6-15 下午09:15:27 */ public void put(T t) throws InterruptedException { notFull.acquire();//获取入队列许可 coreSign.acquire();//模拟独占锁 独占信号 try { items[tail] = t; if (++tail == getCapacity()) { tail = 0; } ++count; } finally{ coreSign.release();//释放模拟信号 notEmpty.release();//通知队列非空 } }
/** * 取数据 * @return * @throws InterruptedException * @Author YHJ create at 2013-6-15 下午09:18:36 */ public T take() throws InterruptedException { notEmpty.acquire();//获取出队列许可 coreSign.acquire();//模拟独占锁 独占信号 try { T ret = items[head]; items[head] = null;//GC if (++head == getCapacity()) { head = 0; } --count; return ret; } finally { coreSign.release(); notFull.release(); } }
/** * 获取容量(队列) * @return * @Author YHJ create at 2013-6-15 下午09:18:45 */ public int getCapacity() { return items.length; }
/** * 获取元素数目 * @return * @throws InterruptedException * @Author YHJ create at 2013-6-15 下午09:19:04 */ public int size() throws InterruptedException { coreSign.acquire(); try { return count; } finally { coreSign.release(); } }
//主函数 public static void main(String[] args) { final ProductQueue<Object> queue = new ProductQueue<Object>(5); new Thread(){//消费线程 每100毫秒收一次 public void run() { try { for(int i =0;i<10;++i){ System.out.println(new Date()+" - 消费者"+i+": 等待取数据..."); Object obj = queue.take(); System.out.println(new Date()+" - 消费者"+i+": 取到数据"+obj); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); }
}; }.start(); new Thread(){//生成线程 每10ms放进去一个数据 public void run() { try { for(int i =0;i<10;++i){ System.out.println(new Date()+" - 生产者"+i+": 等待生产数据..."); Msg msg = new Msg(); queue.put(msg); System.out.println(new Date()+" - 生产者"+i+": 生产数据"+msg); Thread.sleep(10); } } catch (InterruptedException e) { e.printStackTrace(); }
}; }.start(); } } 运行结果:
|
这个示例比较特别,前面我们说到Semaphore只是替代许可的对象进行计数,并不参与操作,因此最上面的例子需要加锁,而此处的巧妙之处在于使用了一个信号量为一的Semaphore来代替独占锁,同时用另外两个许可信号来代替前面的Condition,使得整个代码简洁很多(至少去掉了自旋锁定的代码,看起来简洁多了)。原理我就不多解释了,基于前面的知识,大家一看就明白了!


浙公网安备 33010602011771号