(转)JVM调优总结 -Xms -Xmx -Xmn -Xss

  1. 堆大小设置
    JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
    典型设置:

    • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
      Xmx3550m:设置JVM最大可用内存为3550M。
      -Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
      -Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
      -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
    • java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
      -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
      -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
      -XX:MaxPermSize=16m:设置持久代大小为16m。
      -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
  2. 回收器选择
    JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

    1. 吞吐量优先的并行收集器
      如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
      典型配置

      • java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
        -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
        -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
        -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
        -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
        -XX:+UseAdaptiveSizePolicy
        :设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
    2. 响应时间优先的并发收集器
      如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
      典型配置

      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
        -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
        -XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
        -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
        -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
  3. 辅助信息
    JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

    • -XX:+PrintGC
      输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]                [Full GC 121376K->10414K(130112K), 0.0650971 secs]
    • -XX:+PrintGCDetails
      输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]                [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
    • -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
      输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
    • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
      输出形式:Application time: 0.5291524 seconds
    • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
      输出形式:Total time for which application threads were stopped: 0.0468229 seconds
    • -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
      输出形式:
      34.702: [GC {Heap before gc invocations=7:
      def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
      from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
      to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
      tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
      compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
      the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
      ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
      rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
       def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
      from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
      to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
      tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
      compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
      the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
      ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
      rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      }
      , 0.0757599 secs]
    • -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
  4. 常见配置汇总
    1. 堆设置
      • -Xms:初始堆大小
      • -Xmx:最大堆大小
      • -XX:NewSize=n:设置年轻代大小
      • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
      • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
      • -XX:MaxPermSize=n:设置持久代大小
    2. 收集器设置
      • -XX:+UseSerialGC:设置串行收集器
      • -XX:+UseParallelGC:设置并行收集器
      • -XX:+UseParalledlOldGC:设置并行年老代收集器
      • -XX:+UseConcMarkSweepGC:设置并发收集器
    3. 垃圾回收统计信息
      • -XX:+PrintGC
      • -XX:+PrintGCDetails
      • -XX:+PrintGCTimeStamps
      • -Xloggc:filename
    4. 并行收集器设置
      • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
      • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
      • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    5. 并发收集器设置
      • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
      • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

四、调优总结

  1. 年轻代大小选择
    • 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  2. 年老代大小选择
    • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
      • 并发垃圾收集信息
      • 持久代并发收集次数
      • 传统GC信息
      • 花在年轻代和年老代回收上的时间比例

      减少年轻代和年老代花费的时间,一般会提高应用的效率

    • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
  3. 较小堆引起的碎片问题
    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

    • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

SimpleDateFormat的线程安全问题与解决方案

1. 原因

SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,你会发现有如下的调用:

复制代码
Date parse() {

  calendar.clear(); // 清理calendar

  ... // 执行一些操作, 设置 calendar 的日期什么的

  calendar.getTime(); // 获取calendar的时间

}
复制代码

这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date

 

2. 问题重现

复制代码
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author zhenwei.liu created on 2013 13-8-29 下午5:35
 * @version $Id$
 */
public class DateFormatTest extends Thread {
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    private String name;
    private String dateStr;
    private boolean sleep;

    public DateFormatTest(String name, String dateStr, boolean sleep) {
        this.name = name;
        this.dateStr = dateStr;
        this.sleep = sleep;
    }

    @Override
    public void run() {

        Date date = null;

        if (sleep) {
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            date = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        System.out.println(name + " : date: " + date);
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executor = Executors.newCachedThreadPool();

        // A 会sleep 2s 后开始执行sdf.parse()
        executor.execute(new DateFormatTest("A", "1991-09-13", true));
        // B 打了断点,会卡在方法中间
        executor.execute(new DateFormatTest("B", "2013-09-13", false));

        executor.shutdown();
    }
}
复制代码

使用Debug模式执行这段代码,并在sdf.parse()方法里打上断点

复制代码
parse() {

    calendar.clear()

    // 这里打一个断点

    calendar.getTime()

}
复制代码

过程:

1) 首先A线程跑起来以后会进入sleep

2) B线程跑起来, 卡在断点处

3) A线程醒过来, 执行 calendar.clear(), 并将设置sdf.calendar的date为1991-09-13, 此时 A B 的 calendar 都为 1991-09-13

4) 让断点继续执行, 输出如下

A : date: Fri Sep 13 00:00:00 CDT 1991
B : date: Fri Sep 13 00:00:00 CDT 1991

这并不是我们期待的结果

 

 

3. 解决方案

最简单的解决方案我们可以把static去掉,这样每个新的线程都会有一个自己的sdf实例,从而避免线程安全的问题

然而,使用这种方法,在高并发的情况下会大量的new sdf以及销毁sdf,这样是非常耗费资源的

在并发情况下,网站的请求任务与线程执行情况大概可以理解为如下

例如Tomcat的线程池的最大Thread数为4, 现在需要执行的任务有1000个(理解为有1000个用户点了你的网站的某个功能),

而这1000个任务都会用到我们写的日期函数处理类

A) 假如说日期函数处理类使用的是new SimpleDateFormat的方法,那么这里就会有1000次sdf的创建和销毁

B) Java中提供了一种ThreadLocal的解决方案,它的工作方式是,每个线程只会有一个实例,也就是说我们执行完这1000个任务,总共只会实例化4个sdf.

而且,它并不会有多线程的并发问题,因为,单个线程执行任务肯定是顺序的,例如Thread #1负责执行Task #1-#250, 那么他是顺序而执行Task #1-#250

而Thread #2拥有自己的sdf实例,他也是顺序执行任务 Task #251-#500, 以此类推

下面是一个使用ThreadLocal解决sdf多线程问题的例子

复制代码
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author zhenwei.liu created on 2013 13-8-29 下午5:35
 * @version $Id$
 */
public class DateUtil {

    /** 锁对象 */
    private static final Object lockObj = new Object();

    /** 存放不同的日期模板格式的sdf的Map */
    private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();

    /**
     * 返回一个ThreadLocal的sdf,每个线程只会new一次sdf
     * 
     * @param pattern
     * @return
     */
    private static SimpleDateFormat getSdf(final String pattern) {
        ThreadLocal<SimpleDateFormat> tl = sdfMap.get(pattern);

        // 此处的双重判断和同步是为了防止sdfMap这个单例被多次put重复的sdf
        if (tl == null) {
            synchronized (lockObj) {
                tl = sdfMap.get(pattern);
                if (tl == null) {
                    // 只有Map中还没有这个pattern的sdf才会生成新的sdf并放入map
                    System.out.println("put new sdf of pattern " + pattern + " to map");

                    // 这里是关键,使用ThreadLocal<SimpleDateFormat>替代原来直接new SimpleDateFormat
                    tl = new ThreadLocal<SimpleDateFormat>() {

                        @Override
                        protected SimpleDateFormat initialValue() {
                            System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern);
                            return new SimpleDateFormat(pattern);
                        }
                    };
                    sdfMap.put(pattern, tl);
                }
            }
        }

        return tl.get();
    }

    /**
     * 是用ThreadLocal<SimpleDateFormat>来获取SimpleDateFormat,这样每个线程只会有一个SimpleDateFormat
     * 
     * @param date
     * @param pattern
     * @return
     */
    public static String format(Date date, String pattern) {
        return getSdf(pattern).format(date);
    }

    public static Date parse(String dateStr, String pattern) throws ParseException {
        return getSdf(pattern).parse(dateStr);
    }

}
复制代码

测试类

复制代码
import java.text.ParseException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author zhenyu.nie created on 2013 13-8-26 下午2:13
 * @version 1.0.0
 */
public class Test {

    public static void main(String[] args) {
        final String patten1 = "yyyy-MM-dd";
        final String patten2 = "yyyy-MM";

        Thread t1 = new Thread() {

            @Override
            public void run() {
                try {
                    DateUtil.parse("1992-09-13", patten1);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread() {

            @Override
            public void run() {
                try {
                    DateUtil.parse("2000-09", patten2);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t3 = new Thread() {

            @Override
            public void run() {
                try {
                    DateUtil.parse("1992-09-13", patten1);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t4 = new Thread() {

            @Override
            public void run() {
                try {
                    DateUtil.parse("2000-09", patten2);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t5 = new Thread() {

            @Override
            public void run() {
                try {
                    DateUtil.parse("2000-09-13", patten1);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t6 = new Thread() {

            @Override
            public void run() {
                try {
                    DateUtil.parse("2000-09", patten2);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        };

        System.out.println("单线程执行: ");
        ExecutorService exec = Executors.newFixedThreadPool(1);
        exec.execute(t1);
        exec.execute(t2);
        exec.execute(t3);
        exec.execute(t4);
        exec.execute(t5);
        exec.execute(t6);
        exec.shutdown();

        sleep(1000);

        System.out.println("双线程执行: ");
        ExecutorService exec2 = Executors.newFixedThreadPool(2);
        exec2.execute(t1);
        exec2.execute(t2);
        exec2.execute(t3);
        exec2.execute(t4);
        exec2.execute(t5);
        exec2.execute(t6);
        exec2.shutdown();
    }

    private static void sleep(long millSec) {
        try {
            TimeUnit.MILLISECONDS.sleep(millSec);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

输出

单线程执行:
put new sdf of pattern yyyy-MM-dd to map
thread: Thread[pool-1-thread-1,5,main] init pattern: yyyy-MM-dd
put new sdf of pattern yyyy-MM to map
thread: Thread[pool-1-thread-1,5,main] init pattern: yyyy-MM
双线程执行:
thread: Thread[pool-2-thread-1,5,main] init pattern: yyyy-MM-dd
thread: Thread[pool-2-thread-2,5,main] init pattern: yyyy-MM
thread: Thread[pool-2-thread-1,5,main] init pattern: yyyy-MM
thread: Thread[pool-2-thread-2,5,main] init pattern: yyyy-MM-dd

从输出我们可以看出:

1) 1个线程执行这6个任务的时候,这个线程首次使用过的时候会new一个新的sdf,并且以后都一直用这个sdf,而不是每次处理任务都新建一个新的sdf

2) 2个线程执行6个任务的时候也是同理,但是2个线程的sdf是分开的,每个线程都有自己的”yyyy-MM-dd”, “yyyy-MM”的sdf,所以他们不会有线程安全安全问题

试想,如果使用的是new的实现方法,那么不管是用1个线程去执行,还是用2个线程去执行这6个任务,都需要new 6个sdf

Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

摘要  在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法。   以下是本文目录大纲:   一.CountDownLatch用法   二.CyclicBarri…

  在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法。

以下是本文目录大纲:

一.CountDownLatch用法

二.CyclicBarrier用法

三.Semaphore用法

若有不正之处请多多谅解,并欢迎批评指正。

请尊重作者劳动成果,转载请标明原文链接:

http://www.cnblogs.com/dolphin0520/p/3920397.html

 

一.CountDownLatch用法

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

CountDownLatch类只提供了一个构造器:

1
public CountDownLatch(int count) {  };  //参数count为计数值

然后下面这3个方法是CountDownLatch类中最重要的方法:

1
2
3
public void await() throws InterruptedException { };   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown() { };  //将count值减1

下面看一个例子大家就清楚CountDownLatch的用法了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Test {
     public static void main(String[] args) {   
         final CountDownLatch latch = new CountDownLatch(2);
         
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
         
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                     Thread.sleep(3000);
                     System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                     latch.countDown();
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
         
         try {
             System.out.println("等待2个子线程执行完毕...");
            latch.await();
            System.out.println("2个子线程已经执行完毕");
            System.out.println("继续执行主线程");
        catch (InterruptedException e) {
            e.printStackTrace();
        }
     }
}

执行结果:

复制代码
线程Thread-0正在执行
线程Thread-1正在执行
等待2个子线程执行完毕...
线程Thread-0执行完毕
线程Thread-1执行完毕
2个子线程已经执行完毕
继续执行主线程
复制代码

二.CyclicBarrier用法

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器:

1
2
3
4
5
public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。

然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:

1
2
public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;

第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。

下面举几个例子就明白了:

假若有若干个线程都要进行写数据操作,并且只有所有线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处理其他任务...");
        }
    }
}

执行结果:

复制代码
线程Thread-0正在写入数据...
线程Thread-3正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
复制代码

从上面输出结果可以看出,每个写入线程执行完写数据操作之后,就在等待其他线程写入操作完毕。

当所有线程线程写入操作完毕之后,所有线程就继续进行后续的操作了。

如果说想在所有线程写入操作完之后,进行额外的其他操作可以为CyclicBarrier提供Runnable参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N,new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程"+Thread.currentThread().getName());   
            }
        });
        
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处理其他任务...");
        }
    }
}

运行结果:

复制代码
线程Thread-0正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2正在写入数据...
线程Thread-3正在写入数据...
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
当前线程Thread-3
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
复制代码

从结果可以看出,当四个线程都到达barrier状态后,会从四个线程中选择一个线程去执行Runnable。

下面看一下为await指定时间的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        
        for(int i=0;i<N;i++) {
            if(i<N-1)
                new Writer(barrier).start();
            else {
                try {
                    Thread.sleep(5000);
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
                new Writer(barrier).start();
            }
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                try {
                    cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
                catch (TimeoutException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有线程写入完毕,继续处理其他任务...");
        }
    }
}

执行结果:

复制代码
线程Thread-0正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-3正在写入数据...
java.util.concurrent.TimeoutException
Thread-1所有线程写入完毕,继续处理其他任务...
Thread-0所有线程写入完毕,继续处理其他任务...
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
Thread-2所有线程写入完毕,继续处理其他任务...
java.util.concurrent.BrokenBarrierException
线程Thread-3写入数据完毕,等待其他线程写入完毕
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
Thread-3所有线程写入完毕,继续处理其他任务...
复制代码

上面的代码在main方法的for循环中,故意让最后一个线程启动延迟,因为在前面三个线程都达到barrier之后,等待了指定的时间发现第四个线程还没有达到barrier,就抛出异常并继续执行后面的任务。

另外CyclicBarrier是可以重用的,看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
        
        try {
            Thread.sleep(25000);
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("CyclicBarrier重用");
        
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
            
                cyclicBarrier.await();
            catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有线程写入完毕,继续处理其他任务...");
        }
    }
}

执行结果:

复制代码
线程Thread-0正在写入数据...
线程Thread-1正在写入数据...
线程Thread-3正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
Thread-0所有线程写入完毕,继续处理其他任务...
Thread-3所有线程写入完毕,继续处理其他任务...
Thread-1所有线程写入完毕,继续处理其他任务...
Thread-2所有线程写入完毕,继续处理其他任务...
CyclicBarrier重用
线程Thread-4正在写入数据...
线程Thread-5正在写入数据...
线程Thread-6正在写入数据...
线程Thread-7正在写入数据...
线程Thread-7写入数据完毕,等待其他线程写入完毕
线程Thread-5写入数据完毕,等待其他线程写入完毕
线程Thread-6写入数据完毕,等待其他线程写入完毕
线程Thread-4写入数据完毕,等待其他线程写入完毕
Thread-4所有线程写入完毕,继续处理其他任务...
Thread-5所有线程写入完毕,继续处理其他任务...
Thread-6所有线程写入完毕,继续处理其他任务...
Thread-7所有线程写入完毕,继续处理其他任务...
复制代码

从执行结果可以看出,在初次的4个线程越过barrier状态后,又可以用来进行新一轮的使用。而CountDownLatch无法进行重复使用。

三.Semaphore用法

Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

Semaphore类位于java.util.concurrent包下,它提供了2个构造器:

1
2
3
4
5
6
public Semaphore(int permits) {          //参数permits表示许可数目,即同时可以允许多少线程进行访问
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

下面说一下Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:

1
2
3
4
public void acquire() throws InterruptedException {  }     //获取一个许可
public void acquire(int permits) throws InterruptedException { }    //获取permits个许可
public void release() { }          //释放一个许可
public void release(int permits) { }    //释放permits个许可

acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。

release()用来释放许可。注意,在释放许可之前,必须先获获得许可。

这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

1
2
3
4
public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

另外还可以通过availablePermits()方法得到可用的许可数目。

下面通过一个例子来看一下Semaphore的具体使用:

假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {
    public static void main(String[] args) {
        int N = 8;            //工人数
        Semaphore semaphore = new Semaphore(5); //机器数目
        for(int i=0;i<N;i++)
            new Worker(i,semaphore).start();
    }
    
    static class Worker extends Thread{
        private int num;
        private Semaphore semaphore;
        public Worker(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }
        
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("工人"+this.num+"占用一个机器在生产...");
                Thread.sleep(2000);
                System.out.println("工人"+this.num+"释放出机器");
                semaphore.release();           
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:

复制代码
工人0占用一个机器在生产...
工人1占用一个机器在生产...
工人2占用一个机器在生产...
工人4占用一个机器在生产...
工人5占用一个机器在生产...
工人0释放出机器
工人2释放出机器
工人3占用一个机器在生产...
工人7占用一个机器在生产...
工人4释放出机器
工人5释放出机器
工人1释放出机器
工人6占用一个机器在生产...
工人3释放出机器
工人7释放出机器
工人6释放出机器
复制代码

下面对上面说的三个辅助类进行一个总结:

1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
云栖社区站内文章,如需转载,请保留作者和出处(云栖社区),并邮件通知云栖社区(yqeditor@list.alibaba-inc.com)。

(转)Java常见的几种内存溢出及解决方法

Java常见的几种内存溢出及解决方法【情况一】:
java.lang.OutOfMemoryError:Javaheapspace:这种是java堆内存不够,一个原因是真不够(如递归的层数太多等),另一个原因是程序中有死循环;
如果是java堆内存不够的话,可以通过调整JVM下面的配置来解决:
-Xms3062m
-Xmx3062m
【情况二】
java.lang.OutOfMemoryError:GCoverheadlimitexceeded
【解释】:JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。
【解决方案】:
1、查看系统是否有使用大内存的代码或死循环;
2、通过添加JVM配置,来限制使用内存:
-XX:-UseGCOverheadLimit
【情况三】:
java.lang.OutOfMemoryError:PermGenspace:这种是P区内存不够,可通过调整JVM的配置:
-XX:MaxPermSize=128m
-XXermSize=128m
【注】:
JVM的Perm区主要用于存放Class和Meta信息的,Class在被Loader时就会被放到PermGenspace,这个区域成为年老代,GC在主程序运行期间不会对年老区进行清理,默认是64M大小,当程序需要加载的对象比较多时,超过64M就会报这部分内存溢出了,需要加大内存分配,一般128m足够。
【情况四】:
java.lang.OutOfMemoryError:Directbuffermemory
调整-XX:MaxDirectMemorySize=参数,如添加JVM配置:
-XX:MaxDirectMemorySize=128m
【情况五】:
java.lang.OutOfMemoryError:unabletocreatenewnativethread
【原因】:Stack空间不足以创建额外的线程,要么是创建的线程过多,要么是Stack空间确实小了。
【解决】:由于JVM没有提供参数设置总的stack空间大小,但可以设置单个线程栈的大小;而系统的用户空间一共是3G,除了Text/Data/BSS/MemoryMapping几个段之外,Heap和Stack空间的总量有限,是此消彼长的。因此遇到这个错误,可以通过两个途径解决:1.通过-Xss启动参数减少单个线程栈大小,这样便能开更多线程(当然不能太小,太小会出现StackOverflowError);2.通过-Xms-Xmx两参数减少Heap大小,将内存让给Stack(前提是保证Heap空间够用)。
【情况六】:
java.lang.StackOverflowError
【原因】:这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。
【解决】:优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小。

线程池调整真的很重要

来源:blog.bramp.net

翻译:ImportNew

链接:http://www.importnew.com/17633.html

知道吗,你的Java web应用其实是使用线程池来处理请求的。这一实现细节被许多人忽略,但是你迟早都需要理解线程池如何使用,以及如何正确地根据应用调整线程池配置。这篇文章的目的是为了解释线程模型——什么是线程池、以及怎样正确地配置线程池。

单线程模型

让我们从一些基础的线程模型开始,然后再随着线程模型的演变进行更深一步的学习。你使用的任何应用服务器或框架,如Tomcat、Dropwizard、Jetty等,它们的基本原理其实是相同的。Web服务器的最底层实际上是一个socket。这个socket监听并接受到达的TCP连接。一旦一个连接被建立,就可以通过这个新建立的连接读取、解析信息,然后将这些信息包装成一个HTTP请求。这个HTTP请求还将被移交至web应用程序,来完成请求的动作。

我们将通过一个简单的服务器程序来展示线程在其中所起到的作用。这个服务器程序展示了大部分应用服务器的底层实现细节。让我们以一个简单的单线程web服务器程序开始,它的代码像下面这样:

ServerSocket listener = new ServerSocket(8080);

try {

while (true) {

Socket socket = listener.accept();

try {

handleRequest(socket);

} catch (IOException e) {

e.printStackTrace();

}

}

} finally {

listener.close();

}

这段代码在8080端口上创建了一个ServerSocket,紧接着通过循环来监听和接受新到达的连接。一旦连接建立,会将socket传递给handleRequest方法。这个方法可能会读取该HTTP请求,处理这个请求,然后写回一个响应。在这个简单的例子中,handleRequest方法从socket中读取简单的一行数据,然后返回一个简短的HTTP响应。但是,handleRequest有可能需要处理一些更复杂的任务,例如读数据库或者执行其它一些IO操作。

final static String response =

“HTTP/1.0 200 OKrn” +

“Content-type: text/plainrn” +

“rn” +

“Hello Worldrn”;

 

public static void handleRequest(Socket socket) throws IOException {

// Read the input stream, and return “200 OK”

try {

BufferedReader in = new BufferedReader(

new InputStreamReader(socket.getInputStream()));

 

log.info(in.readLine());

 

OutputStream out = socket.getOutputStream();

out.write(response.getBytes(StandardCharsets.UTF_8));

} finally {

socket.close();

}

}

因为只有一个线程处理所有的socket,因此只有在完全处理好一个请求后,才能再接受下一个请求。在实际的应用中,handleRequest方法可能需要经过100毫秒才能返回,那么这个服务器程序在一秒中,只能按顺序处理10个请求。

多线程模型

尽管handleRequest可能会被IO操作阻塞,CPU却可能是空闲的,它可以处理其它更多请求,但这对单线程模型来说是不能实现的。因此,通过创建多个线程,可以使服务器程序实现并发操作:

public static class HandleRequestRunnable implements Runnable {

final Socket socket;

 

public HandleRequestRunnable(Socket socket) {

this.socket = socket;

}

 

public void run() {

try {

handleRequest(socket);

} catch (IOException e) {

e.printStackTrace();

}

}

}

 

// Main loop here

ServerSocket listener = new ServerSocket(8080);

try {

while (true) {

Socket socket = listener.accept();

new Thread( new HandleRequestRunnable(socket) ).start();

}

} finally {

listener.close();

}

上面这段代码中,accept()方法仍然是在一个单线程循环中被调用。但是当TCP连接建立,socket创建时,服务器就创建一个新的线程。这个新生的线程将执行和单线程模型中一样的handleRequest方法。

新线程的建立使调用accept方法的线程能够处理更多的TCP连接,这样服务器就能并发地处理请求了。这一技术被称为“thread per request”(一个线程处理一个请求),也是现在最流行的服务器技术。值得注意的是,还有一些其它的服务器技术,如NGINX和Node.js采用的事件驱动异步模型,它们都没有使用线程池。因此,它们都不在本文的讨论范围内。

“thread per request”方式里创建新线程(稍后销毁这个线程)的操作是昂贵的,因为Java虚拟机和操作系统都需要为这一操作分配资源。另外,在上面那段代码的中,可以创建的线程数量是不受限制的。这么做的隐患很大,因为它可能导致服务器资源迅速枯竭。

资源枯竭

每个线程都需要一定的内存空间来作为自己的栈空间。在最近的64位虚拟机版本中,默认的栈空间是1024KB。如果server收到很多请求,或者handleRequest方法的执行时间变得比较长,就会造成服务器产生很多并发线程。如果要维护1000个线程,仅就栈空间而言,虚拟机就必须耗费1GB的RAM空间。另外,为处理请求,每个线程都会在堆上产生许多对象,这就有可能导致虚拟机的堆空间被迅速占满,给虚拟机的垃圾收集器带来很大压力,造成频繁的垃圾回收,最终导致OutOfMemoryErrors。

线程消耗的不仅是RAM资源,这些线程还可能消耗其它有限的资源,例如文件句柄、数据库连接等。过多地消耗这类资源可能导致一些其它的错误或造成系统崩溃。因此,要防止系统资源被线程耗尽,就必须对服务器产生的线程数量做出限制。

通过使用-Xss参数来调整每个线程的栈空间,可以在一定程度上解决资源枯竭的问题,但它绝不是灵丹妙药。一个小的栈空间可以使得每个线程占用的内存减小,但这样可能会造成StackOverflowErrors栈溢出错误。栈空间的调整方式不尽相同,但是对许多应用来说,1024KB过于浪费了,而256KB或512KB会更加合适。Java所允许的最小栈空间的大小是160KB。

线程池

可以通过一个简单的线程池来避免持续地创建新线程,限制最大线程数量。线程池跟踪着所有线程,在线程数量达到上限前,它会创建新的线程,当有空闲线程时,它会使用空闲线程。

ServerSocket listener = new ServerSocket(8080);

ExecutorService executor = Executors.newFixedThreadPool(4);

try {

while (true) {

Socket socket = listener.accept();

executor.submit( new HandleRequestRunnable(socket) );

}

} finally {

listener.close();

}

上面这段代码使用了ExecutorService类来提交任务(Runnable)。提交的任务将会被线程池中的线程执行,而不是通过新创建的线程执行。在这个例子中,所有的请求都通过一个线程数量固定为4的线程池来完成。这个线程池限制了并发执行的请求数量,从而限制了系统资源的使用。

除了newFixedThreadPool方法创建的线程池外,Executors类还提供了newCachedThreadPool 方法来创建线程池。这种线程池同样有无法限制线程数量的问题,但是它会优先使用线程池中已创建的空闲线程来处理请求。这种类型的线程池特别适用于执行短期任务的请求,因为它们不会长时间的阻塞外部资源。

ThreadPoolExecutors 类也可以直接创建,这样就可以对它进行一些个性化的配置。例如可以配置线程池内最小线程数和最大线程数,也可以配置线程创建和销毁的策略。稍后,本文将介绍这样的例子。

工作队列

对于线程数量固定的线程池,善于观察的读者可能会提出这样的一个疑问:当线程池中的线程都在工作时,一个新的请求到达,会发生什么呢?当线程池中的线程都在工作时,ThreadPoolExecutor可能会使用一个队列来组织新到达的请求,直到线程池中有空闲的线程可以使用。Executors.nexFixedThreadPool方法会默认创建一个没有长度限制的LinkedList。这个LinkedList也可能会产生系统资源耗尽的问题,虽然这个过程会比较缓慢,因为队列中的请求所占用的资源比线程占用的资源要少得多。但是在我们的例子中,队列中的每个请求都保持着一个socket,而每一个socket都需要打开一个文件句柄,操作系统对同时打开的文件句柄数量是有限制的,所以队列中保持socket并不是一个好的方式,除非必须这么做。因此,限制工作队列的长度也是有意义的。

public static ExecutorService newBoundedFixedThreadPool(int nThreads, int capacity) {

return new ThreadPoolExecutor(nThreads, nThreads,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>(capacity),

new ThreadPoolExecutor.DiscardPolicy());

}

 

public static void boundedThreadPoolServerSocket() throws IOException {

ServerSocket listener = new ServerSocket(8080);

ExecutorService executor = newBoundedFixedThreadPool(4, 16);

try {

while (true) {

Socket socket = listener.accept();

executor.submit( new HandleRequestRunnable(socket) );

}

} finally {

listener.close();

}

}

我们再一次创建一个线程池,这一次我们没有使用Executors.newFixedThreadPool方法,而是自定义了一个ThreadPoolExecutor,在构造方法中传递了一个大小限制为16个元素的LinkedBlockingQueue。同样的,类ArrayBlockingQueue也可以被用来限制队列的长度。

如果所有的线程都在执行任务,而且工作队列也被请求填满了,此时对于新到达请求的处理方式,取决于ThreadPoolExecutor构造方法的最后一个参数。在我们这个例子中,我们使用的是DiscardPolicy,这个参数会让线程池丢弃新到达的请求。还有一些其它的处理策略,例如AbortPolicy会让Executor抛出一个异常,CallerRunsPolicy会使任务在它的调用端线程池中执行。CallerRunsPolicy策略提供了一个简单的方式来限制任务提交的速度。但是这样做可能是有害的,因为它会阻塞一个原本不应被阻塞的线程。

一个好的默认策略应该是Discard或Abort,它们都会使线程池丢弃新到达的任务。这样服务器就能容易地向客户端响应一个错误,例如HTTP的503错误“Service unavailable”。有的人可能会认为,队列的长度应该是允许增长的,这样所有的任务最终都能被执行。但是用户是不愿意长时间等待的,而且若任务到达的速度超过任务处理的速度,队列将会无限地增长。队列是被用来缓冲突然爆发的请求,或者处理短期任务的,通常情况下,队列应该是空的。

多少线程合适呢?

现在,我们知道了如何创建一个线程池。但是有一个更困难的问题,线程池里应该创建多少个线程呢?我们已经知道了线程池中的最大线程数量应该被限制,才不会导致系统资源耗尽。这些系统资源包括了内存(堆栈)、打开的文件句柄、打开的TCP连接、打开的数据库连接以及其它有限的系统资源。相反的,如果线程执行的是CPU密集型任务而不是IO密集型任务,服务器的物理内核数就应该被视为是有限的资源,这样创建的线程数就不应该超过系统的内核数。

系统应创建多少线程取决于这个应用执行的任务。开发人员应使用现实的请求来对系统进行负载测试,测试不同的线程池大小配置对系统的影响。每次测试都增加线程池的大小,直到系统达到崩溃的临界点。这个方法使你可以发现线程池线程数量的上限。超过这个上限,系统的资源将耗尽。在某些情况下,可以谨慎地增加系统的资源,例如分配更多的RAM空间给JVM,或者调整操作系统使其支持同时打开更多的文件句柄。然而,在某些情况下创建的线程数量会达到我们测试出的理论上限,这非常值得我们注意。稍后还会看到这方面的内容。

利特尔法则

排队论,特别的,Little’s Law,可以用来帮助我们理解线程池的一些特性。简单地说,利特尔法则解释了这三种变量的关系:L—系统里的请求数量、λ—请求到达的速率和W—每个请求的处理时间。例如,如果每秒10个请求到达,处理一个请求需要1秒,那么系统在每个时刻都有10个请求在处理。如果处理每个请求的时间翻倍,那么系统每时刻需要处理的请求数也翻倍为20,因此需要20个线程。

任务的执行时间对于系统中正在处理的请求数量有着很大的影响,一些后端资源的迟延,例如数据库,通常会使得请求的处理时间被延长,从而导致线程池中的线程被迅速用尽。因此,理论上测出的线程数上限对于这种情况就不是很合适,这个上限值还应该考虑到线程的执行时间,并结合理论上的上限值。

例如,假设JVM最多能同时处理的请求数为1000。如果我们预计每个请求需要耗费的时间不超过30秒,那么,在最坏的情况下我们每秒能同时处理的请求数不会超过33 ⅓个。但是,如果一切都很顺利,每个请求只需使用500ms就可以完成,那么通过1000个线程应用每秒就可以处理2000个请求。当系统突然出现短暂的任务执行迟延的问题时,通过使用一个队列来减缓这一问题是可行的。

为什么线程数配置不当会带来麻烦?

如果线程池的线程数量过少,我们就无法充分利用系统资源,这使得用户需要花费很长时间来等待请求的响应。但是,如果允许创建过多的线程,系统的资源又会被耗尽,这会对系统造成更大的破坏。

不仅仅是本地的资源被耗尽,其它一些应用也会受到影响。例如,许多应用都使用同一个后端数据库进行查询等操作。数据库有并发连接数量的限制。如果一个应用不加限制地占用了所有数据库连接,其它获取数据库连接的应用都将被阻塞。这将导致许多应用运行中断。

更糟的是,资源耗尽还会引发一些连锁故障。设想这样一个场景,一个应用有许多个实例,这些实例都运行在一个负载均衡器之后。如果一个实例因为过多的请求而占用了过多内存,JVM就需要花更多的时间进行垃圾收集工作,那么JVM处理请求的时间就减少了。这样一来,这个应用实例处理请求的能力降低了,系统中的其它实例就必须处理更多的请求。其它的实例也会因为请求数过多以及线程池大小没有限制的原因产生资源枯竭等问题。这些实例用尽了内存资源,导致虚拟机进行频繁地内存收集操作。这样的恶性循环会在这些实例中产生,直到整个系统奔溃。

我见过许多没有进行负载测试的应用,这些应用能够创建任意多的线程。通常情况下,这些应用只要很少数量的线程就能处理好以一定速率到达的请求。但是,如果应用需要使用其它的一些远程服务来处理用户请求,而这个远程服务的处理能力突然降低了,这将增加【大】W的值(应用处理请求的平均时间)。这样,线程池的线程就会被迅速用尽。如果对应用进行线程数量的负载测试,那么资源枯竭问题就会在测试中显现出来。

多少个线程池合适?

对于微服务架构和面向服务的架构(SOA)来说,它们通常需要请求一些后端服务。线程池的配置非常容易导致程序失败,因此必须谨慎地配置线程池。如果远程服务的性能下降,系统中的线程数量就会迅速达到线程池的上限,其它后续到达的服务就会被丢弃。这些后续的请求也许并不是要使用性能出现故障的服务,但是它们都只能被丢弃了。

针对不同的后端服务请求,设置不同的线程池可以解决这一问题。在这个模式中,仍然使用同一个线程池来处理用户的请求,但是当用户的请求需要调用一个远程服务时,这个任务就被传递给一个指定的后端线程池。这样处理用户请求的主线程池就不会因为调用后端服务而产生很大的负担。当后端服务出现故障时,只有调用这个服务的线程池才会受到影响。

使用多个线程池还有一个好处,就是它能帮助避免出现死锁问题。如果每个空闲线程都因为一个尚未处理完毕的请求阻塞,就会发生死锁,没有一个线程可以继续往下执行。如果使用多个线程池,理解好每个线程池应负责的工作,那么死锁的问题就能在一定程度上避免。

截止时间和一些最佳实践

一个最佳实践是给需要远程调用的请求规定一个截止时间。如果远程服务在规定的时间内没有响应,就丢弃这个请求。这样的技术也可以用在线程池中,如果线程处理某个请求的时间超过了规定时间,那么这个线程就应被停止,为新到达的请求腾出资源,这样也就给W(处理请求的平均时间)规定了上限。虽然这样的做法看起来有些浪费,但是如果一个用户(特别是当用户在使用浏览器时),在等待请求的响应,那么30秒以后,浏览器无论如何也会放弃这个请求,或者更有可能的是:用户不会耐心地等待这个请求响应,而是进行其它操作去了。

快速失败也是一个可以用来处理后端请求的线程池方案。如果后端服务失效了,线程池中的线程数会迅速到达上限,这些线程都在等待没有响应的后端服务。如果使用快速失败机制,当后端服务被标记为失效时,所有的后续请求都会迅速失败,而不是进行不必要的等待。当然,它也需要一种机制来判断后端何时恢复为可用的。

最后,如果一个请求需要独立地调用多个后端服务,那么这个请求就应能并行地调用这些后端服务,而不是顺序地进行。这样就能降低请求的等待时间,但这是以增加线程数为代价的。

幸运的是,有一个非常好的库hystrix,这个库封装了许多很好的线程策略,然后以非常简单和友好的方式将这些借口暴露出来。

总结

我希望这篇文章能改进你对线程池的理解。一个合适的线程池配置需要理解应用的需求,还需要考虑这几个因素,系统允许的最大线程数、处理用户请求所需的时间。好的线程池配置不仅可以避免系统出现连锁故障,还能帮助计划和提供服务。

即使你的应用没有直接地使用一个线程池,它们也间接地通过应用服务器或其它更高级的抽象形式使用了线程池。Tomcat、JBoss、Undertow、Dropwizard 都提供了多种可配置的线程池(这些线程池正是你编写的Servlet运行的地方)。

深入理解Java之线程池(下)

接上文

下面是这三个静态方法的具体实现;

public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>());

}

public static ExecutorService newSingleThreadExecutor() {

return new FinalizableDelegatedExecutorService

(new ThreadPoolExecutor(1, 1,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>()));

}

public static ExecutorService newCachedThreadPool() {

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

60L, TimeUnit.SECONDS,

new SynchronousQueue<Runnable>());

}

从它们的具体实现来看,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了。

newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;

newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;

newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

实际中,如果Executors提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型和数量来进行配置。

另外,如果ThreadPoolExecutor达不到要求,可以自己继承ThreadPoolExecutor类进行重写。

四.如何合理配置线程池的大小

本节来讨论一个比较重要的话题:如何合理配置线程池大小,仅供参考。

一般需要根据任务的类型来配置线程池大小:

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1

如果是IO密集型任务,参考值可以设置为2*NCPU

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

参考资料:

http://ifeve.com/java-threadpool/

http://blog.163.com/among_1985/blog/static/275005232012618849266/

http://developer.51cto.com/art/201203/321885.htm

http://blog.csdn.net/java2000_wl/article/details/22097059

http://blog.csdn.net/cutesource/article/details/6061229

http://blog.csdn.net/xieyuooo/article/details/8718741

《JDK API 1.6》

深入理解Java之线程池(中)

接上文

下面我们看一下Worker类的实现:

private final class Worker implements Runnable {

private final ReentrantLock runLock = new ReentrantLock();

private Runnable firstTask;

volatile long completedTasks;

Thread thread;

Worker(Runnable firstTask) {

this.firstTask = firstTask;

}

boolean isActive() {

return runLock.isLocked();

}

void interruptIfIdle() {

final ReentrantLock runLock = this.runLock;

if (runLock.tryLock()) {

try {

if (thread != Thread.currentThread())

thread.interrupt();

} finally {

runLock.unlock();

}

}

}

void interruptNow() {

thread.interrupt();

}

 

private void runTask(Runnable task) {

final ReentrantLock runLock = this.runLock;

runLock.lock();

try {

if (runState < STOP &&

Thread.interrupted() &&

runState >= STOP)

boolean ran = false;

beforeExecute(thread, task);   //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据

//自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等

try {

task.run();

ran = true;

afterExecute(task, null);

++completedTasks;

} catch (RuntimeException ex) {

if (!ran)

afterExecute(task, ex);

throw ex;

}

} finally {

runLock.unlock();

}

}

 

public void run() {

try {

Runnable task = firstTask;

firstTask = null;

while (task != null || (task = getTask()) != null) {

runTask(task);

task = null;

}

} finally {

workerDone(this);   //当任务队列中没有任务时,进行清理工作

}

}

}

它实际上实现了Runnable接口,因此上面的Thread t = threadFactory.newThread(w);效果跟下面这句的效果基本一样:

Thread t = new Thread(w);

相当于传进去了一个Runnable任务,在线程t中执行这个Runnable。

既然Worker实现了Runnable接口,那么自然最核心的方法便是run()方法了:

public void run() {

try {

Runnable task = firstTask;

firstTask = null;

while (task != null || (task = getTask()) != null) {

runTask(task);

task = null;

}

} finally {

workerDone(this);

}

}

从run方法的实现可以看出,它首先执行的是通过构造器传进来的任务firstTask,在调用runTask()执行完firstTask之后,在while循环里面不断通过getTask()去取新的任务来执行,那么去哪里取呢?自然是从任务缓存队列里面去取,getTask是ThreadPoolExecutor类中的方法,并不是Worker类中的方法,下面是getTask方法的实现:

Runnable getTask() {

for (;;) {

try {

int state = runState;

if (state > SHUTDOWN)

return null;

Runnable r;

if (state == SHUTDOWN)  // Help drain queue

r = workQueue.poll();

else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,

//则通过poll取任务,若等待一定的时间取不到任务,则返回null

r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);

else

r = workQueue.take();

if (r != null)

return r;

if (workerCanExit()) {    //如果没取到任务,即r为null,则判断当前的worker是否可以退出

if (runState >= SHUTDOWN) // Wake up others

interruptIdleWorkers();   //中断处于空闲状态的worker

return null;

}

// Else retry

} catch (InterruptedException ie) {

// On interruption, re-check runState

}

}

}

在getTask中,先判断当前线程池状态,如果runState大于SHUTDOWN(即为STOP或者TERMINATED),则直接返回null。

如果runState为SHUTDOWN或者RUNNING,则从任务缓存队列取任务。

如果当前线程池的线程数大于核心池大小corePoolSize或者允许为核心池中的线程设置空闲存活时间,则调用poll(time,timeUnit)来取任务,这个方法会等待一定的时间,如果取不到任务就返回null。

然后判断取到的任务r是否为null,为null则通过调用workerCanExit()方法来判断当前worker是否可以退出,我们看一下workerCanExit()的实现:

private boolean workerCanExit() {

final ReentrantLock mainLock = this.mainLock;

mainLock.lock();

boolean canExit;

//如果runState大于等于STOP,或者任务缓存队列为空了

//或者  允许为核心池线程设置空闲存活时间并且线程池中的线程数目大于1

try {

canExit = runState >= STOP ||

workQueue.isEmpty() ||

(allowCoreThreadTimeOut &&

poolSize > Math.max(1, corePoolSize));

} finally {

mainLock.unlock();

}

return canExit;

}

也就是说如果线程池处于STOP状态、或者任务队列已为空或者允许为核心池线程设置空闲存活时间并且线程数大于1时,允许worker退出。如果允许worker退出,则调用interruptIdleWorkers()中断处于空闲状态的worker,我们看一下interruptIdleWorkers()的实现:

void interruptIdleWorkers() {

final ReentrantLock mainLock = this.mainLock;

mainLock.lock();

try {

for (Worker w : workers)  //实际上调用的是worker的interruptIfIdle()方法

w.interruptIfIdle();

} finally {

mainLock.unlock();

}

}

从实现可以看出,它实际上调用的是worker的interruptIfIdle()方法,在worker的interruptIfIdle()方法中:

void interruptIfIdle() {

final ReentrantLock runLock = this.runLock;

if (runLock.tryLock()) {    //注意这里,是调用tryLock()来获取锁的,因为如果当前worker正在执行任务,锁已经被获取了,是无法获取到锁的

//如果成功获取了锁,说明当前worker处于空闲状态

try {

if (thread != Thread.currentThread())

thread.interrupt();

} finally {

runLock.unlock();

}

}

}

这里有一个非常巧妙的设计方式,假如我们来设计线程池,可能会有一个任务分派线程,当发现有线程空闲时,就从任务缓存队列中取一个任务交给空闲线程执行。但是在这里,并没有采用这样的方式,因为这样会要额外地对任务分派线程进行管理,无形地会增加难度和复杂度,这里直接让执行完任务的线程去任务缓存队列里面取任务来执行。

我们再看addIfUnderMaximumPoolSize方法的实现,这个方法的实现思想和addIfUnderCorePoolSize方法的实现思想非常相似,唯一的区别在于addIfUnderMaximumPoolSize方法是在线程池中的线程数达到了核心池大小并且往任务队列中添加任务失败的情况下执行的:

private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {

Thread t = null;

final ReentrantLock mainLock = this.mainLock;

mainLock.lock();

try {

if (poolSize < maximumPoolSize && runState == RUNNING)

t = addThread(firstTask);

} finally {

mainLock.unlock();

}

if (t == null)

return false;

t.start();

return true;

}

看到没有,其实它和addIfUnderCorePoolSize方法的实现基本一模一样,只是if语句判断条件中的poolSize < maximumPoolSize不同而已。

到这里,大部分朋友应该对任务提交给线程池之后到被执行的整个过程有了一个基本的了解,下面总结一下:

1)首先,要清楚corePoolSize和maximumPoolSize的含义;

2)其次,要知道Worker是用来起到什么作用的;

3)要知道任务提交给线程池之后的处理策略,这里总结一下主要有4点:

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

3.线程池中的线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。

在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

  • prestartCoreThread():初始化一个核心线程;
  • prestartAllCoreThreads():初始化所有核心线程

下面是这2个方法的实现:

public boolean prestartCoreThread() {

return addIfUnderCorePoolSize(null); //注意传进去的参数是null

}

 

public int prestartAllCoreThreads() {

int n = 0;

while (addIfUnderCorePoolSize(null))//注意传进去的参数是null

++n;

return n;

}

注意上面传进去的参数是null,根据第2小节的分析可知如果传进去的参数为null,则最后执行线程会阻塞在getTask方法中的

r = workQueue.take();

即等待任务队列中有任务。

4.任务缓存队列及排队策略

在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。

workQueue的类型为BlockingQueue<Runnable>,通常可以取下面三种类型:

1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

5.任务拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

6.线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

7.线程池容量的动态调整

ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),

  • setCorePoolSize:设置核心池大小
  • setMaximumPoolSize:设置线程池最大能创建的线程数目大小

当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。

三.使用示例

前面我们讨论了关于线程池的实现原理,这一节我们来看一下它的具体使用:

public class Test {

public static void main(String[] args) {

ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,

new ArrayBlockingQueue<Runnable>(5));

 

for(int i=0;i<15;i++){

MyTask myTask = new MyTask(i);

executor.execute(myTask);

System.out.println(“线程池中线程数目:”+executor.getPoolSize()+”,队列中等待执行的任务数目:”+

executor.getQueue().size()+”,已执行玩别的任务数目:”+executor.getCompletedTaskCount());

}

executor.shutdown();

}

}

 

class MyTask implements Runnable {

private int taskNum;

 

public MyTask(int num) {

this.taskNum = num;

}

 

@Override

public void run() {

System.out.println(“正在执行task “+taskNum);

try {

Thread.currentThread().sleep(4000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“task “+taskNum+”执行完毕”);

}

}

执行结果:

正在执行task 0

线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 1

线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 2

线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 3

线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 4

线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 10

线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 11

线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 12

线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 13

线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 14

task 3执行完毕

task 0执行完毕

task 2执行完毕

task 1执行完毕

正在执行task 8

正在执行task 7

正在执行task 6

正在执行task 5

task 4执行完毕

task 10执行完毕

task 11执行完毕

task 13执行完毕

task 12执行完毕

正在执行task 9

task 14执行完毕

task 8执行完毕

task 5执行完毕

task 7执行完毕

task 6执行完毕

task 9执行完毕

从执行结果可以看出,当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。如果上面程序中,将for循环中改成执行20个任务,就会抛出任务拒绝异常了。

不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:

Executors.newCachedThreadPool();        //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE

Executors.newSingleThreadExecutor();   //创建容量为1的缓冲池

Executors.newFixedThreadPool(int);    //创建固定容量大小的缓冲池

接下文

深入理解Java之线程池(上)

来源:海 子

链接:http://www.cnblogs.com/dolphin0520/p/3932921.html

在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPoolExecutor类中的方法讲起,然后再讲述它的实现原理,接着给出了它的使用示例,最后讨论了一下如何合理配置线程池的大小。

以下是本文的目录大纲:

一.Java中的ThreadPoolExecutor类

二.深入剖析线程池实现原理

三.使用示例

四.如何合理配置线程池的大小

若有不正之处请多多谅解,并欢迎批评指正。

一.Java中的ThreadPoolExecutor类

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。

在ThreadPoolExecutor类中提供了四个构造方法:

public class ThreadPoolExecutor extends AbstractExecutorService {

…..

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue);

 

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);

 

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);

 

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

}

从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

下面解释下一下构造器中各个参数的含义:

  • corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

TimeUnit.DAYS;               //天

TimeUnit.HOURS;             //小时

TimeUnit.MINUTES;           //分钟

TimeUnit.SECONDS;           //秒

TimeUnit.MILLISECONDS;      //毫秒

TimeUnit.MICROSECONDS;      //微妙

TimeUnit.NANOSECONDS;       //纳秒

  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

ArrayBlockingQueue;

LinkedBlockingQueue;

SynchronousQueue;

ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。

  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略,有以下四种取值:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

具体参数的配置与线程池的关系将在下一节讲述。

从上面给出的ThreadPoolExecutor类的代码可以知道,ThreadPoolExecutor继承了AbstractExecutorService,我们来看一下AbstractExecutorService的实现:

public abstract class AbstractExecutorService implements ExecutorService {

 

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };

public Future<?> submit(Runnable task) {};

public <T> Future<T> submit(Runnable task, T result) { };

public <T> Future<T> submit(Callable<T> task) { };

private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,

boolean timed, long nanos)

throws InterruptedException, ExecutionException, TimeoutException {

};

public <T> T invokeAny(Collection<? extends Callable<T>> tasks)

throws InterruptedException, ExecutionException {

};

public <T> T invokeAny(Collection<? extends Callable<T>> tasks,

long timeout, TimeUnit unit)

throws InterruptedException, ExecutionException, TimeoutException {

};

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

throws InterruptedException {

};

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,

long timeout, TimeUnit unit)

throws InterruptedException {

};

}

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。

我们接着看ExecutorService接口的实现:

public interface ExecutorService extends Executor {

 

void shutdown();

boolean isShutdown();

boolean isTerminated();

boolean awaitTermination(long timeout, TimeUnit unit)

throws InterruptedException;

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

throws InterruptedException;

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,

long timeout, TimeUnit unit)

throws InterruptedException;

 

<T> T invokeAny(Collection<? extends Callable<T>> tasks)

throws InterruptedException, ExecutionException;

<T> T invokeAny(Collection<? extends Callable<T>> tasks,

long timeout, TimeUnit unit)

throws InterruptedException, ExecutionException, TimeoutException;

}

而ExecutorService又是继承了Executor接口,我们看一下Executor接口的实现:

public interface Executor {

void execute(Runnable command);

}

到这里,大家应该明白了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor几个之间的关系了。

Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;

然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;

然后ThreadPoolExecutor继承了类AbstractExecutorService。

在ThreadPoolExecutor类中有几个非常重要的方法:

execute()

submit()

shutdown()

shutdownNow()

execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。

shutdown()和shutdownNow()是用来关闭线程池的。

还有很多其他的方法:

比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友可以自行查阅API。

二.深入剖析线程池实现原理

在上一节我们从宏观上介绍了ThreadPoolExecutor,下面我们来深入解析一下线程池的具体实现原理,将从下面几个方面讲解:

1.线程池状态

 

2.任务的执行

3.线程池中的线程初始化

 

4.任务缓存队列及排队策略

 

5.任务拒绝策略

 

6.线程池的关闭

 

7.线程池容量的动态调整

1.线程池状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

volatile int runState;

static final int RUNNING    = 0;

static final int SHUTDOWN   = 1;

static final int STOP       = 2;

static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

下面的几个static final变量表示runState可能的几个取值。

当创建线程池后,初始时,线程池处于RUNNING状态;

如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

2.任务的执行

在了解将任务提交给线程池到任务执行完毕整个过程之前,我们先来看一下ThreadPoolExecutor类中其他的一些比较重要成员变量:

private final BlockingQueue<Runnable> workQueue;              //任务缓存队列,用来存放等待执行的任务

private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小

//、runState等)的改变都要使用这个锁

private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集

 

private volatile long  keepAliveTime;    //线程存货时间

private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间

private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)

private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数

 

private volatile int   poolSize;       //线程池中当前的线程数

 

private volatile RejectedExecutionHandler handler; //任务拒绝策略

 

private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程

 

private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数

 

private long completedTaskCount;   //用来记录已经执行完毕的任务个数

每个变量的作用都已经标明出来了,这里要重点解释一下corePoolSize、maximumPoolSize、largestPoolSize三个变量。

corePoolSize在很多地方被翻译成核心池大小,其实我的理解这个就是线程池的大小。举个简单的例子:

假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。

因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;

当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;

如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;

然后就将任务也分配给这4个临时工人做;

如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。

当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。

这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。

也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。

不过为了方便理解,在本文后面还是将corePoolSize翻译成核心池大小。

largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。

下面我们进入正题,看一下任务从提交到最终执行完毕经历了哪些过程。

在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然通过submit也可以提交任务,但是实际上submit方法里面最终调用的还是execute()方法,所以我们只需要研究execute()方法的实现原理即可:

public void execute(Runnable command) {

if (command == null)

throw new NullPointerException();

if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {

if (runState == RUNNING && workQueue.offer(command)) {

if (runState != RUNNING || poolSize == 0)

ensureQueuedTaskHandled(command);

}

else if (!addIfUnderMaximumPoolSize(command))

reject(command); // is shutdown or saturated

}

}

上面的代码可能看起来不是那么容易理解,下面我们一句一句解释:

首先,判断提交的任务command是否为null,若是null,则抛出空指针异常;

接着是这句,这句要好好理解一下:

if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))

由于是或条件运算符,所以先计算前半部分的值,如果线程池中当前线程数不小于核心池大小,那么就会直接进入下面的if语句块了。

如果线程池中当前线程数小于核心池大小,则接着执行后半部分,也就是执行

addIfUnderCorePoolSize(command)

如果执行完addIfUnderCorePoolSize这个方法返回false,则继续执行下面的if语句块,否则整个方法就直接执行完毕了。

如果执行完addIfUnderCorePoolSize这个方法返回false,然后接着判断:

if (runState == RUNNING && workQueue.offer(command))

如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列;如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,则执行:

addIfUnderMaximumPoolSize(command)

如果执行addIfUnderMaximumPoolSize方法失败,则执行reject()方法进行任务拒绝处理。

回到前面:

if (runState == RUNNING && workQueue.offer(command))

这句的执行,如果说当前线程池处于RUNNING状态且将任务放入任务缓存队列成功,则继续进行判断:

if (runState != RUNNING || poolSize == 0)

这句判断是为了防止在将此任务添加进任务缓存队列的同时其他线程突然调用shutdown或者shutdownNow方法关闭了线程池的一种应急措施。如果是这样就执行:

ensureQueuedTaskHandled(command)

进行应急处理,从名字可以看出是保证 添加到任务缓存队列中的任务得到处理。

我们接着看2个关键方法的实现:addIfUnderCorePoolSize和addIfUnderMaximumPoolSize:

private boolean addIfUnderCorePoolSize(Runnable firstTask) {

Thread t = null;

final ReentrantLock mainLock = this.mainLock;

mainLock.lock();

try {

if (poolSize < corePoolSize && runState == RUNNING)

t = addThread(firstTask);        //创建线程去执行firstTask任务

} finally {

mainLock.unlock();

}

if (t == null)

return false;

t.start();

return true;

}

这个是addIfUnderCorePoolSize方法的具体实现,从名字可以看出它的意图就是当低于核心吃大小时执行的方法。下面看其具体实现,首先获取到锁,因为这地方涉及到线程池状态的变化,先通过if语句判断当前线程池中的线程数目是否小于核心池大小,有朋友也许会有疑问:前面在execute()方法中不是已经判断过了吗,只有线程池当前线程数目小于核心池大小才会执行addIfUnderCorePoolSize方法的,为何这地方还要继续判断?原因很简单,前面的判断过程中并没有加锁,因此可能在execute方法判断的时候poolSize小于corePoolSize,而判断完之后,在其他线程中又向线程池提交了任务,就可能导致poolSize不小于corePoolSize了,所以需要在这个地方继续判断。然后接着判断线程池的状态是否为RUNNING,原因也很简单,因为有可能在其他线程中调用了shutdown或者shutdownNow方法。然后就是执行

t = addThread(firstTask);

这个方法也非常关键,传进去的参数为提交的任务,返回值为Thread类型。然后接着在下面判断t是否为空,为空则表明创建线程失败(即poolSize>=corePoolSize或者runState不等于RUNNING),否则调用t.start()方法启动线程。

我们来看一下addThread方法的实现:

private Thread addThread(Runnable firstTask) {

Worker w = new Worker(firstTask);

Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务

if (t != null) {

w.thread = t;            //将创建的线程的引用赋值为w的成员变量

workers.add(w);

int nt = ++poolSize;     //当前线程数加1

if (nt > largestPoolSize)

largestPoolSize = nt;

}

return t;

}

在addThread方法中,首先用提交的任务创建了一个Worker对象,然后调用线程工厂threadFactory创建了一个新的线程t,然后将线程t的引用赋值给了Worker对象的成员变量thread,接着通过workers.add(w)将Worker对象添加到工作集当中。

接下文

大型网站架构系列:负载均衡详解(下)

来源:ITFLY8

链接:http://www.cnblogs.com/itfly8/p/5080743.html

《大型网站架构系列:负载均衡详解(上)》

一、软件负载均衡概述

硬件负载均衡性能优越,功能全面,但是价格昂贵,一般适合初期或者土豪级公司长期使用。因此软件负载均衡在互联网领域大量使用。常用的软件负载均衡软件有Nginx,Lvs,HaProxy等。本文参考大量文档,部分为直接拷贝。

二、Ngnix负载均衡

Ngnix是一款轻量级的Web服务器/反向代理服务器,工作在七层Http协议的负载均衡系统。具有高性能、高并发、低内存使用等特点。是一个轻量级的Http和反向代理服务器。Nginx使用epoll and kqueue作为开发模型。能够支持高达 50,000 个并发连接数的响应。

操作系统:Liunx,Windows(Linux、FreeBSD、Solaris、Mac OS X、AIX以及Microsoft Windows)

开发语言:C

并发性能:官方支持每秒5万并发,实际国内一般到每秒2万并发,有优化到每秒10万并发的。具体性能看应用场景。

2.1.特点

1.模块化设计:良好的扩展性,可以通过模块方式进行功能扩展。

2.高可靠性:主控进程和worker是同步实现的,一个worker出现问题,会立刻启动另一个worker。

3.内存消耗低:一万个长连接(keep-alive),仅消耗2.5MB内存。

4.支持热部署:不用停止服务器,实现更新配置文件,更换日志文件、更新服务器程序版本。

5.并发能力强:官方数据每秒支持5万并发;

6.功能丰富:优秀的反向代理功能和灵活的负载均衡策略

2.2.功能

2.2.1 基本功能

支持静态资源的web服务器。

http,smtp,pop3协议的反向代理服务器、缓存、负载均衡;

支持FASTCGI(fpm)

支持模块化,过滤器(让文本可以实现压缩,节约带宽),ssl及图像大小调整。

内置的健康检查功能

基于名称和ip的虚拟主机

定制访问日志

支持平滑升级

支持KEEPALIVE

支持url rewrite

支持路径别名

支持基于IP和用户名的访问控制。

支持传输速率限制,支持并发数限制。

2.2.2 性能

Nginx的高并发,官方测试支持5万并发连接。实际生产环境能到2-3万并发连接数。10000个非活跃的HTTP keep-alive 连接仅占用约2.5MB内存。三万并发连接下,10个Nginx进程,消耗内存150M。淘宝tengine团队测试结果是“24G内存机器上,处理并发请求可达200万”。

2.3架构

2.3.1Nginx的基本工作模式

一个master进程,生成一个或者多个worker进程。但是这里master是使用root身份启动的,因为nginx要工作在80端口。而只有管理员才有权限启动小于低于1023的端口。master主要是负责的作用只是启动worker,加载配置文件,负责系统的平滑升级。其它的工作是交给worker。那么当worker被启动之后,也只是负责一些web最简单的工作,而其他的工作都是有worker中调用的模块来实现的。

模块之间是以流水线的方式实现功能的。流水线,指的是一个用户请求,由多个模块组合各自的功能依次实现完成的。比如:第一个模块只负责分析请求首部,第二个模块只负责查找数据,第三个模块只负责压缩数据,依次完成各自工作。来实现整个工作的完成。

他们是如何实现热部署的呢?其实是这样的,我们前面说master不负责具体的工作,而是调用worker工作,他只是负责读取配置文件,因此当一个模块修改或者配置文件发生变化,是由master进行读取,因此此时不会影响到worker工作。在master进行读取配置文件之后,不会立即的把修改的配置文件告知worker。而是让被修改的worker继续使用老的配置文件工作,当worker工作完毕之后,直接当掉这个子进程,更换新的子进程,使用新的规则。

2.3.2Nginx支持的sendfile机制

Sendfile机制,用户将请求发给内核,内核根据用户的请求调用相应用户进程,进程在处理时需要资源。此时再把请求发给内核(进程没有直接IO的能力),由内核加载数据。内核查找到数据之后,会把数据复制给用户进程,由用户进程对数据进行封装,之后交给内核,内核在进行tcp/ip首部的封装,最后再发给客户端。这个功能用户进程只是发生了一个封装报文的过程,却要绕一大圈。因此nginx引入了sendfile机制,使得内核在接受到数据之后,不再依靠用户进程给予封装,而是自己查找自己封装,减少了一个很长一段时间的浪费,这是一个提升性能的核心点。

以上内容摘自网友发布的文章,简单一句话是资源的处理,直接通过内核层进行数据传递,避免了数据传递到应用层,应用层再传递到内核层的开销。

目前高并发的处理,一般都采用sendfile模式。通过直接操作内核层数据,减少应用与内核层数据传递。

2.3.3Nginx通信模型(I/O复用机制)

开发模型:epoll和kqueue。

支持的事件机制:kqueue、epoll、rt signals、/dev/poll 、event ports、select以及poll。

支持的kqueue特性包括EV_CLEAR、EV_DISABLE、NOTE_LOWAT、EV_EOF,可用数据的数量,错误代码.

支持sendfile、sendfile64和sendfilev;文件AIO;DIRECTIO;支持Accept-filters和TCP_DEFER_ACCEP.

以上概念较多,大家自行百度或谷歌,知识领域是网络通信(BIO,NIO,AIO)和多线程方面的知识。

2.4均衡策略

nginx的负载均衡策略可以划分为两大类:内置策略和扩展策略。内置策略包含加权轮询和ip hash,在默认情况下这两种策略会编译进nginx内核,只需在nginx配置中指明参数即可。扩展策略有很多,如fair、通用hash、consistent hash等,默认不编译进nginx内核。由于在nginx版本升级中负载均衡的代码没有本质性的变化,因此下面将以nginx1.0.15稳定版为例,从源码角度分析各个策略。

2.4.1. 加权轮询(weighted round robin)

轮询的原理很简单,首先我们介绍一下轮询的基本流程。如下是处理一次请求的流程图:

图中有两点需要注意,第一,如果可以把加权轮询算法分为先深搜索和先广搜索,那么nginx采用的是先深搜索算法,即将首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器;第二,当所有后端机器都down掉时,nginx会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在timeout的状态,从而导致整个前端被夯住。

2.4.2. ip hash

ip hash是nginx内置的另一个负载均衡的策略,流程和轮询很类似,只是其中的算法和具体的策略有些变化,如下图所示:

2.4.3. fair

fair策略是扩展策略,默认不被编译进nginx内核。其原理是根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行分流。这种策略具有很强的自适应性,但是实际的网络环境往往不是那么简单,因此要慎用。

2.4.4 通用hash、一致性hash

这两种也是扩展策略,在具体的实现上有些差别,通用hash比较简单,可以以nginx内置的变量为key进行hash,一致性hash采用了nginx内置的一致性hash环,可以支持memcache。

2.5场景

Ngnix一般作为入口负载均衡或内部负载均衡,结合反向代理服务器使用。以下架构示例,仅供参考,具体使用根据场景而定。

2.5.1入口负载均衡架构

Ngnix服务器在用户访问的最前端。根据用户请求再转发到具体的应用服务器或二级负载均衡服务器(LVS)

2.5.2内部负载均衡架构

LVS作为入口负载均衡,将请求转发到二级Ngnix服务器,Ngnix再根据请求转发到具体的应用服务器。

2.5.3Ngnix高可用

分布式系统中,应用只部署一台服务器会存在单点故障,负载均衡同样有类似的问题。一般可采用主备或负载均衡设备集群的方式节约单点故障或高并发请求分流。

Ngnix高可用,至少包含两个Ngnix服务器,一台主服务器,一台备服务器,之间使用Keepalived做健康监控和故障检测。开放VIP端口,通过防火墙进行外部映射。

DNS解析公网的IP实际为VIP。

三、LVS负载均衡

LVS是一个开源的软件,由毕业于国防科技大学的章文嵩博士于1998年5月创立,用来实现Linux平台下的简单负载均衡。LVS是Linux Virtual Server的缩写,意思是Linux虚拟服务器。

基于IP层的负载均衡调度技术,它在操作系统核心层上,将来自IP层的TCP/UDP请求均衡地转移到不同的 服务器,从而将一组服务器构成一个高性能、高可用的虚拟服务器。

 

操作系统:Liunx

开发语言:C

并发性能:默认4096,可以修改但需要重新编译。

3.1.功能

LVS的主要功能是实现IP层(网络层)负载均衡,有NAT,TUN,DR三种请求转发模式。

3.1.1 LVS/NAT方式的负载均衡集群

NAT是指Network Address Translation,它的转发流程是:Director机器收到外界请求,改写数据包的目标地址,按相应的调度算法将其发送到相应Real Server上,Real Server处理完该请求后,将结果数据包返回到其默认网关,即Director机器上,Director机器再改写数据包的源地址,最后将其返回给外界。这样就完成一次负载调度。

 

构架一个最简单的LVS/NAT方式的负载均衡集群Real Server可以是任何的操作系统,而且无需做任何特殊的设定,惟一要做的就是将其默认网关指向Director机器。Real Server可以使用局域网的内部IP(192.168.0.0/24)。Director要有两块网卡,一块网卡绑定一个外部IP地址 (10.0.0.1),另一块网卡绑定局域网的内部IP(192.168.0.254),作为Real Server的默认网关。

LVS/NAT方式实现起来最为简单,而且Real Server使用的是内部IP,可以节省Real IP的开销。但因为执行NAT需要重写流经Director的数据包,在速度上有一定延迟;

当用户的请求非常短,而服务器的回应非常大的情况下,会对Director形成很大压力,成为新的瓶颈,从而使整个系统的性能受到限制。

3.1.2 LVS/TUN方式的负载均衡集群

TUN是指IP Tunneling,它的转发流程是:Director机器收到外界请求,按相应的调度算法,通过IP隧道发送到相应Real Server,Real Server处理完该请求后,将结果数据包直接返回给客户。至此完成一次负载调度。

 

最简单的LVS/TUN方式的负载均衡集群架构使用IP Tunneling技术,在Director机器和Real Server机器之间架设一个IP Tunnel,通过IP Tunnel将负载分配到Real Server机器上。Director和Real Server之间的关系比较松散,可以是在同一个网络中,也可以是在不同的网络中,只要两者能够通过IP Tunnel相连就行。收到负载分配的Real Server机器处理完后会直接将反馈数据送回给客户,而不必通过Director机器。实际应用中,服务器必须拥有正式的IP地址用于与客户机直接通信,并且所有服务器必须支持IP隧道协议。

 

该方式中Director将客户请求分配到不同的Real Server,Real Server处理请求后直接回应给用户,这样Director就只处理客户机与服务器的一半连接,极大地提高了Director的调度处理能力,使集群系统能容纳更多的节点数。另外TUN方式中的Real Server可以在任何LAN或WAN上运行,这样可以构筑跨地域的集群,其应对灾难的能力也更强,但是服务器需要为IP封装付出一定的资源开销,而且后端的Real Server必须是支持IP Tunneling的操作系统。

3.3.3 LVS/TUN方式的负载均衡集群

DR是指Direct Routing,它的转发流程是:Director机器收到外界请求,按相应的调度算法将其直接发送到相应Real Server,Real Server处理完该请求后,将结果数据包直接返回给客户,完成一次负载调度。

 

构架一个最简单的LVS/DR方式的负载均衡集群Real Server和Director都在同一个物理网段中,Director的网卡IP是192.168.0.253,再绑定另一个IP: 192.168.0.254作为对外界的virtual IP,外界客户通过该IP来访问整个集群系统。Real Server在lo上绑定IP:192.168.0.254,同时加入相应的路由。

 

LVS/DR方式与前面的LVS/TUN方式有些类似,前台的Director机器也是只需要接收和调度外界的请求,而不需要负责返回这些请求的反馈结果,所以能够负载更多的Real Server,提高Director的调度处理能力,使集群系统容纳更多的Real Server。但LVS/DR需要改写请求报文的MAC地址,所以所有服务器必须在同一物理网段内。

3.3 架构

LVS架设的服务器集群系统有三个部分组成:最前端的负载均衡层(Loader Balancer),中间的服务器群组层,用Server Array表示,最底层的数据共享存储层,用Shared Storage表示。在用户看来所有的应用都是透明的,用户只是在使用一个虚拟服务器提供的高性能服务。

LVS的体系架构如图:

LVS的各个层次的详细介绍:

Load Balancer层:位于整个集群系统的最前端,有一台或者多台负载调度器(Director Server)组成,LVS模块就安装在Director Server上,而Director的主要作用类似于一个路由器,它含有完成LVS功能所设定的路由表,通过这些路由表把用户的请求分发给Server Array层的应用服务器(Real Server)上。同时,在Director Server上还要安装对Real Server服务的监控模块Ldirectord,此模块用于监测各个Real Server服务的健康状况。在Real Server不可用时把它从LVS路由表中剔除,恢复时重新加入。

Server Array层:由一组实际运行应用服务的机器组成,Real Server可以是WEB服务器、MAIL服务器、FTP服务器、DNS服务器、视频服务器中的一个或者多个,每个Real Server之间通过高速的LAN或分布在各地的WAN相连接。在实际的应用中,Director Server也可以同时兼任Real Server的角色。

Shared Storage层:是为所有Real Server提供共享存储空间和内容一致性的存储区域,在物理上,一般有磁盘阵列设备组成,为了提供内容的一致性,一般可以通过NFS网络文件系统共享数 据,但是NFS在繁忙的业务系统中,性能并不是很好,此时可以采用集群文件系统,例如Red hat的GFS文件系统,oracle提供的OCFS2文件系统等。

从整个LVS结构可以看出,Director Server是整个LVS的核心,目前,用于Director Server的操作系统只能是Linux和FreeBSD,linux2.6内核不用任何设置就可以支持LVS功能,而FreeBSD作为 Director Server的应用还不是很多,性能也不是很好。对于Real Server,几乎可以是所有的系统平台,Linux、windows、Solaris、AIX、BSD系列都能很好的支持。

3.4均衡策略

LVS默认支持八种负载均衡策略,简述如下:

3.4.1.轮询调度(Round Robin)

调度器通过“轮询”调度算法将外部请求按顺序轮流分配到集群中的真实服务器上,它均等地对待每一台服务器,而不管服务器上实际的连接数和系统负载。

3.4.2.加权轮询(Weighted Round Robin)

调度器通过“加权轮询”调度算法根据真实服务器的不同处理能力来调度访问请求。这样可以保证处理能力强的服务器能处理更多的访问流量。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。

3.4.3.最少链接(Least Connections)

调度器通过“最少连接”调度算法动态地将网络请求调度到已建立的链接数最少的服务器上。如果集群系统的真实服务器具有相近的系统性能,采用“最小连接”调度算法可以较好地均衡负载。

3.4.4.加权最少链接(Weighted Least Connections)

在集群系统中的服务器性能差异较大的情况下,调度器采用“加权最少链接”调度算法优化负载均衡性能,具有较高权值的服务器将承受较大比例的活动连接负载。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。

3.4.5.基于局部性的最少链接(Locality-Based Least Connections)

“基于局部性的最少链接”调度算法是针对目标IP地址的负载均衡,目前主要用于Cache集群系统。该算法根据请求的目标IP地址找出该目标IP地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器;若服务器不存在,或者该服务器超载且有服务器处于一半的工作负载,则用“最少链接” 的原则选出一个可用的服务器,将请求发送到该服务器。

3.4.6.带复制的基于局部性最少链接(Locality-Based Least Connections with Replication)

“带复制的基于局部性最少链接”调度算法也是针对目标IP地址的负载均衡,目前主要用于Cache集群系统。它与LBLC算法的不同之处是它要维护从一个目标IP地址到一组服务器的映射,而LBLC算法维护从一个目标IP地址到一台服务器的映射。该算法根据请求的目标IP地址找出该目标IP地址对应的服务器组,按“最小连接”原则从服务器组中选出一台服务器,若服务器没有超载,将请求发送到该服务器;若服务器超载,则按“最小连接”原则从这个集群中选出一台服务器,将该服务器加入到服务器组中,将请求发送到该服务器。同时,当该服务器组有一段时间没有被修改,将最忙的服务器从服务器组中删除,以降低复制的程度。

3.4.7.目标地址散列(Destination Hashing)

“目标地址散列”调度算法根据请求的目标IP地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。

3.4.8.源地址散列(Source Hashing)

“源地址散列”调度算法根据请求的源IP地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。

 

除具备以上负载均衡算法外,还可以自定义均衡策略。

3.5场景

一般作为入口负载均衡或内部负载均衡,结合反向代理服务器使用。相关架构可参考Ngnix场景架构。

四、HaProxy负载均衡

HAProxy也是使用较多的一款负载均衡软件。HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,是免费、快速并且可靠的一种解决方案。特别适用于那些负载特大的web站点。运行模式使得它可以很简单安全的整合到当前的架构中,同时可以保护你的web服务器不被暴露到网络上。

4.1.特点

  • 支持两种代理模式:TCP(四层)和HTTP(七层),支持虚拟主机;
  • 配置简单,支持url检测后端服务器状态;
  • 做负载均衡软件使用,在高并发情况下,处理速度高于nginx;
  • TCP层多用于Mysql从(读)服务器负载均衡。 (对Mysql进行负载均衡,对后端的DB节点进行检测和负载均衡)
  • 能够补充Nginx的一些缺点比如Session的保持,Cookie引导等工作

4.2.均衡策略

支持四种常用算法:

1.roundrobin:轮询,轮流分配到后端服务器;

2.static-rr:根据后端服务器性能分配;

3.leastconn:最小连接者优先处理;

4.source:根据请求源IP,与Nginx的IP_Hash类似。

五、本次分享总结

以上是本周的分享,从主要讲解了软件负载均衡的应用背景,Ngnix负载均衡,LVS负载均衡,Haproxy负载均衡。

因为时间关系,有些讲解的不细致,大家可以问下度娘/Google,希望本次分享对大家有帮助。

大型网站架构系列:负载均衡详解(上)

来源:ITFLY8

链接:http://www.cnblogs.com/itfly8/p/5043435.html

面对大量用户访问、高并发请求,海量数据,可以使用高性能的服务器、大型数据库,存储设备,高性能Web服务器,采用高效率的编程语言比如(Go,Scala)等,当单机容量达到极限时,我们需要考虑业务拆分和分布式部署,来解决大型网站访问量大,并发量高,海量数据的问题。

从单机网站到分布式网站,很重要的区别是业务拆分和分布式部署,将应用拆分后,部署到不同的机器上,实现大规模分布式系统。分布式和业务拆分解决了,从集中到分布的问题,但是每个部署的独立业务还存在单点的问题和访问统一入口问题,为解决单点故障,我们可以采取冗余的方式。将相同的应用部署到多台机器上。解决访问统一入口问题,我们可以在集群前面增加负载均衡设备,实现流量分发。

负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。

本文是负载均衡详解的第一篇文章,介绍负载均衡的原理,负载均衡分类(DNS负载均衡,HTTP负载均衡,IP负载均衡,链路层负载均衡,混合型P负载均衡)。部分内容摘自读书笔记。

本次分享大纲

1.负载均衡原理

2.DNS负载均衡

3.HTTP负载均衡

4.IP负载均衡

5.链路层负载均衡

6.混合型P负载均衡

一、负载均衡原理

系统的扩展可分为纵向(垂直)扩展和横向(水平)扩展。纵向扩展,是从单机的角度通过增加硬件处理能力,比如CPU处理能力,内存容量,磁盘等方面,实现服务器处理能力的提升,不能满足大型分布式系统(网站),大流量,高并发,海量数据的问题。因此需要采用横向扩展的方式,通过添加机器来满足大型网站服务的处理能力。比如:一台机器不能满足,则增加两台或者多台机器,共同承担访问压力。这就是典型的集群和负载均衡架构:如下图:

应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。

负载均衡设备:将用户访问的请求,根据负载均衡算法,分发到集群中的一台处理服务器。(一种把网络请求分散到一个服务器集群中的可用服务器上去的设备)

负载均衡的作用(解决的问题):

1.解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力);

2.提供故障转移,实现高可用;

3.通过添加或减少服务器数量,提供网站伸缩性(扩展性);

4.安全防护;(负载均衡设备上做一些过滤,黑白名单等处理)

二、负载均衡分类

根据实现技术不同,可分为DNS负载均衡,HTTP负载均衡,IP负载均衡,链路层负载均衡等。

2.1DNS负载均衡

最早的负载均衡技术,利用域名解析实现负载均衡,在DNS服务器,配置多个A记录,这些A记录对应的服务器构成集群。大型网站总是部分使用DNS解析,作为第一级负载均衡。如下图:

优点

1. 使用简单:负载均衡工作,交给DNS服务器处理,省掉了负载均衡服务器维护的麻烦

2. 提高性能:可以支持基于地址的域名解析,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能;

缺点

1. 可用性差:DNS解析是多级解析,新增/修改DNS后,解析时间较长;解析过程中,用户访问网站将失败;

2. 扩展性低:DNS负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展;

3. 维护性差:也不能反映服务器的当前运行状态;支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载)

实践建议

将DNS作为第一级负载均衡,A记录对应着内部负载均衡的IP地址,通过内部负载均衡将请求分发到真实的Web服务器上。一般用于互联网公司,复杂的业务系统不合适使用。如下图:

1.3  IP负载均衡

在网络层通过修改请求目标地址进行负载均衡。

用户请求数据包,到达负载均衡服务器后,负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法得到一台真实服务器地址,然后将请求目的地址修改为,获得的真实ip地址,不需要经过用户进程处理。

真实服务器处理完成后,响应数据包回到负载均衡服务器,负载均衡服务器,再将数据包源地址修改为自身的ip地址,发送给用户浏览器。如下图:

IP负载均衡,真实物理服务器返回给负载均衡服务器,存在两种方式:(1)负载均衡服务器在修改目的ip地址的同时修改源地址。将数据包源地址设为自身盘,即源地址转换(snat)。(2)将负载均衡服务器同时作为真实物理服务器集群的网关服务器。

优点:

(1)在内核进程完成数据分发,比在应用层分发性能更好;

缺点:

(2)所有请求响应都需要经过负载均衡服务器,集群最大吞吐量受限于负载均衡服务器网卡带宽;

2.4链路层负载均衡

在通信协议的数据链路层修改mac地址,进行负载均衡。

数据分发时,不修改ip地址,指修改目标mac地址,配置真实物理服务器集群所有机器虚拟ip和负载均衡服务器ip地址一致,达到不修改数据包的源地址和目标地址,进行数据分发的目的。

实际处理服务器ip和数据请求目的ip一致,不需要经过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。也称为直接路由模式(DR模式)。如下图:

优点:性能好;

缺点:配置复杂;

实践建议:DR模式是目前使用最广泛的一种负载均衡方式。

2.5混合型负载均衡

由于多个服务器群内硬件设备、各自的规模、提供的服务等的差异,可以考虑给每个服务器群采用最合适的负载均衡方式,然后又在这多个服务器群间再一次负载均衡或群集起来以一个整体向外界提供服务(即把这多个服务器群当做一个新的服务器群),从而达到最佳的性能。将这种方式称之为混合型负载均衡。

此种方式有时也用于单台均衡设备的性能不能满足大量连接请求的情况下。是目前大型互联网公司,普遍使用的方式。

方式一,如下图:

以上模式适合有动静分离的场景,反向代理服务器(集群)可以起到缓存和动态请求分发的作用,当时静态资源缓存在代理服务器时,则直接返回到浏览器。如果动态页面则请求后面的应用负载均衡(应用集群)。

方式二,如下图:

以上模式,适合动态请求场景。

因混合模式,可以根据具体场景,灵活搭配各种方式,以上两种方式仅供参考。

三、负载均衡算法

常用的负载均衡算法有,轮询,随机,最少链接,源地址散列,加权等方式;

3.1 轮询

将所有请求,依次分发到每台服务器上,适合服务器硬件同相同的场景。

优点:服务器请求数目相同;

缺点:服务器压力不一样,不适合服务器配置不同的情况;

3.2 随机

请求随机分配到各个服务器。

优点:使用简单;

缺点:不适合机器配置不同的场景;

3.3 最少链接

将请求分配到连接数最少的服务器(目前处理请求最少的服务器)。

优点:根据服务器当前的请求处理情况,动态分配;

缺点:算法实现相对复杂,需要监控服务器请求连接数;

3.4 Hash(源地址散列)

根据IP地址进行Hash计算,得到IP地址。

优点:将来自同一IP地址的请求,同一会话期内,转发到相同的服务器;实现会话粘滞。

缺点:目标服务器宕机后,会话会丢失;

3.5 加权

在轮询,随机,最少链接,Hash’等算法的基础上,通过加权的方式,进行负载服务器分配。

优点:根据权重,调节转发服务器的请求数目;

缺点:使用相对复杂;

四、硬件负载均衡

采用硬件的方式实现负载均衡,一般是单独的负载均衡服务器,价格昂贵,一般土豪级公司可以考虑,业界领先的有两款,F5和A10。

使用硬件负载均衡,主要考虑一下几个方面:

(1)功能考虑:功能全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡;

(2)性能考虑:一般软件负载均衡支持到5万级并发已经很困难了,硬件负载均衡可以支持

(3)稳定性:商用硬件负载均衡,经过了良好的严格的测试,从经过大规模使用,在稳定性方面高;

(4)安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙,防DDOS攻击等安全功能;

(5)维护角度:提供良好的维护管理界面,售后服务和技术支持;

(6)土豪公司:F5 Big Ip 价格:15w~55w不等;A10 价格:55w-100w不等;

缺点

(1)价格昂贵;

(2)扩展能力差;

4.4小结

(1)一般硬件的负载均衡也要做双机高可用,因此成本会比较高。

(2)互联网公司一般使用开源软件,因此大部分应用采用软件负载均衡;部分采用硬件负载均衡。

比如某互联网公司,目前是使用几台F5做全局负载均衡,内部使用Nginx等软件负载均衡。