【java并发】线程同步工具Semaphore的使用

Semaphore通常用于限制可以访问某些资源(物理或逻辑的)的线程数目,我们可以自己设定最大访问量。它有两个很常用的方法是acquire()和release(),分别是获得许可和释放许可。 
  官方JDK上面对Semaphore的解释是这样子的 :

一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()和release()获取和释放访问许可。

  我的解释是这样子的:

Semaphore相当于一个厕所,我在造的时候可以想造几个坑就造几个坑,假如现在我就造了3个坑,现在有10个人想要来上厕所,那么每次就只能3个人上,谁最先抢到谁就进去,出来了一个人后,第4个人才能进去,这个就限制了上厕所的人数了,就这个道理。每个人上厕所之前都先acquire()一下,如果有坑,就可以进入,没有就被阻塞,在外面等;上完厕所后,会release()一下,释放一个坑出来,以保证下一个人acquire()的时候有坑。

  我觉得我的解释比官方的要好……

1. Semaphore基本使用

  Semaphore在限制资源访问量的问题上用处很大,比如限制一个文件的并发访问次数等,它的原理很好理解。下面写一个Semaphore的示例代码:

public class SemaphoreTest {

    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();//使用并发库,创建缓存的线程池
        final Semaphore sp = new Semaphore(3);//创建一个Semaphore信号量,并设置最大并发数为3

        //availablePermits() //用来获取当前可用的访问次数
        system.out.println("初始化:当前有" + (3 - sp.availablePermits() + "个并发"));

        //创建10个任务,上面的缓存线程池就会创建10个对应的线程去执行
        for (int index = 0; index < 10; index++) {
            final int NO = index;  //记录第几个任务
            Runnable run = new Runnable() {  //具体任务
                public void run() {
                    try {
                        sp.acquire();  // 获取许可
                        System.out.println(Thread.currentThread().getName()
                            + "获取许可" + "("+NO+")," + "剩余:" + sp.availablePermits());
                        Thread.sleep(1000);
                        // 访问完后记得释放 ,否则在控制台只能打印3条记录,之后线程一直阻塞
                        sp.release();  //释放许可
                        System.out.println(Thread.currentThread().getName()
                            + "释放许可" + "("+NO+")," + "剩余:" + sp.availablePermits());
                    } catch (InterruptedException e) {
                    }
                }
            };
            service.execute(run);  //执行任务
        }
        service.shutdown(); //关闭线程池
    }
}

  代码结构很容易理解,10个任务,每次最多3个线程去执行任务,其他线程被阻塞。可以通过打印信息来看线程的执行情况:

初始化:当前有0个并发 
pool-1-thread-1获取许可(0),剩余:1 
pool-1-thread-3获取许可(2),剩余:0 
pool-1-thread-2获取许可(1),剩余:1 
pool-1-thread-1释放许可(0),剩余:3 
pool-1-thread-4获取许可(3),剩余:1 
pool-1-thread-5获取许可(4),剩余:1 
pool-1-thread-2释放许可(1),剩余:3 
pool-1-thread-3释放许可(2),剩余:3 
pool-1-thread-6获取许可(5),剩余:0 
pool-1-thread-4释放许可(3),剩余:2 
pool-1-thread-9获取许可(8),剩余:0 
pool-1-thread-5释放许可(4),剩余:2 
pool-1-thread-6释放许可(5),剩余:2 
pool-1-thread-8获取许可(7),剩余:0 
pool-1-thread-7获取许可(6),剩余:2 
pool-1-thread-8释放许可(7),剩余:2 
pool-1-thread-10获取许可(9),剩余:2 
pool-1-thread-7释放许可(6),剩余:2 
pool-1-thread-9释放许可(8),剩余:2 
pool-1-thread-10释放许可(9),剩余:3

  从结果中看,前三个为什么剩余的不是3,2,1呢?包括下面,每次释放的时候剩余的量好像也不对,其实是对的,只不过线程运行太快,前三个是这样子的:因为最大访问量是3,所以前三个在打印语句之前都执行完了aquire()方法了,或者有部分执行了,从上面的结果来看,线程1是第一个进去的,线程2第二个进去,然后线程1和2开始打印,所以只剩1个了,接下来线程3进来了,打印只剩0个了。后面释放的时候也是,打印前可能有不止一个释放了。

2. Semaphore同步问题

  我从网上查了一下,有些人说Semaphore实现了同步功能,我觉得不对,因为我自己写了个测试代码试了,并不会自己解决并发问题,如果多个线程操作同一个数据,还是需要自己同步一下的。然后我查了一下官方JDK文档(要永远相信官方的文档),它里面是这样说的:

获得一项前,每个线程必须从信号量获取许可,从而保证可以使用该项。该线程结束后,将项返回到池中并将许可返回到该信号量,从而允许其他线程获取该项。注意,调用 acquire() 时无法保持同步锁,因为这会阻止将项返回到池中。信号量封装所需的同步,以限制对池的访问,这同维持该池本身一致性所需的同步是分开的。

  这段官方的解释就很明确了,然后我就明白了网上有些人说的实现了同步的意思是信号量本身封装所需的同步,也就是说我拿到了一个,别人就无法拿到了,我释放了别人才能拿到(就跟我举的厕所的坑一样),但是我拿到了之后去操作公共数据的时候,针对这个数据操作的同步Semaphore就不管了,这就需要我们自己去同步了。下面写一个同步的测试代码:

public class SemaphoreTest2 {

    private static int data = http://blog.csdn.net/eson_15/article/details/0;

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final Semaphore sp = new Semaphore(3);
        System.out.println("初始化:当前有" + (3 - sp.availablePermits() + "个并发"));

        // 10个任务
        for (int index = 0; index < 10; index++) {
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        // 获取许可
                        sp.acquire();
                        System.out.println(Thread.currentThread().getName()
                                + "获取许可" + "(" + NO + ")," + "剩余:" + sp.availablePermits());
                        //实现同步
                        synchronized(SemaphoreTest2.class) {
                            System.out.println(Thread.currentThread().getName()
                                    + "执行data自增前:data="http://blog.csdn.net/eson_15/article/details/ + data);
                            data++;
                            System.out.println(Thread.currentThread().getName()
                                    + "执行data自增后:data="http://blog.csdn.net/eson_15/article/details/ + data);
                        }

                        sp.release();
                        System.out.println(Thread.currentThread().getName()
                                + "释放许可" + "(" + NO + ")," + "剩余:" + sp.availablePermits());
                    } catch (InterruptedException e) {
                    }
                }
            };
            service.execute(run);
        }
        service.shutdown();
    }
}

  看一下运行结果(部分):

初始化:当前有0个并发 
pool-1-thread-2获取许可(1),剩余:0 
pool-1-thread-2执行data自增前:data=http://blog.csdn.net/eson_15/article/details/0 
pool-1-thread-3获取许可(2),剩余:0 
pool-1-thread-1获取许可(0),剩余:0 
pool-1-thread-2执行data自增后:data=http://blog.csdn.net/eson_15/article/details/1 
pool-1-thread-3执行data自增前:data=http://blog.csdn.net/eson_15/article/details/1 
pool-1-thread-3执行data自增后:data=http://blog.csdn.net/eson_15/article/details/2 
pool-1-thread-1执行data自增前:data=http://blog.csdn.net/eson_15/article/details/2 
pool-1-thread-7获取许可(6),剩余:1 
pool-1-thread-3释放许可(2),剩余:2 
pool-1-thread-1执行data自增后:data=http://blog.csdn.net/eson_15/article/details/3

  从结果中可以看出,每个线程在操作数据的前后,是不会受其他线程的影响的,但是其他线程可以获取许可,获取许可了之后就被阻塞在外面,等待当前线程操作完data才能去操作。当然也可以在当前线程操作data的时候,其他线程释放许可,因为这完全不冲突。 
  那如果把上面同步代码块去掉,再试试看会成什么乱七八糟的结果(部分):

初始化:当前有0个并发 
pool-1-thread-3获取许可(2),剩余:0 
pool-1-thread-2获取许可(1),剩余:0 
pool-1-thread-3执行data自增前:data=http://blog.csdn.net/eson_15/article/details/0 
pool-1-thread-2执行data自增前:data=http://blog.csdn.net/eson_15/article/details/0 
pool-1-thread-1获取许可(0),剩余:0 
pool-1-thread-3执行data自增后:data=http://blog.csdn.net/eson_15/article/details/1 
pool-1-thread-2执行data自增后:data=http://blog.csdn.net/eson_15/article/details/2 
pool-1-thread-7获取许可(6),剩余:0 
pool-1-thread-1执行data自增前:data=http://blog.csdn.net/eson_15/article/details/2 
pool-1-thread-8获取许可(7),剩余:0 
pool-1-thread-7执行data自增前:data=http://blog.csdn.net/eson_15/article/details/2 
pool-1-thread-2释放许可(1),剩余:1 
pool-1-thread-7执行data自增后:data=http://blog.csdn.net/eson_15/article/details/4

  从结果中看,已经很明显了,线程2和3都进去了,然后初始data都是0,线程3自增了一下,打印出1是没问题的,但是线程2呢?也自增了一下,却打印出了2。也就是说,线程2在操作数据的前后,数据已经被线程3修改过了,再一次证明Semaphere并没有实现对共有数据的同步,在操作公共数据的时候,需要我们自己实现。 
  Semaphere中如果设置信号量为1的话,那就说明每次只能一个线程去操作任务,那这样的话也就不存在线程安全问题了,所以如果设置信号量为1的话,就可以去掉那个synchronized,不过效率就不行了。 
  Semaphere的使用就总结这么多吧! 
  

  相关阅读:http://blog.csdn.net/column/details/bingfa.html



—–乐于分享,共同进步! 
—–更多文章请看:http://blog.csdn.net/eson_15

时间: 2024-12-29 17:00:38

【java并发】线程同步工具Semaphore的使用的相关文章

Java 并发 线程同步

Java 并发 线程同步 @author ixenos 同步 1.异步线程本身包含了执行时需要的数据和方法,不需要外部提供的资源和方法,在执行时也不关心与其并发执行的其他线程的状态和行为 2.然而,大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取,这将产生同步问题(可见性和同步性的丢失) 比如两个线程同时执行指令account[to] += amount,这不是原子操作,可能被处理如下: a)将account[to]加载到寄存器 b)增加amount c)将结果写回acco

【JAVA并发】同步工具类

同步工具类主要包括闭锁(如CountDownLatch),栅栏(如CyclicBarrier),信号量(如Semaphore)和阻塞队列(如LinkedBlockingQueue)等: 使用同步工具类可以协调线程的控制流: 同步工具类封装了一些状态,这些状态决定线程是继续执行还是等待,此外同步工具类还提供了修改状态的方法: 下面将简单介绍以上同步工具类: 闭锁 可以让一个线程等待一组事件发生后(不一定要线程结束)继续执行: 以CountDownLatch为例,内部包含一个计数器,一开始初始化为一

JAVA 并发编程-线程同步工具类(十二)

本文主要介绍一些java线程同步工具类,并不进行具体讲解,当有需要时,可以再去结合实例学习. 信号灯(Semaphore) 应用场景举例: 例如公司的打卡系统,如果有一个打卡机,那么一次就只能有一个人打卡,其余的人就被阻塞住,打卡完以后就可由下一个人打卡.如果有3个打卡机,那么一次就允许3个人或者少于三个人打卡,其余的人就得等待打卡机空闲下来才能继续打卡. 结果: 已进入1个线程,还可进入2个 已进入2个线程,还可进入1个 已进入3个线程,还可进入0个 空余出1个 已进入4个线程,还可进入0个

经典线程同步 信号量Semaphore

阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event> <秒杀多线程第七篇经典线程同步互斥量Mutex> 前面介绍了关键段CS.事件Event.互斥量Mutex在经典线程同步问题中的使用.本篇介绍用信号量Semaphore来解决这个问题. 首先也来看看如何使用信号量,信号量Semaphore常用有三个函数,使用很方便.下面是这几个函数的原型和使

秒杀多线程第八篇 经典线程同步 信号量Semaphore

版权声明:本文为博主原创文章,未经博主允许不得转载. 阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event> <秒杀多线程第七篇经典线程同步互斥量Mutex> 前面介绍了关键段CS.事件Event.互斥量Mutex在经典线程同步问题中的使用.本篇介绍用信号量Semaphore来解决这个问题. 首先也来看看如何使用信号量,信号量Semaphore

Java 并发 线程的生命周期

Java 并发 线程的生命周期 @author ixenos 线程的生命周期 线程状态: a)     New 新建 b)     Runnable 可运行 c)     Running 运行 (调用getState()时显示为Runnable) d)     Blocked 阻塞 i.          I/O阻塞 (不释放锁) I/O操作完成解除阻塞,进入Runnable状态 ii.          同步阻塞(不释放锁) 运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会

转---秒杀多线程第八篇 经典线程同步 信号量Semaphore

阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event> <秒杀多线程第七篇经典线程同步互斥量Mutex> 前面介绍了关键段CS.事件Event.互斥量Mutex在经典线程同步问题中的使用.本篇介绍用信号量Semaphore来解决这个问题. 首先也来看看如何使用信号量,信号量Semaphore常用有三个函数,使用很方便.下面是这几个函数的原型和使

Java 并发 线程属性

Java 并发 线程属性 @author ixenos 线程优先级 1.每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程 2.默认情况下,一个线程继承它的父线程的优先级 当在一个运行的线程A里,创建另一个线程B的时候,那么A是父线程,B是子线程.当在一个运行的线程A里,创建线程B,然后又创建了线程C,这时候虽然B比C创建早,可是B并不是C的父线程,而A是B和C的父线程. 3.线程的优先级高度依赖于系统,当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台

Java 并发 线程的优先级

Java 并发 线程的优先级 @author ixenos 低优先级线程的执行时刻 1.在任意时刻,当有多个线程处于可运行状态时,运行系统总是挑选一个优先级最高的线程执行,只有当线程停止.退出或者由于某些原因不执行的时候,低优先级的线程才可能被执行 2.两个优先级相同的线程同时等待执行时,那么运行系统会以round-robin的方式选择一个线程执行(即轮询调度,以该算法所定的)(Java的优先级策略是抢占式调度!) 3.被选中的线程可因为一下原因退出,而给其他线程执行的机会: 1) 一个更高优先