【并发技术14】线程同步工具Semaphore的使用

简介: 【并发技术14】线程同步工具Semaphore的使用

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

官方JDK上面对 Semaphore 的解释是这样子的:

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

我的解释是这样子的:

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

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


1. Semaphore基本使用


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

1. public class SemaphoreTest {
2.     public static void main(String[] args) {    
3.         ExecutorService service = Executors.newCachedThreadPool();//使用并发库,创建缓存的线程池
4.         final Semaphore sp = new Semaphore(3);//创建一个Semaphore信号量,并设置最大并发数为3
5. 
6.         //availablePermits() //用来获取当前可用的访问次数
7.         System.out.println("初始化:当前有" + (3 - sp.availablePermits() + "个并发"));
8. 
9.         //创建10个任务,上面的缓存线程池就会创建10个对应的线程去执行
10.         for (int index = 0; index < 10; index++) {
11.             final int NO = index;  //记录第几个任务
12.             Runnable run = new Runnable() {  //具体任务
13.                 public void run() {  
14.                     try {                          
15.                         sp.acquire();  // 获取许可 
16.                         System.out.println(Thread.currentThread().getName() 
17.                             + "获取许可" + "("+NO+")," + "剩余:" + sp.availablePermits());  
18.                         Thread.sleep(1000);  
19.                         // 访问完后记得释放 ,否则在控制台只能打印3条记录,之后线程一直阻塞
20.                         sp.release();  //释放许可
21.                         System.out.println(Thread.currentThread().getName() 
22.                             + "释放许可" + "("+NO+")," + "剩余:" + sp.availablePermits());  
23.                     } catch (InterruptedException e) {  
24.                     }  
25.                 }  
26.             };  
27.             service.execute(run);  //执行任务
28.         }         
29.         service.shutdown(); //关闭线程池
30.     }
31. }

代码结构很容易理解,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 = 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=" + data);
                            data++;
                            System.out.println(Thread.currentThread().getName()
                                    + "执行data自增后:data=" + 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=0

pool-1-thread-3获取许可(2),剩余:0

pool-1-thread-1获取许可(0),剩余:0

pool-1-thread-2执行data自增后:data=1

pool-1-thread-3执行data自增前:data=1

pool-1-thread-3执行data自增后:data=2

pool-1-thread-1执行data自增前:data=2

pool-1-thread-7获取许可(6),剩余:1

pool-1-thread-3释放许可(2),剩余:2

pool-1-thread-1执行data自增后:data=3

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

初始化:当前有0个并发

pool-1-thread-3获取许可(2),剩余:0

pool-1-thread-2获取许可(1),剩余:0

pool-1-thread-3执行data自增前:data=0

pool-1-thread-2执行data自增前:data=0

pool-1-thread-1获取许可(0),剩余:0

pool-1-thread-3执行data自增后:data=1

pool-1-thread-2执行data自增后:data=2

pool-1-thread-7获取许可(6),剩余:0

pool-1-thread-1执行data自增前:data=2

pool-1-thread-8获取许可(7),剩余:0

pool-1-thread-7执行data自增前:data=2

pool-1-thread-2释放许可(1),剩余:1

pool-1-thread-7执行data自增后:data=4

从结果中看,已经很明显了,线程2和3都进去了,然后初始 data 都是0,线程3自增了一下,打印出1是没问题的,但是线程2呢?也自增了一下,却打印出了2。也就是说,线程2在操作数据的前后,数据已经被线程3修改过了,再一次证明 Semaphere 并没有实现对共有数据的同步,在操作公共数据的时候,需要我们自己实现。

Semaphere 中如果设置信号量为1的话,那就说明每次只能一个线程去操作任务,那这样的话也就不存在线程安全问题了,所以如果设置信号量为1的话,就可以去掉那个 synchronized,不过效率就不行了。


相关文章
|
24天前
|
安全 Java 调度
Java语言多线程编程技术深度解析
Java语言多线程编程技术深度解析
291 1
|
3天前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
|
12天前
|
安全 Java API
Java并发基础-启动和终止线程
Java并发基础-启动和终止线程
21 0
|
12天前
|
Java 调度
Java并发基础-线程简介(状态、常用方法)
Java并发基础-线程简介(状态、常用方法)
16 0
|
5天前
|
Java
【技术瑜伽师】Java 线程:修炼生命周期的平衡之道,达到多线程编程的最高境界!
【6月更文挑战第19天】Java多线程编程犹如瑜伽修行,从创建线程开始,如`new Thread(Runnable)`,到启动线程的活跃,用`start()`赋予生命。面对竞争与冲突,借助同步机制保证资源访问的有序,如`synchronized`关键字。线程可能阻塞等待,如同瑜伽的静止与耐心。完成任务后线程终止,整个过程需密切关注状态变换,以求多线程间的和谐与平衡。持续修炼,如同瑜伽般持之以恒,实现高效稳定的多线程程序。
|
5天前
|
Java 开发者
【技术成长日记】Java 线程的自我修养:从新手到大师的生命周期修炼手册!
【6月更文挑战第19天】Java线程之旅,从新手到大师的进阶之路:始于创建线程的懵懂,理解就绪与运行状态的成长,克服同步难题的进阶,至洞悉生命周期的精通。通过实例,展示线程的创建、运行与同步,展现技能的不断提升与升华。
|
5天前
|
监控 程序员 调度
协程实现单线程并发(入门)
协程实现单线程并发(入门)
11 1
|
5天前
|
Java
【技术解码】Java线程的五味人生:新建、就绪、运行、阻塞与死亡的哲学解读!
【6月更文挑战第19天】Java线程生命周期如同人生旅程,经历新建、就绪、运行、阻塞至死亡五阶段。从`new Thread()`的诞生到`start()`的蓄势待发,再到`run()`的全力以赴,线程在代码中奔跑。阻塞时面临挑战,等待资源释放,最终通过`join()`或中断结束生命。线程的每个状态转变,都是编程世界与哲思的交汇点。
|
11天前
|
API
java-多线程-CountDownLatch(闭锁) CyclicBarrier(栅栏) Semaphore(信号量)-
java-多线程-CountDownLatch(闭锁) CyclicBarrier(栅栏) Semaphore(信号量)-
13 1
|
24天前
|
安全 Java
JAVA语言中的多线程编程技术
JAVA语言中的多线程编程技术