Java并发之Semaphore实践

背景

在实际开发中会碰到一个接口特别耗时,需要进行大量的数据处理和计算。如果不限制接口的请求调用,将会有大量的请求阻塞,导致服务器崩溃。如何解决这种问题呢?下面我们一起看下如何通过Semaphore来实现接口的访问限制。

简介

Semaphore(信号量)是java.util.current包中的一个对象。简单来讲就是通过锁的方式去获取信号量,如果获取到则执行,否则不执行

主要应用在控制并发量,一段时间内只允许特定的线程去执行,甚至可以控制并发量为1,也就是同步执行某一个方法

主要方法

  • Semaphore semaphore = new Semaphore(1); 通过这个方式创建信号量对象,初始化参数是许可总数

  • availablePermits 获取当前存在的许可

  • acquire 争取信号,该方式是先通过自旋锁的方式获取信息,直到permits小于0,然后再通过阻塞的方式去等待的获取许可

  • tryAcquire 非阻塞的方式获取信号量,通过自旋锁的方式获取,如果获取失败直接返回

  • tryAcquire(long timeout, TimeUnit unit) 再特定的实际内阻塞,获取信号量,获取失败返回false

  • release 释放许可,需要注意的是该方法可以传一个参数,释放许可的量,这个方法会扩充许可量,最后在try finally中执行,然后判断是否成功获取许可后再释放许可

代码分析和解读

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);
        for (int i = 0; i < 100; i++) {
            MyThread thread = new MyThread(semaphore);
            thread.setName(i + 1+"");
            thread.start();
        }
    }
​
​
    static class MyThread extends Thread {
        Semaphore semaphore;
        public MyThread(Semaphore semaphore){
            this.semaphore = semaphore;
        }
​
​
        @Override
        public void run() {
            boolean acquire = false;
            try {
                int i = semaphore.availablePermits();
                String name = Thread.currentThread().getName();
                if (i < 1) {
                    System.out.println(String.format("线程%s没获取到:%s", name, i));
                    return;
                }
                System.out.println(String.format("线程%s开始争取信号!!:%s", name, i));
                Thread.sleep(1000);
//                if(Integer.valueOf(name)>50){
//                    throw new RuntimeException("当前信号量:"+semaphore.availablePermits());
//                }
                acquire = semaphore.tryAcquire();
                if (acquire) {
                    System.out.println(Thread.currentThread().getName()+"争取到信号:开始执行"+semaphore.availablePermits());
                    Thread.sleep(5000);
                }
                System.out.println(Thread.currentThread().getName() + "  " + semaphore.getQueueLength());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if(acquire){
                    semaphore.release(1);
                }
            }
        }
    }
}

tryAcquire 适合用在一个程序的执行时间特别长

假设在一个非常大的并发的情况下,有大量线程读取availablePermits是大于0的,这个适合都会通过tryAcquire 或acquire方法去获取许可;如果使用acquire去获取,假设这个后续的执行线程需要占用这个许可大量的时间,这就导致其他线程在阻塞状态,这会导致资源得不到释放,高并发情况下会导致内存飙升,甚至僚机

acquire适合用在执行时间特别短,或需要某一个方法有比较高的成功率

acquire方法在获取许可的时候,先是通过自旋的方式获取,然后再是阻塞等待,直到前面的线程执行完成释放许可,在执行时间特别短的程序可以使用这种方式,会有一个比较高的几率获取到许可,然后成功执行程序。如果用tryAcquire的方式,则会出现大量无效的请求

release方法注意 事项

需要确定释放获取到许可了再在finally中释放许可,否则碰到异常的情况,acquire方法没获取到许可,在finally中释放了许可,会导致许可量大大增

生产实践

该模块已经运用到华宇的在线Excel平台中,用来限制数据清洗等耗时的接口请求。

代码展示

这里是通过AOP和Semaphore来限制接口的请求

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLimitAp {
​
    String value() default "";
​
    SysSemaphoreLimitAspect.ModuleSemaphoreEnum semaphore();
}
​

package com.huayu.sys.core.aop;
​
import com.huayu.sys.common.BaseResult;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
​
import java.lang.reflect.Method;
import java.util.concurrent.Semaphore;
​
@Aspect
@Component
@Slf4j
public class SysSemaphoreLimitAspect {
​
​
    @Pointcut("@annotation(com.huayu.sys.core.aop.SysLimitAp)")
    public void logPointCut() {
    }
​
    public final static String SERVICE_BUSY_RESPONSE = "正在处理服务请求,请稍后再试";
​
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        SysLimitAp annotation = method.getAnnotation(SysLimitAp.class);
        if (annotation == null) {
            throw new IllegalArgumentException("annotation不能为空");
        }
        ModuleSemaphoreEnum semaphoreEnum = annotation.semaphore();
        if (semaphoreEnum == null) {
            throw new IllegalArgumentException("semaphoreEnum参数不能为空");
        }
        Semaphore semaphore = semaphoreEnum.semaphore;
        int availablePermits=semaphore.availablePermits();
        if(availablePermits<1){
            return BaseResult.initBaseResult(SERVICE_BUSY_RESPONSE);
        }
        boolean hasAcquired = false;
        try {
            hasAcquired = semaphore.tryAcquire(1);
            if(hasAcquired){
                Object result = point.proceed();
                return result;
            }else{
                return BaseResult.initBaseResult(SERVICE_BUSY_RESPONSE);
            }
        }  finally {
            if (hasAcquired) {
                semaphore.release(1);
            }
        }
    }
​
​
    /**
     * 实例枚举
     */
    public enum ModuleSemaphoreEnum {
        SEMAPHORE_APPLY_SNAPSHOT(1),
        SEMAPHORE_PUSH_REPORT(1),
        SEMAPHORE_PUSH_EXPAND_REPORT(1);
​
        public int permits;
        public Semaphore semaphore;
​
        ModuleSemaphoreEnum(int permits){
            this.permits = permits;
            this.semaphore = new Semaphore(permits);
        }
    }
​
}
​

具体使用方式:

@SysLimitAp(semaphore = SysSemaphoreLimitAspect.ModuleSemaphoreEnum.SEMAPHORE_PUSH_REPORT)

在接口上使用该注解,我这里使用的是信号量是1,同一时间只允许1个线程去调用该接口。

如果第一次请求没结束,再次访问该接口,可以给前端返回【服务器繁忙】等提示信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值