“J.U.C”:Semaphore (r)

来源:互联网 发布:对淘宝处理结果不满意 编辑:程序博客网 时间:2024/05/18 03:13

信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个“共享锁”(可以表示某个资源的个数,例如停车场的车位)。

Java并发提供了两种加锁模式:共享锁和独占锁。前面LZ介绍的ReentrantLock就是独占锁。对于独占锁而言,它每次只能有一个线程持有,而共享锁则不同,它允许多个线程并行持有锁,并发访问共享资源。

独占锁它所采用的是一种悲观的加锁策略,  对于写而言为了避免冲突独占是必须的,但是对于读就没有必要了,因为它不会影响数据的一致性。如果某个只读线程获取独占锁,则其他读线程都只能等待了,这种情况下就限制了不必要的并发性,降低了吞吐量。而共享锁则不同,它放宽了加锁的条件,采用了乐观锁机制,它是允许多个读线程同时访问同一个共享资源的。

Semaphore简介

Semaphore,在API中是这样介绍的,一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。下面LZ以理发为例来简述Semaphore。

为了简单起见,我们假设只有三个理发师、一个接待人。一开始来了五个客人,接待人则安排三个客人进行理发,其余两个人必须在那里等着,此后每个来理发店的人都必须等待。一段时间后,一个理发师完成理发后,接待人则安排另一个人(公平还是非公平机制呢??)来理发。在这里理发师则相当于公共资源,接待人则相当于信号量(Semaphore),客户相当于线程。

进一步讲,我们确定信号量Semaphore是一个非负整数(>=1)。当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1;

当信号量Semaphore = 1 时,它可以当作互斥锁使用。其中0、1就相当于它的状态,当=1时表示其他线程可以获取,当=0时,排他,即其他线程必须要等待。

Semaphore源码分析

Semaphore的结构如下:

2015081800001

从上面可以看出,Semaphore和ReentrantLock一样,都是包含公平锁(FairySync)和非公平锁(NonfairSync),两个锁都是继承Sync,而Sync也是继承自AQS。其构造函数如下:

[java] view plain copy
 print?
  1. /** 
  2.      * 创建具有给定的许可数和非公平的公平设置的 Semaphore。 
  3.      */  
  4.     public Semaphore(int permits) {  
  5.         sync = new NonfairSync(permits);  
  6.     }  
  7.       
  8.     /** 
  9.      * 创建具有给定的许可数和给定的公平设置的 Semaphore。 
  10.      */  
  11.     public Semaphore(int permits, boolean fair) {  
  12.         sync = fair ? new FairSync(permits) : new NonfairSync(permits);  
  13.     }  

信号量的获取:acquire()

在ReentrantLock中已经阐述过,公平锁和非公平锁获取锁机制的差别:对于公平锁而言,如果当前线程不在CLH队列的头部,则需要排队等候,而非公平锁则不同,它无论当前线程处于CLH队列的何处都会直接获取锁。所以公平信号量和非公平信号量的区别也一样。

[java] view plain copy
 print?
  1.     public void acquire() throws InterruptedException {  
  2.         sync.acquireSharedInterruptibly(1);   //可以直接响应中断
  3.     }  
  4.       
  5.     public final void acquireSharedInterruptibly(int arg)  
  6.             throws InterruptedException {  
  7.         if (Thread.interrupted())  
  8.             throw new InterruptedException();  
  9.         if (tryAcquireShared(arg) < 0)  
  10.             doAcquireSharedInterruptibly(arg);  
  11.     }  

对于公平信号量和非公平信号量,他们机制的差异就体现在traAcquireShared()方法中:

公平锁

[java] view plain copy
 print?
  1. protected int tryAcquireShared(int acquires) {  
  2.         for (;;) {  
  3.             //判断该线程是否位于CLH队列的列头,如果是的话返回 -1,调用doAcquireSharedInterruptibly()  
  4.             if (hasQueuedPredecessors())  
  5.                 return -1;  
  6.             //获取当前的信号量许可  
  7.             int available = getState();  
  8.             //设置“获得acquires个信号量许可之后,剩余的信号量许可数”  
  9.             int remaining = available - acquires;  
  10.               
  11.             //如果剩余信号量 > 0 ,则设置“可获取的信号量”为remaining  
  12.             if (remaining < 0 || compareAndSetState(available, remaining))  //如果大于0 就尝试CAS方式更改state
  13.                 return remaining;  
  14.         }  
  15.     }  

tryAcquireShared是尝试获取 信号量,remaining表示下次可获取的信号量。

对于hasQueuedPredecessors、compareAndSetState在ReentrantLock中已经阐述了,hasQueuedPredecessors用于判断该线程是否位于CLH队列列头,compareAndSetState用于设置state的,它是进行原子操作的。代码如下:

[java] view plain copy
 print?
  1.     public final boolean hasQueuedPredecessors() {  
  2.         Node t = tail; // Read fields in reverse initialization order  
  3.         Node h = head;  
  4.         Node s;  
  5.         return h != t &&  
  6.             ((s = h.next) == null || s.thread != Thread.currentThread());  
  7.     }  
  8.   
  9.     protected final boolean compareAndSetState(int expect, int update) {  
  10.         return unsafe.compareAndSwapInt(this, stateOffset, expect, update);  
  11.     }  

doAcquireSharedInterruptibly源代码如下:

[java] view plain copy
 print?
  1. private void doAcquireSharedInterruptibly(int arg)  
  2.             throws InterruptedException {  
  3.             /* 
  4.              * 创建CLH队列的node节点,Node.SHARED表示该节点为共享锁 
  5.              */  
  6.             final Node node = addWaiter(Node.SHARED);  
  7.             boolean failed = true;  
  8.             try {  
  9.                 for (;;) {  
  10.                     //获取该节点的前继节点  
  11.                     final Node p = node.predecessor();  
  12.                     //当p为头节点时,基于公平锁机制,线程尝试获取锁  
  13.                     if (p == head) {  
  14.                         //尝试获取锁  
  15.                         int r = tryAcquireShared(arg);      
  16.                         if (r >= 0) {  
  17.                             setHeadAndPropagate(node, r);    
  18.                             p.next = null// help GC  
  19.                             failed = false;  
  20.                             return;  
  21.                         }  
  22.                     }  
  23.                     //判断当前线程是否需要阻塞,如果阻塞的话,则一直处于阻塞状态知道获取共享锁为止  
  24.                     if (shouldParkAfterFailedAcquire(p, node) &&  
  25.                         parkAndCheckInterrupt())  
  26.                         throw new InterruptedException();   //如果被中断过,就直接跑出中断异常
  27.                 }  
  28.             } finally {  
  29.                 if (failed)  
  30.                     cancelAcquire(node);  
  31.             }  
  32.         }  

doAcquireSharedInterruptibly主要是做两个工作;1、尝试获取共享锁,2、阻塞线程直到线程获取共享锁。

addWaiter(Node.SHARED):创建”当前线程“的Node节点,且Node中记录的锁的类型是”共享锁“(Node.SHARED);并将该节点添加到CLH队列末尾。

shouldParkAfterFailedAcquire:如果在尝试获取锁失败之后,线程应该等待,返回true;否则返回false。

parkAndCheckInterrupt:当前线程会进入等待状态,直到获取到共享锁才继续运行。

对于addWaiter、shouldParkAfterFailedAcquire、parkAndCheckInterruptLZ在“【Java并发编程实战】—–“J.U.C”:ReentrantLock之二lock方法分析”中详细介绍了。

非公平锁

对于非公平锁就简单多了,她没有那些所谓的要判断是不是CLH队列的列头,如下

[java] view plain copy
 print?
  1. final int nonfairTryAcquireShared(int acquires) {  
  2.             for (;;) {  
  3.                 int available = getState();  
  4.                 int remaining = available - acquires;  
  5.                 if (remaining < 0 ||  
  6.                     compareAndSetState(available, remaining))  
  7.                     return remaining;  
  8.             }  
  9.         }  

在非公平锁中,tryAcquireShared直接调用AQS的nonfairTryAcquireShared()。通过上面的代码我可看到非公平锁并没有通过if (hasQueuedPredecessors())这样的条件来判断该节点是否为CLH队列的头节点,而是直接判断信号量。

信号量的释放:release()

信号量Semaphore的释放和获取不同,它没有分公平锁和非公平锁。如下:

[java] view plain copy
 print?
  1. public void release() {  
  2.         sync.releaseShared(1);  
  3.     }  
  4.     public final boolean releaseShared(int arg) {  
  5.         //尝试释放共享锁  
  6.         if (tryReleaseShared(arg)) {  
  7.             doReleaseShared();  
  8.             return true;  
  9.         }  
  10.         return false;  
  11.     }  

release()释放线索所占有的共享锁,它首先通过tryReleaseShared尝试释放共享锁,如果成功直接返回,如果失败则调用doReleaseShared来释放共享锁。

tryReleaseShared:

[java] view plain copy
 print?
  1. protected final boolean tryReleaseShared(int releases) {  
  2.         for (;;) {  
  3.             int current = getState();  
  4.             //信号量的许可数 = 当前信号许可数 + 待释放的信号许可数  
  5.             int next = current + releases;  
  6.             if (next < current) // overflow  
  7.                 throw new Error("Maximum permit count exceeded");  
  8.             //设置可获取的信号许可数为next  
  9.             if (compareAndSetState(current, next))  
  10.                 return true;  
  11.         }  
  12.     }  

doReleaseShared:

[java] view plain copy
 print?
  1. private void doReleaseShared() {  
  2.             for (;;) {  
  3.                 //node 头节点  
  4.                 Node h = head;  
  5.                 //h != null,且h != 尾节点  
  6.                 if (h != null && h != tail) {  
  7.                     //获取h节点对应线程的状态  
  8.                     int ws = h.waitStatus;  
  9.                     //若h节点状态为SIGNAL,表示h节点的下一个节点需要被唤醒  
  10.                     if (ws == Node.SIGNAL) {  
  11.                         //设置h节点状态  
  12.                         if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))  
  13.                             continue;  
  14.                         //唤醒h节点对应的下一个节点  
  15.                         unparkSuccessor(h);  
  16.                     }  
  17.                     //若h节点对应的状态== 0 ,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态  
  18.                     else if (ws == 0 &&  
  19.                              !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))  
  20.                         continue;                  
  21.                 }  
  22.                 //h == head时,则退出循环,若h节点发生改变时则循环继续  
  23.                 if (h == head)                    
  24.                     break;  
  25.             }  
  26.         }  

在这里有关的方法,请参考:【Java并发编程实战】—–“J.U.C”:ReentrantLock之三unlock方法分析。

实例

该实例来源于《java7并发编程实战手册》

打印任务:

[java] view plain copy
 print?
  1.     public class PrintQueue {  
  2.     private final Semaphore semaphore;   //声明信号量  
  3.       
  4.     public PrintQueue(){  
  5.         semaphore = new Semaphore(1);  
  6.     }  
  7.       
  8.     public void printJob(Object document){  
  9.         try {  
  10.             semaphore.acquire();//调用acquire获取信号量  
  11.             long duration = (long) (Math.random() * 10);  
  12.             System.out.println( Thread.currentThread().getName() +   
  13.                     "PrintQueue : Printing a job during " + duration);  
  14.             Thread.sleep(duration);  
  15.         } catch (InterruptedException e) {  
  16.             e.printStackTrace();  
  17.         }  finally{  
  18.             semaphore.release();  //释放信号量  
  19.         }  
  20.     }  
  21. }  

Job:

[java] view plain copy
 print?
  1. public class Job implements Runnable{  
  2.     private PrintQueue printQueue;  
  3.       
  4.     public Job(PrintQueue printQueue){  
  5.         this.printQueue = printQueue;  
  6.     }  
  7.       
  8.     @Override  
  9.     public void run() {  
  10.         System.out.println(Thread.currentThread().getName() + " Going to print a job");  
  11.         printQueue.printJob(new Object());  
  12.         System.out.println(Thread.currentThread().getName() + " the document has bean printed");  
  13.     }  
  14. }  

Test:

[java] view plain copy
 print?
  1. public class Test {  
  2.     public static void main(String[] args) {  
  3.         Thread[] threads = new Thread[10];  
  4.           
  5.         PrintQueue printQueue = new PrintQueue();  
  6.           
  7.         for(int i = 0 ; i < 10 ; i++){  
  8.             threads[i] = new Thread(new Job(printQueue),"Thread_" + i);  
  9.         }  
  10.           
  11.         for(int i = 0 ; i < 10 ; i++){  
  12.             threads[i].start();  
  13.         }  
  14.     }  
  15. }  

运行结果:

Thread_0 Going to print a jobThread_0PrintQueue : Printing a job during 1Thread_4 Going to print a jobThread_1 Going to print a jobThread_2 Going to print a jobThread_3 Going to print a jobThread_0 the document has bean printedThread_4PrintQueue : Printing a job during 7Thread_4 the document has bean printedThread_1PrintQueue : Printing a job during 1Thread_2PrintQueue : Printing a job during 3Thread_1 the document has bean printedThread_2 the document has bean printedThread_3PrintQueue : Printing a job during 1Thread_3 the document has bean printed

参考资料

1、Java多线程系列–“JUC锁”11之 Semaphore信号量的原理和示例

2、java信号量控制线程打印顺序的示例分享

3、JAVA多线程–信号量(Semaphore)

0 0
原创粉丝点击