Java限流策略

概要

在大数据量高并发访问时,经常会出现服务或接口面对暴涨的请求而不可用的情况,甚至引发连锁反映导致整个系统崩溃。此时你需要使用的技术手段之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。在限流时,常见的两种算法是漏桶和令牌桶算法算法。

限流算法

令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法。

1. 令牌桶算法

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。

令牌桶算法示例:

public class RateLimiterDemo {

    private static RateLimiter limiter = RateLimiter.create(5);

 

    public static void exec() {

        limiter.acquire(1);

        try {

            // 处理核心逻辑

            TimeUnit.SECONDS.sleep(1);

            System.out.println("--" + System.currentTimeMillis() / 1000);

        catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}

 

Guava RateLimiter 提供了令牌桶算法可用于平滑突发限流策略。
该示例为每秒中产生5个令牌,每200毫秒会产生一个令牌。
limiter.acquire() 表示消费一个令牌。当桶中有足够的令牌时,则直接返回0,否则阻塞,直到有可用的令牌数才返回,返回的值为阻塞的时间。

2. 漏桶算法

它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量,数据可以以任意速度流入到漏桶中。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶为空,则不需要流出水滴,如果漏桶(包缓存)溢出,那么水滴会被溢出丢弃。

3. 计数器限流算法

计数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。

使用计数器限流示例1:

public class CountRateLimiterDemo1 {

 

    private static AtomicInteger count = new AtomicInteger(0);

 

    public static void exec() {

        if (count.get() >= 5) {

            System.out.println("请求用户过多,请稍后在试!"+System.currentTimeMillis()/1000);

        else {

            count.incrementAndGet();

            try {

                //处理核心逻辑

                TimeUnit.SECONDS.sleep(1);

                System.out.println("--"+System.currentTimeMillis()/1000);

            catch (InterruptedException e) {

                e.printStackTrace();

            finally {

                count.decrementAndGet();

            }

        }

    }

}

使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。

弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求。

使用计数器限流示例2:

public class CountRateLimiterDemo2 {

 

    private static Semaphore semphore = new Semaphore(5);

 

    public static void exec() {

        if(semphore.getQueueLength()>100){

            System.out.println("当前等待排队的任务数大于100,请稍候再试...");

        }

        try {

            semphore.acquire();

            // 处理核心逻辑

            TimeUnit.SECONDS.sleep(1);

            System.out.println("--" + System.currentTimeMillis() / 1000);

        catch (InterruptedException e) {

            e.printStackTrace();

        finally {

            semphore.release();

        }

    }

}

使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。

相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。

Semphore信号量的使用

前言:在多线程环境的同步中,我们为了让每个线程具有同步的作用,经常采用synchronize、reetrantlock等同步手段进行上锁,以便在同一时间只能有一个线程具有访问变量和读写变量的权力。然而假如实际的业务场景是允许一组线程访问(组线程数量有限),如何控制一组线程的同步,如果再采取加锁的方法就有点过犹不及了。那么此时信号量就闪亮登场了,对于一组线程的同步访问,对它来说就是小菜一碟

本篇博客的目录

一:semphore的简介

二:semphore的使用方法

三:使用实例

四:总结

一:semphore的简介

1.1:概念

semphpore是jdk提供的一个并发工具类,它位于java.util.concurrent包下,在jdk中对它是这样定义的:

     一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动,Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。简单解释一下这段概念:它就是说semphpore可以对一组线程进行限定,线程每次访问程序之前必须通过它的acquire()方法进入一个房间,也就是没有调用 acquire()方法,线程是无法读取程序的,然后再通过release()方法释放这个线程,其他线程后面才能进入,而它是否允许是通过计数来完成的。

1,.2:理解

举个通俗的例子,假如我们要从出发地A到目的地B,有一辆车,它只能容纳4个人,而我们一共有10个人要从A到B,这辆车就好比是资源,而人乘车这一行为就是线程访问资源,我们进入车需要车票,车票就可以理解为信号量,一次只能进入4个人,其他人只能等待(线程wait,处于阻塞状态),而到了目的地或者有人中途下车了,此时就是release(),释放信号量,那么等待的人才会获得允许上车,每次进入的时候,都会进行计数。

二:semphore的使用方法

2.1:acquire()方法

 acquire() 
          从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。

 

 acquire(int permits) 
          从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。

 acquire方法提供了两种不同的参数用来重载,它主要用于来控制线程是否允许从阻塞状态到活动状态,有点类似于进入高速公路前的“通行证”,只有调用了这个方法,线程才能从阻塞状态变为被唤醒。而acquire(int permits),则提供了指定数量的线程数用来允许此信号量获取一次进入的数量,这个方法比较常用,它经常被用于控制一组线程进行访问资源

2.2:release()方法

 release() 
          释放一个许可,将其返回给信号量。
 release(int permits) 
          释放给定数目的许可,将其返回到信号量。

 release方法和acquire方法相对应,通acquire方法获取了通行证,那么当使用完资源的时候,出去的时候就要调用release方法进行释放锁,这样其他线程才有资格再调用acquire方法再次获取"通行证”。同样它提供了重载的方法可以允许一次释放很多线程。

三:使用实例

3.1:现在来模拟10个人乘车,一辆车只能容纳4个人,所以一次只能进入4个人,而其他人只能处于阻塞状态,只有获取许可,才能进入车中,当一个人下来的时候其他人才能进入继续乘车

3.2:程序实例

3.2.1:我们先来模拟一个人,定义它的行为乘车和下车,比较简单,下面给出示例代码:

public class Person {

    private static final String suffix="号开始乘车";

    private static final String suffix2="号出来了";

    /**
     * 编号
     */
    private Integer no;

    public Person(Integer no) {
        this.no = no;
    }

    public void riding(){

        StringBuilder stringBuilder = new StringBuilder();

        System.out.println(stringBuilder.append(this.getNo()).append(suffix));

    }

    public void out(){

        StringBuilder stringBuilder = new StringBuilder();

        System.out.println(stringBuilder.append(this.getNo()).append(suffix2));

    }


    public Integer getNo() {
        return no;
    }

    public void setNo(Integer no) {
        this.no = no;
    }
}

3.2.2:再来定义一个乘车的线程,拥有信号量和人两个引用:

public class RideThread implements Runnable {

    private Semaphore semp;

    private Person person;

    public RideThread(Semaphore semp, Person person) {
        this.semp = semp;
        this.person = person;
    }

    public void run() {

        try {

            // 获取许可
            semp.acquire();

            person.riding();

            Thread.sleep((long) (Math.random() * 10000));

            person.out();
            // 访问完后,释放
            semp.release();


        } catch (InterruptedException e) {

            e.printStackTrace();

        }
    };
}

3.2.3:测试类

public class SemphoreTest {

    public static final Integer personNums=10;

    public static void main(String[] args) {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();

        // 只能4个人能同时上车
        final Semaphore semp = new Semaphore(4);

        // 模拟10个人乘车
        for (int index = 0; index < personNums; index++) {

            final int NO = index;

            Person person = new Person(NO);

            exec.submit(new RideThread(semp,person));

        }

        System.out.println(semp.isFair());

        // 退出线程池

        exec.shutdown();
    }
}

3.3:输出结果

"D:\Java\jdk 1.8.0_1\bin\java" -Didea.launcher.port=7532 "-Didea.launcher.bin.path=D:\IntelliJ IDEA 2016.3.4\bin" -Dfile.encoding=GBK -classpath "D:\Java\jdk 1.8.0_1\jre\lib\charsets.jar;D:\Java\jdk 1.8.0_1\jre\lib\deploy.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\access-bridge-32.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\cldrdata.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\dnsns.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\jaccess.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\jfxrt.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\localedata.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\nashorn.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\sunec.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\sunmscapi.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk 1.8.0_1\jre\lib\ext\zipfs.jar;D:\Java\jdk 1.8.0_1\jre\lib\javaws.jar;D:\Java\jdk 1.8.0_1\jre\lib\jce.jar;D:\Java\jdk 1.8.0_1\jre\lib\jfr.jar;D:\Java\jdk 1.8.0_1\jre\lib\jfxswt.jar;D:\Java\jdk 1.8.0_1\jre\lib\jsse.jar;D:\Java\jdk 1.8.0_1\jre\lib\management-agent.jar;D:\Java\jdk 1.8.0_1\jre\lib\plugin.jar;D:\Java\jdk 1.8.0_1\jre\lib\resources.jar;D:\Java\jdk 1.8.0_1\jre\lib\rt.jar;E:\Elas Search\Test\out\production\Test;D:\IntelliJ IDEA 2016.3.4\lib\idea_rt.jar" com.intellij.rt.execution.application.AppMain Semphore.SemphoreTest
1号开始乘车
0号开始乘车
3号开始乘车
2号开始乘车
false
3号出来了
4号开始乘车
2号出来了
5号开始乘车
4号出来了
6号开始乘车
5号出来了
7号开始乘车
1号出来了
8号开始乘车
8号出来了
9号开始乘车
7号出来了
0号出来了
6号出来了
9号出来了

可以看出来最开始先进入4个人乘车,首先4个人获取了许可证,然后后面的都是一个出来,另一个进去,只有当一个线程获取信号量再释放信号量的时候,其它线程才能乘车。这样按照顺序,严格限定每次只有空位的时候其他线程才能访问资源!观察结果,会发现最开始的4个是杂序的(后面进入的顺序是for循环控制的),这是“非公平的”,因为在构造Semphore的时候,没有限定第二个参数isFair,这样默认是非公平的,符合按照顺序来进行线程的访问,假如要公平的话,我们可以指定第二个参数为true,这样构造出来的Semhore就是公平的,很多线程去抢,谁抢到是谁的,我们把第二个参数设置为true来观察一下输出的结果:

是否是公平锁:true
0号开始乘车
1号开始乘车
2号开始乘车
3号开始乘车

执行了很多次,都可以看出来最开始的线程是按照顺序进行的,这就是公平信号量,严格遵守顺序依次执行!

四:总结

      本篇博客讲述了Semphore的使用方法,只是抛砖引玉简单的阐述了几个重要的方法和基本使用,在实际的开发中,会遇到更加复杂的业务场景,如何选择jdk提供给我们的便捷的开发工具,在并发中做到没有脏数据,高效、稳定是我们每个开发者都将追求的目标。

转自:https://www.cnblogs.com/java1024/p/7725632.htmlhttps://www.cnblogs.com/wyq178/p/9611075.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值