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

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

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

我的解释是这样子的:

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

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

1. Semaphore基本使用

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

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 
=
 
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,不过效率就不行了。

Semaphore 就聊这么多~ 这里有技术、有段子、有生活、也有资源,来吧,还等什么呢~

更多推荐阅读:

微服务架构盛行的时代,你需要了解点 Spring Boot 【并发技术13】条件阻塞Condition的应用 【并发技术12】线程锁技术的使用 知道程序猿为什么没有女朋友吗?真正的原因在这~ 读一篇故事,交一个朋友~