Fork me on GitHub
Java回顾之集合

Java回顾之集合

第一篇:Java回顾之I/O

  第二篇:Java回顾之网络通信

  第三篇:Java回顾之多线程

  第四篇:Java回顾之多线程同步

 

  在这篇文章里,我们关注Java中的集合(Collection)。集合是编程语言中基础的一部分,Java自JDK早期,就引入了Java Collection Framework。设计JCF的那个人,后来还写了一本书,叫《Effective Java》。

  Java中的集合主要集中在2部分,一部分是java.util包中,一部分是java.util.concurrent中,后者是在前者的基础上,定义了一些实现了同步功能的集合。

  这篇文章主要关注java.util下的各种集合对象。Java中的集合对象可以粗略的分为3类:List、Set和Map。对应的UML图如下(包括了java.util下大部分的集合对象):

  (这张图经过缩放已经变形,完整清晰版图片请参见:https://files.cnblogs.com/wing011203/java_collection_structure.zip,解压缩后就可以看到未经缩放的版本。)

  Collection概述

  Java集合中的List和Set都从Collection出来,它是一个学习集合很不错的入口,它包含了集合中通常需要有的操作:

  • 添加元素:add/addAll
  • 清空集合:clear
  • 删除元素:remove/removeAll
  • 判断集合中是否包含某元素:contains/containsAll
  • 判断集合是否为空:isEmpty
  • 计算集合中元素的个数:size
  • 将集合转换为数组:toArray
  • 获取迭代器:iterator

  我们来看一个简单的例子,下面的代码会返回一个集合,集合中的元素是随机生成的整数:

复制代码
 1 private static Collection initCollection()
 2 {
 3     Collection<Integer> collection = new ArrayList<Integer>();
 4     Random r = new Random();
 5     for (int i = 0 ; i < 5; i++)
 6     {
 7         collection.add(new Integer(r.nextInt(100)));
 8     }
 9     
10     return collection;
11 }
复制代码

  在对集合进行操作的过程中,遍历是一个经常使用的操作,我们可以使用两种方式对集合进行遍历:

  1) 使用迭代器对集合进行遍历。正如上面描述Collection接口时所说,所有集合都会有一个迭代器,我们可以用它来遍历集合。

复制代码
1 private static void accessCollectionByIterator(Collection<Integer> collection)
2 {
3     Iterator<Integer> iterator = collection.iterator();
4     System.out.println("The value in the list:");
5     while(iterator.hasNext())
6     {
7         System.out.println(iterator.next());
8     }
9 }
复制代码

  2)使用foreach遍历集合。

复制代码
1 private static void accessCollectionByFor(Collection<Integer> collection)
2 {
3     System.out.println("The value in the list:");
4     for(Integer value : collection)
5     {
6         System.out.println(value);
7     }
8 }
复制代码

  List

  Java中的List是对数组的有效扩展,它是这样一种结构,如果不使用泛型,它可以容纳任何类型的元素,如果使用泛型,那么它只能容纳泛型指定的类型的元素。和数组相比,List的容量是可以动态扩展的。

  List中的元素是可以重复的,里面的元素是“有序”的,这里的“有序”,并不是排序的意思,而是说我们可以对某个元素在集合中的位置进行指定。

  List中常用的集合对象包括:ArrayList、Vector和LinkedList,其中前两者是基于数组来进行存储,后者是基于链表进行存储。其中Vector是线程安全的,其余两个不是线程安全的。

  List中是可以包括null的,即使是使用了泛型。

  ArrayList可能是我们平时用到的最多的集合对象了,在上述的示例代码中,我们也是使用它来实例化一个Collection对象,在此不再赘述。

  Vector

  Vector的示例如下,首先我们看如何生成和输出Vector:

复制代码
 1 private static void vectorTest1()
 2 {
 3     List<Integer> list = new Vector<Integer>();
 4     for (int i = 0 ; i < 5; i++)
 5     {
 6         list.add(new Integer(100));
 7     }
 8     list.add(null);
 9     System.out.println("size of vector is " + list.size());
10     System.out.println(list);
11 }
复制代码

  它的元素中,既包括了重复元素,也包括了null,输出结果如下:

size of vector is 6
[100, 100, 100, 100, 100, null]

  下面的示例,演示了Vector中的一些常用方法:

复制代码
 1 private static void vectorTest2()
 2 {
 3     Vector<Integer> list = new Vector<Integer>();
 4     Random r = new Random();
 5     for (int i = 0 ; i < 10; i++)
 6     {
 7         list.add(new Integer(r.nextInt(100)));
 8     }
 9     System.out.println("size of vector is " + list.size());
10     System.out.println(list);
11     System.out.println(list.firstElement());
12     System.out.println(list.lastElement());
13     System.out.println(list.subList(3, 8));
14     List<Integer> temp = new ArrayList<Integer>();
15     for(int i = 4; i < 7; i++)
16     {
17         temp.add(list.get(i));
18     }
19     list.retainAll(temp);
20     System.out.println("size of vector is " + list.size());
21     System.out.println(list);
22 }
复制代码

  它的输出结果如下:

复制代码
size of vector is 10
[39, 41, 20, 9, 29, 32, 54, 12, 94, 82]
39
82
[9, 29, 32, 54, 12]
size of vector is 3
[29, 32, 54]
复制代码

  LinkedList

  LinkedList使用链表来存储数据,它的示例代码如下:

LinkedList示例

  这里列出了LinkedList常用的各个方法,从方法名可以看出,LinkedList也可以用来实现栈和队列。

  输出结果如下:

复制代码
size of linked list is 11
[17, 21, 5, 84, 19, 57, 68, 26, 27, 47, null]
17
17
null
17
17
null
17
21
null
5
size of linked list is 8
[100, 84, 19, 57, 68, 26, 27, 47]
复制代码

  Set

  Set 和List类似,都是用来存储单个元素,单个元素的数量不确定。但Set不能包含重复元素,如果向Set中插入两个相同元素,那么后一个元素不会被插入。

  Set可以大致分为两类:不排序Set和排序Set,不排序Set包括HashSet和LinkedHashSet,排序Set主要指TreeSet。其中HashSet和LinkedHashSet可以包含null。

  HashSet

  HashSet是由Hash表支持的一种集合,它不是线程安全的。

  我们来看下面的示例,它和Vector的第一个示例基本上是相同的:

复制代码
 1 private static void hashSetTest1()
 2 {
 3     Set<Integer> set = new HashSet<Integer>();
 4     
 5     for (int i = 0; i < 3; i++)
 6     {
 7         set.add(new Integer(100));
 8     }
 9     set.add(null);
10     
11     System.out.println("size of set is " + set.size());
12     System.out.println(set);
13 }
复制代码

  这里,HashSet中既包含了重复元素,又包含了null,和Vector不同,这里的输出结果如下:

size of set is 2
[null, 100]

  对于HashSet是如何判断两个元素是否是重复的,我们可以深入考察一下。Object中也定义了equals方法,对于HashSet中的元素,它是根据equals方法来判断元素是否相等的,为了证明这一点,我们可以定义个“不正常”的类型:

定义MyInteger对象

  可以看到,对于MyInteger来说,对于任意两个实例,我们都认为它是不相等的。

  下面是对应的测试方法:

复制代码
 1 private static void hashSetTest2()
 2 {
 3     Set<MyInteger> set = new HashSet<MyInteger>();
 4     
 5     for (int i = 0; i < 3; i++)
 6     {
 7         set.add(new MyInteger(100));
 8     }
 9     
10     System.out.println("size of set is " + set.size());
11     System.out.println(set);
12 }
复制代码

  它的输出结果如下:

size of set is 3
[100, 100, 100]

  可以看到,现在HashSet里有“重复”元素了,但对于MyInteger来说,它们不是“相同”的。

  TreeSet

  TreeSet是支持排序的一种Set,它的父接口是SortedSet。

  我们首先来看一下TreeSet都有哪些基本操作:

复制代码
 1 private static void treeSetTest1()
 2 {
 3     TreeSet<Integer> set = new TreeSet<Integer>();
 4     
 5     Random r = new Random();
 6     for (int i = 0 ; i < 5; i++)
 7     {
 8         set.add(new Integer(r.nextInt(100)));
 9     }
10 
11     System.out.println(set);
12     System.out.println(set.first());
13     System.out.println(set.last());
14     System.out.println(set.descendingSet());
15     System.out.println(set.headSet(new Integer(50)));
16     System.out.println(set.tailSet(new Integer(50)));
17     System.out.println(set.subSet(30, 60));
18     System.out.println(set.floor(50));
19     System.out.println(set.ceiling(50));
20 }
复制代码

  它的输出结果如下:

复制代码
[8, 42, 48, 49, 53]
8
53
[53, 49, 48, 42, 8]
[8, 42, 48, 49]
[53]
[42, 48, 49, 53]
49
53
复制代码

  TreeSet中的元素,一般都实现了Comparable接口,默认情况下,对于Integer来说,SortedList是采用升序来存储的,我们也可以自定义Compare方式,例如以降序的方式来存储。

  下面,我们首先重新定义Integer:

定义MyInteger2对象

  下面是测试代码:

复制代码
 1 private static void treeSetTest2()
 2 {
 3     TreeSet<Integer> set1 = new TreeSet<Integer>();
 4     TreeSet<MyInteger2> set2 = new TreeSet<MyInteger2>();
 5     Random r = new Random();
 6     for (int i = 0 ; i < 5; i++)
 7     {
 8         int value = r.nextInt(100);
 9         set1.add(new Integer(value));
10         set2.add(new MyInteger2(value));
11     }
12     System.out.println("Set1 as below:");
13     System.out.println(set1);
14     System.out.println("Set2 as below:");
15     System.out.println(set2);
16 }
复制代码

  代码的运行结果如我们所预期的那样,如下所示:

Set1 as below:
[13, 41, 42, 45, 61]
Set2 as below:
[61, 45, 42, 41, 13]

  Map

  Map中存储的是“键值对”,和Set类似,Java中的Map也有两种:排序的和不排序的,不排序的包括HashMap、Hashtable和LinkedHashMap,排序的包括TreeMap。

  非排序Map

  HashMap和Hashtable都是采取Hash表的方式进行存储,HashMap不是线程安全的,Hashtable是线程安全的,我们可以把HashMap看做是“简化”版的Hashtable。

  HashMap是可以存储null的,无论是对Key还是对Value。Hashtable是不可以存储null的。

  无论HashMap还是Hashtable,我们观察它的构造函数,就会发现它可以有两个参数:initialCapacity和loadFactor,默认情况下,initialCapacity等于16,loadFactor等于0.75。这和Hash表中可以存放的元素数目有关系,当元素数目超过initialCapacity*loadFactor时,会触发rehash方法,对hash表进行扩容。如果我们需要向其中插入过多元素,需要适当调整这两个参数。

  我们首先来看HashMap的示例:

复制代码
 1 private static void hashMapTest1()
 2 {
 3     Map<Integer,String> map = new HashMap<Integer, String>();
 4     
 5     map.put(new Integer(1), "a");
 6     map.put(new Integer(2), "b");
 7     map.put(new Integer(3), "c");
 8     
 9     System.out.println(map);
10     System.out.println(map.entrySet());
11     System.out.println(map.keySet());
12     System.out.println(map.values());
13 }
复制代码

  这会输出HashMap里的元素信息,如下所示。

{1=a, 2=b, 3=c}
[1=a, 2=b, 3=c]
[1, 2, 3]
[a, b, c]

  下面的示例是对null的演示:

复制代码
 1 private static void hashMapTest2()
 2 {
 3     Map<Integer,String> map = new HashMap<Integer, String>();
 4     
 5     map.put(null, null);
 6     map.put(null, null);
 7     map.put(new Integer(4), null);
 8     map.put(new Integer(5), null);
 9     
10     System.out.println(map);
11     System.out.println(map.entrySet());
12     System.out.println(map.keySet());
13     System.out.println(map.values());
14 }
复制代码

  执行结果如下:

{null=null, 4=null, 5=null}
[null=null, 4=null, 5=null]
[null, 4, 5]
[null, null, null]

  接下来我们演示Hashtable,和上述两个示例基本上完全一样(代码不再展开):

Hashtable示例

  执行结果如下:

复制代码
{3=c, 2=b, 1=a}
[3=c, 2=b, 1=a]
[3, 2, 1]
[c, b, a]
Exception in thread "main" java.lang.NullPointerException
    at java.util.Hashtable.put(Unknown Source)
    at sample.collections.MapSample.hashTableTest2(MapSample.java:61)
    at sample.collections.MapSample.main(MapSample.java:11)
复制代码

  可以很清楚的看到,当我们试图将null插入到hashtable中时,报出了空指针异常。

  排序Map

  排序Map主要是指TreeMap,它对元素增、删、查操作时的时间复杂度都是O(log(n))。它不是线程安全的。

  它的特点和TreeSet非常像,这里不再赘述。

    
作者:李胜攀
         
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  在前一篇博文《小学徒的成长系列—线程》中,我们已经讲解了关于线程的基本概念及其常用的方法,现在在本次博文中,我们就讲解关于守护线程,同步,及线程池的知识吧。

 1.守护线程(后台线程)

  在Java中,线程定义有两种:

  1> 非守护线程(有些教学书籍喜欢叫做非后台线程)

  2> 守护线程(有些教学书籍喜欢叫做后台线程),下面是摘自《Java编程思想》的说法:

  所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分,因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main()方法的就是一个后台线程。

  当然,并不是只有由JVM创建的才是守护线程啦,其实我们也可以定义守护线程,通过Thread类的setDaemon()定义即可。

  下面是sun公司提供的JConsole中的截图

2.线程同步问题

  我们访问很多网站的时候,往往都会有一个计数器,显示我们是第几个访问该网站的,下面我们来模拟一下,eg:

复制代码
 1 package com.thread.tongbu;
 2 
 3 public class NumberAddThread implements Runnable {
 4     public static int number = 0;
 5     
 6     @Override
 7     public void run() {
 8         timmer();  //当多个线程访问该方法修改数据时,将会涉及数据安全问题
 9     }
10     //计算该线程第几个访问的
11     public void timmer() {
12         number++;
13      
14         try {
15             //让该线程睡眠0.1s
16             Thread.sleep(100);    
17         } catch (InterruptedException e) {
18             e.printStackTrace();
19         }
20         //输出该线程是第几个访问该变量的
21         System.out.println(Thread.currentThread().getName() + " : 你是第 " + number + " 个访问");
22     }
23     
24     public static void main(String[] args) {
25         NumberAddThread n = new NumberAddThread();
26         Thread t1 = new Thread(n);
27         Thread t2 = new Thread(n);
28         t1.start();
29         t2.start();
30     }
31 }
复制代码

  本来正常的情况下输出结果应该是 : , 但我们发现结果竟然出乎意料是 : 

  这究竟是为什么呢?

  其实原因在于,刚开始,第一个线程访问的时候,number已经自加为1,然后该线程睡眠了,在它睡眠期间,跌二个线程来了,也给number加1变成了2,这个时候第一个线程才睡眠结束继续执行下一行输出语句,然而此时的number的值已经改变了,输出的结果也不再是1了。换句话说,上面的问题就是run()方法体不具备同步安全性。

  为了解决这个问题,Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的代码块就是同步代码块,同步代码块的格式如下:

  1>修饰对象:

synchronized(obj) {
    //....
    //此处的代码块就是同步代码块   
}

  2>修饰方法,表示整个方法为同步方法:

public synchronized void timmer() {
      //......
      //此处的代码块就是同步代码块        

  注意:synchronized只能修饰对象和方法,不能用来锁定构造器、属性。

  上面代码块中,不管synchronized修饰的是方法还是对象,它始终锁定的是对象的实例变量,或者类变量。当执行同步代码块的时候,就会先获取该对象的同步监视器的锁定,直到线程执行完同步代码块之后才会释放对同步监视器的锁定。

  到现在大家应该知道怎么解决前面程序出现的问题了吧,没错,只要把run()方法修改成如下即可:

1     public void run() {
2         synchronized(this){
3             timmer();
4         }
5     }

 执行结果:

  啊哈,这下我们终于对啦,呵呵。

   3.死锁

  3.1基本概念

  3.1.1什么叫做死锁?

  多个线程在执行过程中因争夺资源而造成的一种僵局,若无外力作用,将无法向前推进。

  3.1.2产生死锁的原因

  1> 竞争资源。当系统中供多个线程共享的资源如打印机等,其数目不足以满足诸线程的需要时,会引起诸线程对资源的竞争而产生死锁;

  2> 线程间推进顺序非法,线程在运行过程中,请求和释放资源的顺序不当,也同样会导致产生进程死锁。

  3.1.3产生死锁的必要条件

  1> 互斥条件,即一段时间某资源只由一个线程占用;

  2> 请求和保持条件,指进程已经保持了至少一个资源,但又提出了新的资源请求新的资源求情,而该资源又已经被其他线程占有,此时请求进程序阻塞,但又对自己已经获得的其他资源保持不放;

  3> 不剥夺条件,指线程已经获得的资源,在未使用完之前,不能被剥夺,只能在使用完时自己释放;

  4> 环路等待,指在发生死锁时,必然存在一个进程—资源的环形链。如P1等待一个P2占用的资源,P2正在等待P3占用的资源,P3正在等待P1占用的资源。

  3.1.4死锁的解除方式

  1>剥夺资源,从其他进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

  2>撤销进程,最简单的撤销进程的方法是使全部死锁进程都夭折掉,稍微温和一点的方法是按照某种顺序逐个撤销进程,直至有足够的资源可用,使死锁状态消除为止。

 3.2Java程序中的死锁状况及其调试方法

  首先我们来看一个程序,eg:

复制代码
 1 package com.thread.tongbu;
 2 
 3 public class TestDeadLock implements Runnable{
 4     static class Pen {}
 5     static class Paper{}
 6     
 7     boolean flag = false;
 8     static Paper paper = new Paper();
 9     static Pen pen = new Pen();
10     
11     @Override
12     public void run() {
13         if(flag) {
14             synchronized (paper) {
15                 try {
16                     Thread.sleep(100);
17                 } catch (InterruptedException e) {
18                     e.printStackTrace();
19                 }
20                 synchronized (pen) {
21                     System.out.println("paper");
22                 }
23             }
24         } else {
25             synchronized (pen) {
26                 try {
27                     Thread.sleep(100);
28                 } catch (InterruptedException e) {
29                     e.printStackTrace();
30                 }
31                 synchronized (paper) {
32                     System.out.println("pen");
33                 }
34             }
35         }
36     }
37     
38     public static void main(String[] args) {
39         TestDeadLock td1 = new TestDeadLock();
40         TestDeadLock td2 = new TestDeadLock();
41         td1.flag = false;
42         td2.flag = true;
43         Thread tt1 = new Thread(td2);
44         Thread tt2 = new Thread(td1); 
45         tt1.start();
46         tt2.start();
47     }
48 }
复制代码

  执行的时候,等待了好久,都一直没有出现输出结果,线程也一直没有结束,这是因为出现死锁了。

  那我们怎么确定一定是死锁呢?有两种方法。

  1>使用JDK给我们的的工具JConsole,可以通过打开cmd然后输入jconsole打开。

    1)连接到需要查看的进程。

2)打开线程选项卡,然后点击左下角的“检测死锁”

    3)jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情:

     从上图中我们可以看出:

      ①在线程Thread-1中,从状态可以看出,它想申请Paper这个资源,但是这个资源已经被Thread-0拥有了,所以就堵塞了。

      ②在线程Thread-0中,从状态可以看出,它想申请Pen这个资源,但是这个资源已经被Thread-1拥有了,所以就堵塞了。

    Thread-1一直等待paper资源,而Thread--一直等待pen资源,于是这两个线程就这么僵持了下去,造成了死锁。

  2>直接使用JVM自带的命令

    1)首先通过 jps 命令查看需要查看的Java进程的vmid,如图,我们要查看的进程TestDeadLock的vmid号是7412;

    

    2)然后利用 jstack 查看该进程中的堆栈情况,在cmd中输入 jstack -l 7412 ,移动到输出的信息的最下面即可得到:

    

    至此,相信大家都会看了吧,具体就不说啦,根据输出,找到问题所在的代码,开始调试解决即可啦。

 4.线程池

  4.1简介

  我们都知道对象的创建和销毁都是很消耗性能的,所以为了最大程度的复用对象,降低性能的消耗,就出现了容器对象池,而线程池的本质也是对象池,所以线程池能够最大程度上的复用已有的线程对象,当然除此之外,他还能够最大程度上的复用线程,否则他就不叫线程池啦。我记得我在面试金山的时候,面试官百分百的肯定是不能线程是不能复用的,我当时就不太赞同,当然我也没有理论,因为当时的我,在这块确实不太熟悉,那一次的面试,也第一次让我意识到了我的基础还是太薄弱了。好啦,扯淡了,不好意思,我们讲讲线程池是怎样复用线程的吧。

  本来线程在执行完毕之后就会被挂载或者销毁的,但是,不断的挂载或销毁,是需要一定开销的的,但是如果我们让线程完成任务后忙等待一会儿,就可以维持存在,根据调度策略分配任务给他,就又能复用该线程执行多个任务,减少了线程挂起,恢复,销毁的开销,当然啦,如果一直让线程长期忙等待的话,也是非常消耗性能的。

  下面这个线程类关系图摘自:www-35java-com的博客

  

  当然啦,实际定义线程池的是ThreadPoolExecutor类,但是Java官网的API强烈推荐我们使用Executors,因为它已经为大多数使用情景预定义了设置:

4.2ThreadPoolExecutor

  在这个类中,有一个关键的类Worker,所有的线程对象都要经过Worker的包装,这样才能够做到复用线程而无需创建新的线程。首先我们来看看这个类的构造方法再来一一解析一下吧

复制代码
 1     public ThreadPoolExecutor(int corePoolSize,
 2                               int maximumPoolSize,
 3                               long keepAliveTime,
 4                               TimeUnit unit,
 5                               BlockingQueue<Runnable> workQueue,
 6                               ThreadFactory threadFactory,
 7                               RejectedExecutionHandler handler) {
 8         if (corePoolSize < 0 ||
 9             maximumPoolSize <= 0 ||
10             maximumPoolSize < corePoolSize ||
11             keepAliveTime < 0)
12             throw new IllegalArgumentException();
13         if (workQueue == null || threadFactory == null || handler == null)
14             throw new NullPointerException();
15         this.corePoolSize = corePoolSize;
16         this.maximumPoolSize = maximumPoolSize;
17         this.workQueue = workQueue;
18         this.keepAliveTime = unit.toNanos(keepAliveTime);
19         this.threadFactory = threadFactory;
20         this.handler = handler;
21     }
复制代码

 

根据Java官网文档的解释,构造方法中每个变量的解释如下:

1> corePoolSize :线程池维护线程的最小数量,哪怕是空闲的

2>maximumPoolSize : 线程池维护的最大线程数量  
  由于ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize设置的边界自动调整池大小,其调整规则如下:

  当新任务在方法 execute(java.lang.Runnable) 中提交时

  1) 如果设置的corePoolSize 和 maximumPoolSize相同,则创建了大小固定的线程池;

  2) 如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的;

  3) 如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程;

  4) 如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务

 

3>keepAliveTime :线程池维护线程所允许的空闲时间

 

4>unit : 线程池维护线程所允许的空间时间的单位

 

5>workQueue :线程池所使用的缓冲队列,该缓冲队列的长度决定了能够缓冲的最大数量,缓冲队列有三种通用策略:

  1) 直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性;

 

  2) 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性;

 

  3) 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量.

 

6>被拒绝的任务:当Executor已经关闭,并且Executor将有限边界用于最大线程和工作队列容量,且已经饱和时,在方法execute()中提交的新任务将被拒绝.

  在以上两种情况下,execute 方法都将调用其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) 方法。下面提供了四种预定义的处理程序策略:

    1) 在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException;
    2) 在 ThreadPoolExecutor.CallerRunsPolicy 中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度

    3) 在 ThreadPoolExecutor.DiscardPolicy 中,不能执行的任务将被删除;

    4) 在 ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。

 

  7>创建新线程 : 使用 ThreadFactory 创建新线程。如果没有另外说明,则在同一个 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的 NORM_PRIORITY 优先级和非守护进程状态。通过提供不同的 ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。如果从 newThread 返回 null 时 ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。

  4.3 Executors

  Executors已经为程序员们预定义了大多数使用情景适用的连接池配置,强烈推荐使用这个类来创建对应的连接池。下面我们来介绍一下,该类中常用的几个创建线程池方法。

  1>CachedThreadPool :该连接池将为每个任务创建一个线程,下面是Executors中的newCachedThreadPool()的源代码:

  

  2> FixedThreadPool使用的Thread对象的数量是有限的,如果提交的任务数量大于限制的最大线程数,那么这些任务讲排队,然后当有一个线程的任务结束之后,将会根据调度策略继续等待执行下一个任务或者销毁。下面是Executors中的newFixedThreadPool()的源代码:

  

  3>SingleThreadExecutor就是线程数量为1的FixedThreadPool,如果提交了多个任务,那么这些任务将会排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将会使用相同的线程。下面是Executors中的newSingleThreadExecutor()的源代码:

  

  大概了解到这里吧,下面我们给出一个运行例子,在方法中,分别给出了三种配置的线程池的测试方法,大家要测试哪种只要取消哪行的方法注释然后注释掉其他两个运行即可,由于篇幅问题,具体的运行结果就补贴出来啦,eg:

复制代码
 1 package com.thread.pool;
 2 
 3 import java.util.concurrent.ExecutorService;
 4 import java.util.concurrent.Executors;
 5 
 6 public class TestThreadPool {
 7     
 8     public static void main(String[] args) {
 9 //        testCachedThreadPool();
10 //        testFixedThreadPool(0);
11         testSingleThread();
12     }
13     
14     /**
15      * 创建Runnable任务并添加到线程池中
16      * @param executorService    指定的线程池类型
17      */
18     public static void createTask(ExecutorService executorService) {
19         for (int i = 0; i < 5; i++) {
20             executorService.execute(new TaskThread());    //创建任务并交给线程池进行管理
21         }
22         executorService.shutdown();    //启动一次顺序关闭,执行以前提交的任务,但不接受新任务
23     }
24     
25     /**
26      * CachedThreadPool将为每个任务创建一个线程
27      */
28     public static void testCachedThreadPool() {
29         ExecutorService executorService = Executors.newCachedThreadPool();    //创建CachedThreadPool
30         createTask(executorService);
31     }
32     
33     /**
34      * FixedThreadPool使用的Thrad对象的数量是有限的,如果提交
35      * 的任务数量大于限制的最大线程数,那么这些任务讲排队,然
36      * 后当有一个线程的任务结束之后,将会根据调度策略继续等待
37      * 执行下一个任务或者销毁
38      * @param number 限制 FixedThreadPool 中的线程对象的数量
39      */
40     public static void testFixedThreadPool(int number) {
41         if (number == 0) {
42             number = 3;    //DEFAULT
43         }
44         ExecutorService executorService = Executors.newFixedThreadPool(number);
45         createTask(executorService);
46     }
47     
48     /**
49      * SingleThreadExecutor就是线程数量为1的FixedThreadPool。
50      * 如果提交了多个任务,那么这些任务将会排队,每个任务都会在
51      * 下一个任务开始之前运行结束,所有的任务将会使用相同的线程
52      */
53     public static void testSingleThread() {
54         ExecutorService executorService = Executors.newSingleThreadExecutor();
55         createTask(executorService);
56     }
57 }
复制代码

 

 参考资料:

1.《Java编程思想》第4版 P656

2.诗剑书生的专栏 :http://blog.csdn.net/axman/article/details/1481197

3.狂飙的蜗牛:http://blog.csdn.net/xjtuse_mal/article/details/5687368

4.洞玄的博客:http://dongxuan.iteye.com/blog/901689

5.Java官网文档:http://docs.oracle.com/javase/6/docs/api/

  

[本文为原创,转载请注明出处: http://www.cnblogs.com/xiaoxuetu ]
 
分类: java
posted on 2013-05-08 10:03  HackerVirus  阅读(209)  评论(0编辑  收藏  举报