Java学习笔记总结篇之集合类和多线程

Map List Set等集合类:

一、概述

  1. Collection  
  2.     |--List:元素是有序的。元素可以重复,因为有索引  
  3.          |--ArrayList:底层使用数组结构。查询快,但是增加、删除慢。可变长度数组,默认创建是10,如果添加时超过,则会新建一个ArrayList长度为原长的150%,之后把所有元素copy进新的ArrayList。   
  4.          |--LinkedList:链表数据结构。增加、删除快,查询慢。  
  5.             特有方法:addFirst() addLast() getFirst() getLast() removeFirst() removeLast()  
  6.             1.6之后新方法:peekFirst()....  
  7.          |--Vector:数组数据结构。和ArrayList差不多,比ArrayList早,是线程同步的。增删查都满。一般不用。可变长度,...200%...。支持枚举取元素(也支持迭代器、遍历)。     
  8.     |--Set:元素无序。元素不可以重复  
  9.          |--HashSet:底层数据结构式哈希表。无序,不能重复。  
  10.             HashSet如何保证元素唯一性?  
  11.                 是通过元素的两个方法,hashCode()和equals()来完成。  
  12.             如果元素的HashCode值相同,才会判断equals是否为true。  
  13.             注意,对于判断元素是否存在、及删除都是依赖于以上两个方法。而ArrayList只依赖于equals()       
  14.          |--TreeSet:可以对Set集合中的元素进行自然排序。  
  15.             往TreeSet中存放的对象具有比较性。(可以实现Comparable,并覆盖int compareTo()方法,强制使其具备可比较性)  
  16.             排序时,当主要条件相同时一定要判断次要条件。  
  17.             不能存入重复元素。  
  18.             *使用二叉树,仿堆栈?仿队列?  
  19.             compareTo()返回1 0 -1(各含义)  

  1. Map<K,V>   映射关系。和Set很像,其实Set底层就是使用Map集合  
  2.     |--Hashtable:底层是哈希表数据结构。不允许存入null建和值。线程同步。api 1.0 效率低下  
  3.     |--HashMap:底层是哈希表数据结构。允许存入null建和值。线程不同步。api 1.0 效率高  
  4.     |--TreeMap:底层是二叉树。可以用于给map中的键进行排序。线程不同步。 

TreeSet集合排序方式

1、实现Comparable接口,覆盖compareTo()方法
2、当元素不具备可比较性时,让集合具有可比性。定义一个比较器,并在集合在初始化时传递给TreeSet的构造函数

当两种排序都存在时,以比较器为主


在JAVA的util包中有两个所有集合的父接口Collection和Map,它们的父子关系:


+Collection 这个接口extends自 --java.lang.Iterable接口
 ├+List(接口 代表有序,可重复的集合。列表)
 │├ ArreyList     (Class 数组,随机访问,没有同步,线程不安全)
 │├ Vector        (Class  数组                   同步        线程全)
 │├ LinkedList    (Class  链表   插入删除   没有同步   线程不安全)
 │└ Stack          (Class)
 └+Set(接口 不能含重复的元素。仅接收一次并做内部排序,集)
 │├ HashSet            (Class)
 │├ LinkedHashSet   (Class)
 │└ TreeSet       (Class)

+Map(接口)
 ├ +Map(接口 映射集合)
 │ ├ HashMap            (Class 不同步,线程不安全。除了不同和允许使用null 键值之外,与Hashtable大致相同)
 │ ├ Hashtable           (Class 同步   ,线程安全    。不允许实施null 键值)
 │ ├ +SortedMap 接口
 │ │   ├ TreeMap         (Class)
 │ ├ WeakHashMap     (Class)

以下对众多接口和类的简单说明:首先不能不先说一下数组(Array)

1、效率高,但容量固定且无法动态改变。array还有一个缺点是,无法判断其中实际存有多少元素,length只是告诉我们array的容量。
2、Java中有一个Arrays类,专门用来操作array。
     arrays中拥有一组static函数,
     equals():比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。
     fill():将值填入array中。
     sort():用来对array进行排序。
     binarySearch():在排好序的array中寻找元素。
     System.arraycopy():array的复制。

一、数组Array和集合的区别:

1)  数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型)
2) JAVA集合可以存储和操作数目不固定的一组数据。
3)  若程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,array不适用。

二 set map list的区别  

都是集合接口

  set --其中的值不允许重复,无序的数据结构 
  list   --其中的值允许重复,因为其为有序的数据结构 
  map--成对的数据结构,健值必须具有唯一性(键不能同,否则值替换) 

List 按对象进入的顺序保存对象,不做排序或编辑操作。

Set对每个对象只接受一次,并使用自己内部的排序方法(通常,你只关心某个元素是否属于Set,而不关心它的顺序--否则应该使用List)。

Map同样对每个元素保存一份,但这是基于"键"的,Map也有内置的排序,因而不关心元素添加的顺序。如果添加元素的顺序对你很重要,应该使用 LinkedHashSet或者LinkedHashMap.

Collection 是对象集合, Collection 有两个子接口 List 和 Set

List 可以通过下标 (1,2..) 来取得值,值可以重复


而 Set 只能通过游标来取值,并且值是不能重复的


ArrayList , Vector , LinkedList 是 List 的实现类
ArrayList 是线程不安全的, Vector 是线程安全的,这两个类底层都是由数组实现的
LinkedList 是线程不安全的,底层是由链表实现的  


Map 是键值对集合
HashTable 和 HashMap 是 Map 的实现类
HashTable 是线程安全的,不能存储
null
HashMap 不是线程安全的,可以存储
null

三、 Collections类和Collection接口

         Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

       Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些 Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的 类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。

  所有实现 Collection 接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的 Collection ,有一个 Collection 参数的构造函数用于创建一个新的 Collection ,这个新的 Collection 与传入的 Collection 有相同的元素。后一个构造函数允许用户复制一个 Collection 。

集合类的遍历:遍历通用Collection:

  如何遍历 Collection 中的每一个元素?不论 Collection 的实际类型如何,它都支持一个 iterator() 的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问 Collection 中每一个元素。典型的用法如下:

 

  1. Iterator it = collection.iterator(); // 获得一个迭代子  
  2.   while(it.hasNext()) {  
  3.    Object obj = it.next(); // 得到下一个元素  
  4. }  

由 Collection 接口派生的两个接口是 List 和 Set 。 List 按对象进入的顺序保存对象,不做排序或编辑操作。 Set 对每个对象只接受一次,并使用自己内部的排序方法 ( 通常,你只关心某个元素是否属于 Set, 而不关心它的顺序 -- 否则应该使用 List) 。

四、 List接口,有序可重复的集合

实际上有两种List: 一种是基本的ArrayList,其优点在于随机访问元素,另一种是更强大的LinkedList,它并不是为快速随机访问设计的,而是具有一套更通用的方法。 

List : 次序是List最重要的特点:它保证维护元素特定的顺序。List为Collection添加了许多方法,使得能够向List中间插入与移除元素(这只推荐LinkedList使用。)一个List可以生成ListIterator,使用它可以从两个方向遍历List,也可以从List中间插入和移除元素。 

1. ArrayList类

1) ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。
2) size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。
3) 每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法 并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
4) 和LinkedList一样,ArrayList也是非同步的(unsynchronized)。

5) 由数组实现的List。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。ListIterator只应该用来由后向前遍历ArrayList,而不是用来插入和移除元素。因为那比LinkedList开销要大很多。
2. Vector类
  Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。

3. LinkedList类
  LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在 LinkedList的首部或尾部。如下列方法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 这些方法 (没有在任何接口或基类中定义过)。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
  注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
  List list = Collections.synchronizedList(new LinkedList(...));

4. Stack 类
  Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

用法:

  1. package Test;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.Iterator;  
  5. import java.util.List;  
  6. public class TestList {  
  7.     public static void main(String dd[]) {  
  8.         // new了一个存储list  
  9.         List l = new ArrayList();  
  10.         // 因为Collection framework只能存储对象所以new封装类  
  11.         l.add(new Integer(1));  
  12.         l.add(new Integer(2));  
  13.         l.add(new Integer(3));  
  14.         l.add(new Integer(4));  
  15.   
  16.         Iterator it = l.iterator();  
  17.         //使用 迭代器(Iterator):  
  18.         // hasNext是取值取的是当前值.他的运算过程是判断下个是否有值如果有继续.  
  19.         while (it.hasNext()) {  
  20.             System.out.println("iterator:Element in list is :   " + it.next());  
  21.         }  
  22.         //用for循环和get()方法:  
  23.         for (int i = 0; i < l.size(); i++) {  
  24.             System.out.println("for:Element in list is :   " + l.get(i));  
  25.         }  
  26.     }  
  27. }  
LinkedList

  1. package Test;  
  2.   
  3. import java.util.Iterator;  
  4. import java.util.LinkedList;  
  5.   
  6. public class TestLinkedList {  
  7.     public static void main(String arg[]) {  
  8.         LinkedList ll = new LinkedList();// 声明LinkedList并实例化  
  9.         // 使用add()方法添加元素  
  10.         ll.add("a");  
  11.         ll.add("b");  
  12.         ll.add("c");  
  13.         // 使用Iterator迭代器遍历出集合的元素并打印  
  14.         Iterator it = ll.iterator();  
  15.         while (it.hasNext()) {  
  16.             System.out.println(it.next());  
  17.         }  
  18.         System.out.println("------------------");  
  19.         // 向链表头和尾分别添加x和z  
  20.         ll.addFirst("z");  
  21.         ll.addLast("x");  
  22.         // 遍历查看添加后的结果  
  23.         for (Iterator i = ll.iterator(); i.hasNext();) {  
  24.             System.out.println(i.next());  
  25.         }  
  26.     }  
  27. }  


ArrayList和LinkedList的区别。

1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

如果熟悉数据结构的同学,就会一下明白,ArrayList就是线性表的顺序表示,LinkedList就是线性表的链表表示。

五、 Set接口,代表无序,不可重复的集合


        Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set不保存重复的元素(至于如何判断元素相同则较为负责) 
Set : 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。 

1. HashSet 

     为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。 
2. TreeSet 

     保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。 
3. LinkedHashSet 

     具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。

用法:

  1. Set set=new HashSet();  
  2.     String s1=new String("hello");  
  3.     String s2=s1;  
  4.     String s3=new String("world");  
  5.     set.add(s1);  
  6.     set.add(s2);  
  7.     set.add(s3);  
  8.     System.out.println(set.size());//打印集合中对象的数目 为 2。  
  9.     Set 的 add()方法是如何判断对象是否已经存放在集合中?  
  10.     boolean isExists=false;  
  11.     Iterator iterator=set.iterator();  
  12.     while(it.hasNext())  {  
  13.         String oldStr=it.next();  
  14.         if(newStr.equals(oldStr)){  
  15.            isExists=true;  
  16.         }  
  17.     }  

六、 Map接口:映射


Map没有继承Collection接口, Map 提供 key 到 value 的映射,你可以通过“键”查找“值”。一个 Map 中不能包含相同的 key ,每个 key 只能映射一个 value 。 Map 接口提供3 种集合的视图, Map 的内容可以被当作一组 key 集合,一组 value 集合,或者一组 key-value 映射。

方法 put(Object key, Object value) 添加一个“值” ( 想要得东西 ) 和与“值”相关联的“键” (key) ( 使用它来查找 ) 。方法get(Object key) 返回与给定“键”相关联的“值”。可以用 containsKey() 和 containsValue() 测试 Map 中是否包含某个“键”或“值”。 标准的 Java 类库中包含了几种不同的 Map : HashMap, TreeMap, LinkedHashMap, WeakHashMap, IdentityHashMap 。它们都有同样的基本接口 Map ,但是行为、效率、排序策略、保存对象的生命周期和判定“键”等价的策略等各不相同。


Map 同样对每个元素保存一份,但这是基于
"" 的, Map 也有内置的排序,因而不关心元素添加的顺序。如果添加元素的顺序对你很重要,应该使用 LinkedHashSet 或者 LinkedHashMap.

执行效率是 Map 的一个大问题。看看
get() 要做哪些事,就会明白为什么在 ArrayList 中搜索“键”是相当慢的。而这正是 HashMap 提高速度的地方。 HashMap 使用了特殊的值,称为“散列码” (hash code) ,来取代对键的缓慢搜索。“散列码”是“相对唯一”用以代表对象的int 值,它是通过将该对象的某些信息进行转换而生成的(在下面总结二:需要的注意的地方有更进一步探讨)。所有 Java 对象都能产生散列码,因为 hashCode() 是定义在基类 Object 中的方法 。 HashMap 就是使用对象的 hashCode() 进行快速查询的。此方法能够显著提高性能。

1.  Hashtable类
  Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。Hashtable是同步的。

  添加数据使用 put(key, value) ,取出数据使用get(key) ,这两个基本操作的时间开销为常数。
Hashtable 通过初始化容量 (initial capacity) 和负载因子 (load factor) 两个参数调整性能。通常缺省的 load factor
0.75 较好地实现了时间和空间的均衡。增大 load factor 可以节省空间但相应的查找时间将增大,这会影响像get 和 put 这样的操作。
使用 Hashtable 的简单示例如下,将
123 放到 Hashtable 中,他们的 key 分别是 ”one” , ”two” , ”three” :
     Hashtable numbers
=new Hashtable();
     numbers.put(“one”,
new Integer(1));
     numbers.put(“two”,
new Integer(2));
     numbers.put(“three”,
new Integer(3));
  要取出一个数,比如
2 ,用相应的 key :
     Integer n
= (Integer)numbers.get(“two”);
     System.
out.println(“two=+ n);
   由于作为 key 的对象将通过计算其散列函数来确定与之对应的 value 的位置,因此任何作为 key 的对象都必须实现 hashCode 方法和 equals 方法。 hashCode 方法和 equals 方法继承自根类 Object ,如果你用自定义的类当作 key 的话,要相当小心,按照散列函数的定义,如果两个对象相同,即 obj1.equals(obj2)
=true ,则它们的 hashCode 必须相同,但如果两个对象不同,则它们的 hashCode 不一定不同,如果两个不同对象的 hashCode 相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的 hashCode() 方法,能加快哈希表的操作。
  如果相同的对象有不同的 hashCode ,对哈希表的操作会出现意想不到的结果(期待的
get 方法返回null ),要避免这种问题,只需要牢记一条:要同时复写 equals 方法和 hashCode 方法,而不要只写其中一个。
   Hashtable 是同步的。

2.  HashMap类
  HashMap和Hashtable类似,也是基于hash散列表的实现。不同之处在于 HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时 (values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要 将HashMap的初始化容量设得过高,或者load factor过低。

   LinkedHashMap 类:类似于 HashMap ,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用 (LRU) 的次序。只比 HashMap 慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。

3.  WeakHashMap类 (弱键( weak key ))
  WeakHashMap是一种改进的HashMap,它是为解决特殊问题设计的,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收。

4. TreeMap 类
基于红黑树数据结构的实现。查看“键”或“键值对”时,它们会被排序 ( 次序由 Comparabel 或 Comparator 决定 ) 。 TreeMap 的特点在于,你得到的结果是经过排序的。 TreeMap 是唯一的带有 subMap() 方法的 Map ,它可以返回一个子树。

5. IdentifyHashMap 类
使用
== 代替 equals() 对“键”作比较的 hash map 。专为解决特殊问题而设计。

 用法:

1 添加,删除操作:

  1. Object put(Object key, Object value): 向集合中加入元素  
  2. Object remove(Object key):   删除与KEY相关的元素  
  3. void putAll(Map t):   将来自特定映像的所有元素添加给该映像  
  4. void clear(): 从映像中删除所有映射  
    2 查询操作:
    Object get(Object key): 获得与关键字key相关的值
    Map集合中的键对象不允许重复,也就说,任意两个键对象通过equals()方法比较的结果都是false.
    但是可以将任意多个键独享映射到同一个值对象上。
    Conllections : 集合实用类
    Conllections提供了供JAVA集合实用的静态方法

七、 如何选择

1、容器类和Array的区别、择取
      1)容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。
      2)一旦将对象置入容器内,便损失了该对象的型别信息。

2、
     1)  在各种Lists中,最好的做法是以ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList();
           Vector总是比ArrayList慢,所以要尽量避免使用。
      2) 在各种Sets中,HashSet通常优于HashTree(插入、查找)。只有当需要产生一个经过排序的序列,才用TreeSet。
           HashTree存在的唯一理由:能够维护其内元素的排序状态。
      3) 在各种Maps中,HashMap用于快速查找。
      4)  当元素个数固定,用Array,因为Array效率是最高的。

结论:最常用的是ArrayList,HashSet,HashMap,Array。而且,我们也会发现一个规律,用TreeXXX都是排序的。

注意:

1、Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。
2、Set和Collection拥有一模一样的接口。
3、List可以通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get)
4、一般使用ArrayList。用LinkedList构造堆栈stack、队列queue

5、Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。
      HashMap会利用对象的hashCode来快速找到key。
    *     hashing
          哈希码就是将对象的信息经过一些转变形成一个独一无二的int值,这个值存储在一个array中。
          我们都知道所有存储结构中,array查找速度是最快的。所以,可以加速查找。
      
          发生碰撞时,让array指向多个values。即,数组每个位置上又生成一个梿表。

6、Map中元素,可以将key序列、value序列单独抽取出来。
使用keySet()抽取key序列,将map中的所有keys生成一个Set。
使用values()抽取value序列,将map中的所有values生成一个Collection。

为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复。


Java多线程


线程是一个单独程序流程。多线程是指一个程序可以同时运行多个任务,每个任务由一个单独的线程来完成。也就是说,多个线程可以同时在一个程序中运行,并且每一个线程完成不同的任务。程序可以通过控制线程来控制程序的运行,例如线程的等待、休眠、唤起线程等。本章将向读者介绍线程的机制、如何操作和使用线程以及多线程编程。 

1. 线程的基本知识

        线程是程序运行的基本单位,一个程序中可以同时运行多个线程。如果程序被设置为多线程, 可以提高程序运行的效率和处理速度。 Java中线程的实现通常有两种方法: 派生 Thread类和实现 Runnable接口。本节主要讲述线程的概念和创建线程的方法。 

1. 什么是线程 

        传统的程序设计语言同一时刻只能执行单任务操作,效率非常低,如果网络程序在接收数据时发生阻塞,只能等到程序接收数据之后才能继续运行。随着 Internet 的飞速发展,这种单任务运行的状况越来越不被接受。如果网络接收数据阻塞,后台服务程序就会一直处于等待状态而不能继续任何操作。 这种阻塞情况经常发生, 这时的 CPU资源完全处于闲置状态。         多线程实现后台服务程序可以同时处理多个任务,并不发生阻塞现象。多线程是 Java 语言的一个很重要的特征。 多线程程序设计最大的特点就是能够提高程序执行效率和处理速度。Java 程序可同时并行运行多个相对独立的线程。例如创建一个线程来接收数据,另一个线程发送数据,既使发送线程在接收数据时被阻塞,接受数据线程仍然可以运行。 线程(Thread)是控制线程(Thread of Control)的缩写,它是具有一定顺序的指令序列(即所编写的程序代码)、存放方法中定义局部变量的栈和一些共享数据。线程是相互独立的,每个方法的局部变量和其他线程的局部变量是分开的,因此,任何线程都不能访问除自身之外的其他线程的局部变量。如果两个线程同时访问同一个方法,那每个线程将各自得到此方法的一个拷贝。        Java 提供的多线程机制使一个程序可同时执行多个任务。线程有时也被称为小进程,它是从一个大进程里分离出来的小的独立的线程。由于实现了多线程技术,Java 显得更健壮。多线程带来的好处是更好的交互性能和实时控制性能。多线程是强大而灵巧的编程工具,但要用好它却不是件容易的事。在多线程编程中,每个线程都通过代码实现线程的行为,并将数据供给代码操作。编码和数据有时是相当独立的,可分别向线程提供。多个线程可以同时处理同一代码和同一数据,不同的线程也可以处理各自不同的编码和数据。 

2 .创建线程方法

       Java程序都是声明一个公共类,并在类内实现一个 main 方法。事实上,这些程序就是一个单线程程序。当它执行完main 方法的程序后,线程正好退出,程序同时结束运行。

  1. public class OnlyThread {  
  2.     public static void main(String args[]) {  
  3.         run(); // 调用静态run()方法  
  4.     }  
  5.   
  6.     /** 
  7.      * 实现run()方法 
  8.      */  
  9.     public static void run() {  
  10.         // 循环计算输出的*数目  
  11.         for (int count = 1, row = 1; row < 10; row++, count++) {  
  12.             for (int i = 0; i < count; i++) { // 循环输出指定的count数目的*  
  13.                 System.out.print('*');   
  14.             }  
  15.             System.out.println();  
  16.         }  
  17.     }  
  18. }  

         这 只是建立了一个单一线程并执行的普通小程序,并没有涉及到多线程的概念。

      在 Java程序中,有两种方法创建线程:
        一是对 Thread 类进行派生并覆盖 run方法;

        二是通过实现 runnable接口创建。 

3. Thread 创建线程

     在程序中创建新的线程的方法之一是继承 Thread 类, 并通过 Thread子类声明线程对象。继承Thread 类并覆盖 Thread类的 run 方法完成线程类的声明, 通过new创建派生线程类的线程对象。run 中的代码实现了线程的行为。 

      java.lang.Thread 类是一个通用的线程类,由于默认情况下 run 方法是空的,直接通过 Thread类实例化的线程对象不能完成任何事,所以可以通过派生 Thread 类,并用具体程序代码覆盖Thread 类中的 run 方法,实现具有各种不同功能的线程类。

      1) Thread 创建线程步骤: 

     (1)创建一个新的线程类,继承 Thread 类并覆盖 Thread 类的 run()方法。 
  1. class ThreadType extends Thread{   
  2.      public void run(){   
  3.          ……   
  4.      }   
  5. }   
    (2)创建一个线程类的对象,创建方法与一般对象的创建相同,使用关键字new完成。  
  1. ThreadType  tt = new ThreadType();   
    (3)启动新线程对象,调用 start()方法。 
  1. tt.start();   
    (4)线程自己调用 run()方法。 
  1. void run();   

     2) Thread创建一个线程

     下面是通过Thread创建线程的例子:产生一个新的线程 

  1. class ThreadDemo1 extends Thread {  
  2.     ThreadDemo1() {  
  3.     }  
  4.   
  5.     // 声明ThreadDemo1带参数的构造方法  
  6.     ThreadDemo1(String szName) {  
  7.         super(szName);  
  8.     }  
  9.   
  10.     // 重载run函数  
  11.     public void run() {  
  12.         for (int count = 1, row = 1; row < 10; row++, count++) {  
  13.             for (int i = 0; i < count; i++) {// 循环输出指定的count数目的*  
  14.                 System.out.print('*');  
  15.             }  
  16.             System.out.println();  
  17.         }  
  18.     }  
  19.   
  20.     public static void main(String argv[]) {  
  21.         ThreadDemo1 td = new ThreadDemo1(); // 创建,并初始化ThreadDemo1类型对象td  
  22.         td.start(); // 调用start()方法执行一个新的线程  
  23.     }  
  24. }  

    OnlyThread.java程序与程序ThreadDemo1 .java 表面上看运行结果相同,但是仔细对照会发现,程序 OnlyThread.java 中对 run方法的调用在程序 ThreadDemo1.java 中变成了对 start 方法的调用,并且程序 ThreadDemo1.java 确派生 Thread类,创建新的线程类。 

    3) Thread创建多个线程

//文件:程序10.3 ThreadDemo2.java   描述:产生三个新的线程 
     
  1. public class ThreadDemo2 extends Thread {  
  2.     // 声明无参数,空构造方法  
  3.     ThreadDemo2() {  
  4.     }  
  5.   
  6.     // 声明带有字符串参数的构造方法  
  7.     ThreadDemo2(String szName) {  
  8.         super(szName); // 调用父类的构造方法  
  9.     }  
  10.   
  11.     // 重载run函数  
  12.     public void run() {  
  13.         for (int count = 1, row = 1; row < 10; row++, count++) {  
  14.             for (int i = 0; i < count; i++) {// 循环输出指定的count数目的*  
  15.                 System.out.print('*');  
  16.             }  
  17.             System.out.println();  
  18.         }  
  19.     }  
  20.   
  21.     public static void main(String argv[]) {  
  22.         ThreadDemo2 td1 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td1  
  23.         ThreadDemo2 td2 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td2  
  24.         ThreadDemo2 td3 = new ThreadDemo2(); // 创建,并初始化ThreadDemo2类型对象td3  
  25.         td1.start(); // 启动线程td1  
  26.         td2.start(); // 启动线程td2  
  27.         td3.start(); // 启动线程td3  
  28.     }  
  29. }  

创建了 3 个线程 td1、td2、td3,它们分别执行自己的 run方法。在实际中运行的结果并不是想要的直角三角形, 而是一些乱七八糟的 “*” 行,长短并没有一定的规律,这是因为线程并没有按照程序中调用的顺序来执行, 而是产生了多个线程赛跑现象。 运行结果:



注意:Java线程并不能按调用顺序执行,而是并行执行的单独代码。如果要想得到完整的直角三角形,需要在执行一个线程之前,判断程序前面的线程是否终止,如果已经终止,再来调用该线程。 

4. Runnable 接口创建线程 

        通过实现 Runnable 接口的方法是创建线程类的第二种方法。利用实现 Runnable 接口来创建线程的方法可以解决 Java 语言不支持的多重继承问题。                Runnable 接口提供了 run()方法的原型,因此创建新的线程类时,只要实现此接口,即只要特定的程序代码实现Runnable接口中的 run()方法,就可完成新线程类的运行。

      扩展Thread类创建线程的方式,适合编写简单的应用程序代码,而实现Runnable接口创建线程,能够避免Java单继承的局限,适合同一代码的多线程处理同一资源的情况,代码具有良好的一致性,是更符合面向对象思想的设计方式。

     1) Runnable 创建线程步骤 

   (1)创建一个实现 Runnable 接口的类,并且在这个类中重写 run 方法。 
  1. class ThreadType implements Runnable{   
  2.      public void run(){   
  3.          ……   
  4.      }   
  5. }   
   (2)使用关键字 new新建一个 ThreadType 的实例。 
  1. Runnable rb = new ThreadType ();   
   (3)通过 Runnable 的实例创建一个线程对象,在创建线程对象时,调用的构造函数是 new Thread(ThreadType),它用 ThreadType 中实现的 run()方法作为新线程对象的 run()方法。 
  1. Thread td = new Thread(rb);   
(4)通过调用 ThreadType 对象的 start()方法启动线程运行。 
  1. td.start();   

     2) Runnable 创建线程

  1. class ThreadDemo3 implements Runnable {  
  2.     // 重载run函数  
  3.     public void run() {  
  4.         for (int count = 1, row = 1; row < 10; row++, count++){ // 循环计算输出的*数目  
  5.             for (int i = 0; i < count; i++){ // 循环输出指定的count数目的*  
  6.                 System.out.print('*');   
  7.             }  
  8.             System.out.println();   
  9.         }  
  10.     }  
  11.   
  12.     public static void main(String argv[]) {  
  13.         Runnable rb = new ThreadDemo3(); // 创建,并初始化ThreadDemo3对象rb  
  14.         Thread td = new Thread(rb); // 通过Thread创建线程  
  15.         td.start(); // 启动线程td  
  16.     }  
  17. }  


2. 线程的状态

     线程的整个周期由线程创建、可运行状态、不可运行状态和退出等部分组成,这些状态之间的转化是通过线程提供的一些方法完成的。

1.线程周期

      一个线程有4 种状态,任何一个线程都处于这4种状态中的一种状态: 
      1) 创建(new)状态:调用 new方法产生一个线程对象后、调用 start 方法前所处的状态。线程对象虽然已经创建,但还没有调用 start 方法启动,因此无法执行。当线程处于创建状态时,线程对象可以调用 start 方法进入启动状态,也可以调用 stop 方法进入停止状态。 
     2)可运行(runnable)状态:当线程对象执行 start()方法后,线程就转到可运行状态。进入此状态只是说明线程对象具有了可以运行的条件,但线程并不一定处于运行状态。因为在单处理器系统中运行多线程程序时,一个时间点只有一个线程运行,系统通过调度机制实现宏观意义上的运行线程共享处理器。 因此一个线程是否在运行,除了线程必须处于 Runnable 状态之外,还取决于优先级和调度。 
    3)不可运行(non Runnable)状态:线程处于不可运行状态是由于线程被挂起或者发生阻塞,例如对一个线程调用 wait()函数后,它就可能进入阻塞状态;调用线程的notify或notifyAll 方法后它才能再次回到可执行状态。 
     4)退出(done)状态:一个线程可以从任何一个状态中调用 stop 方法进入退出状态。线程一旦进入退出状态就不存在了,不能再返回到其他的状态。除此之外,如果线程执行完 run方法,也会自动进入退出状态。 
     创建状态、可运行状态、不可运行状态、退出状态之间的转换关系如图 所示。 

          
        通过 new第一次创建线程时,线程位于创建状态,这时不能运行线程,只能等待进一步的方法调用改变其状态。然后,线程通过调用 start方法启动
线程,并进入可执行状态,或者调用方法 stop进入退出状态。当程序位于退出状态时,线程已经结束执行,这是线程的最后一个状态,并且不能转化到其他状态。当程序的所有线程位于退出状态时,程序会强行终止。当线程位于可执行状态时,在一个特定的时间点上,每一个系统处理器只能运行一个线程。 此时如果线程被挂起,执行就会被中断或者进入休眠状态,那么线程将进入不可执行状态,并且不可执行状态可以通过 resume、notify等方法返回到可执行状态。表10-1列举了线程状态转换的函数。 

       线程状态转换函数: 
方法                        描述                                                          有效状态             目的状态 
start()                     开始执行一个线程                                     New                    Runnable 
stop()                     结束执行一个线程                                     New或Runnable    Done 
sleep(long)          暂停一段时间,这个时间为给定的毫秒  Runnable             NonRunnable 
sleep(long,int)     暂停片刻,可以精确到纳秒                     Runnable             NonRunnable 
suspend()            挂起执行                                                      Runnable             NonRunnable 
resume()              恢复执行                                                      NonRunnable        Runnable 
yield()                    明确放弃执行                                              Runnable             Runnable 
wait()                     进入阻塞状态                                              Runnable             NonRunnable 
notify()                   阻塞状态解除                                              NonRunnable       Runnable 
注意:stop()、suspend()和 resume()方法现在已经不提倡使用,这些方法在虚拟机中可能引起“死锁”现象。suspend()和 resume()方法的替代方法是 wait()和 sleep()。线程的退出通常采用自然终止的方法,建议不要人工调用 stop()方法。 

2  线程的创建和启动 

      Java 是面向对象的程序设计语言,设计的重点就是类的设计与实现。Java 利用线程类Thread 来创建线程,线程的创建与普通类对象的创建操作相同。Java通过线程类的构造方法创建一个线程,并通过调用 start 方法启动该线程。 
       实际上,启动线程的目的就是为了执行它的 run()方法,而 Thread 类中默认的 run()方法没有任何可操作代码,所以用 Thread类创建的线程不能完成任何任务。为了让创建的线程完成特定的任务,必须重新定义 run()方法。在第一节中已经讲述过,Java 通常有两种重新定义run()方法的方式:

      1)派生线程类 Thread 的子类,并在子类中重写 run()方法:Thread 子类的实例对象是一个线程对象,并且该线程有专门定制的线程 run()方法,启动线程后就执行子类中重写的 run()方法。 
‰      2)实现 Runnable 接口并重新定义 run()方法:先定义一个实现 Runnable()接口的类,在该类中定义 run()方法,然后创建新的线程类对象,并以该对象作为 Thread 类构造方法的参数创建一个线程。 
      
      注意:调用线程的 run()方法是通过启动线程的start()方法来实现的。 因为线程在调用start()方法之后,系统会自动调用 run()方法。与一般方法调用不同的地方在于一般方法调用另外一个方法后,必须等被调用的方法执行完毕才能返回,而线程的 start()方法被调用之后,系统会得知线程准备完毕并且可以执行run()方法,start()方法就返回了,start()方法不会等待run()方法执行完毕。  

3.  线程状态转换 

1.线程进入可执行状态 
    当以下几种情况发生时,线程进入可执行状态。 
(1)其他线程调用notify()或者 notifyAll()方法,唤起处于不可执行状态的线程。 
      public final void notify() 
      public final void notifyAll() 
     notify 仅仅唤醒一个线程并允许它获得锁,notifyAll 唤醒所有等待这个对象的线程,并允许它们获得锁。
(2)线程调用 sleep(millis)方法,millis毫秒之后线程会进入可执行状态。 
         static void sleep(long millis) throws InterruptedException 在 millis 毫秒数内让当前正在执行的线程进入休眠状态,等到时间过后,该线程会自动苏醒并继续执行。sleep方法的精确度受到系统计数器的影响。 
        static void sleep(long millis, int nanos) throws InterruptedException 在毫秒数(millis)加纳秒数(nanos)内让当前正在执行的线程进入休眠状态,此操作的精确度也受到系统计数器的影响。 

(3)线程对I/O操作的完成。 

2.线程进入不可执行状态 
    当以下几种情况发生时,线程进入不可执行状态。 
(1)线程自动调用 wait()方法,等待某种条件的发生。 
      public final void wait() throws InterruptedException 
     当其他线程调用 notify()方法或 notifyAl()方法后,处于等待状态的线程获得锁之后才会被唤醒,然后该线程一直等待重新获得对象锁才继续运行。 
(2)线程调用 sleep()方法进入不可执行状态,在一定时间后会进入可执行状态。 
(3)线程等待 I/O操作的完成。 

   线程阻塞的例子。 

   

  1. public class  ThreadSleep   
  2. {   
  3.  public static void main(String[ ] args)    
  4.  {   
  5.   SubThread st = new SubThread("SubThread");  //创建,并初始化SubThread 对象st   
  6.   st.start();         //启动线程st   
  7.  }   
  8. }   
  9.    
  10. class SubThread extends Thread{   
  11.  SubThread(){}        //声明,实现SubThread无参数构造方法   
  12.  //声明,实现SubThread带字符串参数构造方法   
  13.  SubThread(String Name)   
  14.  {   
  15.   super(Name);        //调用父类的构造方法   
  16.  }   
  17.  //重载run函数   
  18.  public void run()   
  19.  {   
  20.   for (int count = 1,row = 1; row < 10; row++,count++) //循环计算输出的*数目   
  21.   {   
  22.    for (int i = 0; i < count; i++)      //循环输出指定的count数目的*   
  23.    {   
  24.     System.out.print('*');     //输出*   
  25.    }   
  26.    try         //try-catch块,用于捕获异常   
  27.    {     Thread.sleep(1000);     //线程休眠1秒钟   
  28.     System.out.print("\t wait........");   
  29.    }   
  30.    catch (InterruptedException e)    //捕获异常InterruptedException   
  31.    {   
  32.     e.printStackTrace();     //异常抛出信息   
  33.    }   
  34.    System.out.println();      //输出换行符   
  35.   }   
  36.  }   
  37. }   

      程序ThreadSleep 中,每输出一行*就要休息1 秒钟。当执行 sleep()语句后, 线程进入不可执行状态等待1 秒钟之后,线程 st 会自动苏醒并继续执行。由于 sleep

方法抛出 InterruptedException异常, 所以在调用时必须捕获异常。 

4 .等待线程结束 

     isAlive()方法用来判断一个线程是否存活。当线程处于可执行状态或不可执行状态时,isAlive()方法返回 true; 当线程处于创建状态或退出状态时, 则返回 false。 也就是说, isAlive()方法如果返回 true,并不能判断线程是处于可运行状态还是不可运行状态。isAlive()方法的原型如下所示。 
      public final boolean isAlive() 

      该方法用于测试线程是否处于活动状态。活动状态是指线程已经启动(调用 start方法)且尚未退出所处的状态,包括可运行状态和不可运行状态。可以通过该方法解决程序 10.3中的问题,先判断第一个线程是否已经终止,如果终止再来调用第二个线程。这里提供两种方法:

      第一种方法是不断查询第一个线程是否已经终止,如果没有,则让主线程睡眠一直到它终止即“while/isAlive/sleep”,格式如下。 

  1. 线程1.start();   
  2. while(线程 1.isAlive()) {   
  3.  Thread.sleep(休眠时间);   
  4. }   
  5. 线程2.start();   
    第二种是利用 join()方法。 
    1)public final void join(long millis) throws InterruptedException 等待该线程终止的时间最长为毫秒(millis),超时为0 意味着要一直等下去。 
    2) public final void join(long millis,int nanos) throws InterruptedException 等待该线程终止的时间最长为毫秒(millis)加纳秒(nanos)。  
    3)public final void join() throws InterruptedException 等待该线程终止。 
    等待线程结束并执行另外一个线程的例子。   该例子 等待一个线程的结束的两种方法  :
  
  1. package Test;  
  2.   
  3. class WaitThreadStop extends Thread {  
  4.     // 声明,并实现WaitThreadStop无参数构造方法  
  5.     WaitThreadStop() {  
  6.     }  
  7.   
  8.     // 声明,并实现带有一个字符串参数的构造方法  
  9.     WaitThreadStop(String szName) {  
  10.         super(szName); // 调用父类的构造方法  
  11.     }  
  12.   
  13.     // 重载run函数  
  14.     public void run() {  
  15.         for (int count = 1, row = 1; row < 10; row++, count++) {  
  16.             for (int i = 0; i < count; i++) {  
  17.                 System.out.print('*'); // 输出*  
  18.             }  
  19.             System.out.println(); // 输出换行符  
  20.         }  
  21.     }  
  22. }  
  23.   
  24. public class WaitThreadStopMain {  
  25.     public static void main(String argv[ ]){   
  26.       WaitThreadStopMain test = new WaitThreadStopMain();    //创建,初始化WaitThreadStopMain对象test   
  27.       test.Method1();  //调用Method1方法   
  28.       //test.Method2();   
  29.      }  
  30.     // 第一种方法:while/isAlive/sleep  
  31.     public void Method1() {  
  32.         WaitThreadStop th1 = new WaitThreadStop(); // 创建,并初始化WaitThreadStop对象th1  
  33.         WaitThreadStop th2 = new WaitThreadStop(); // 创建,并初始化WaitThreadStop对象th2  
  34.         // 执行第一个线程  
  35.         th1.start();  
  36.         // 查询第一个线程的状态  
  37.         while (th1.isAlive()) {  
  38.             try {  
  39.                 Thread.sleep(100); // 休眠100毫秒  
  40.             } catch (InterruptedException e) {  
  41.                 e.printStackTrace(); // 异常信息输出  
  42.             }  
  43.         }  
  44.         // 当第一个线程终止后,运行第二个线程  
  45.         th2.start(); // 启动线程th2  
  46.     }  
  47.   
  48.     // 第二种方法,使用join方法实现等待其他线程结束  
  49.     public void Method2() {  
  50.         WaitThreadStop th1 = new WaitThreadStop(); // 创建, 并初始化WaitThreadStop对象th1  
  51.         WaitThreadStop th2 = new WaitThreadStop(); // 创建,并初始化WaitThreadStop对象th2  
  52.         // 执行第一个线程  
  53.         th1.start();  
  54.         try {  
  55.             th1.join(); // th1调用join 方法  
  56.         } catch (InterruptedException e) {  
  57.             e.printStackTrace(); // 异常信息输出  
  58.         }  
  59.         // 执行第二个线程  
  60.         th2.start();  
  61.     }  
  62. }  

3. 线程调度

      多线程应用程序的每一个线程的重要性和优先级可能不同,例如有多个线程都在等待获得CPU的时间片, 那么优先级高的线程就能抢占CPU并得以执行; 当多个线程交替抢占CPU时,优先级高的线程占用的时间应该多。因此,高优先级的线程执行的效率会高些,执行速度也会快些。 

       在 Java 中,CPU的使用通常是抢占式调度模式不需要时间片分配进程。抢占式调度模式是指许多线程同时处于可运行状态,但只有一个线程正在运行。当线程一直运行直到结束,或者进入不可运行状态,或者具有更高优先级的线程变为可运行状态,它将会让出 CPU。线程与优先级相关的方法如下:

      public final void setPriority(int newPriority) 设置线程的优先级为 newPriority :          

      newPriority 的值必须在 MIN_PRIORITY 到MAX_PRIORITY范围内,通常它们的值分别是1和10。目前Windows系统只支持3个级别的优

先级, 它们分别是Thread.MAX_PRIORITY、 Thread.MIN_PRIORITY和Thread.NORM_PRIORITY。  

      public final int getPriority() 获得当前线程的优先级。

     线程优先级的例子:

  1. class  InheritThread extends Thread {   
  2.     //自定义线程的run()方法   
  3.     public void run(){   
  4.          System.out.println("InheritThread is running…"); //输出字符串信息   
  5.          for(int i=0;i<10;i++){   
  6.               System.out.println(" InheritThread: i="+i);  //输出信息   
  7.               try{   
  8.                   Thread.sleep((int)Math.random()*1000); //线程休眠   
  9.              }   
  10.              catch(InterruptedException e)     //捕获异常   
  11.              {}   
  12.         }   
  13.     }   
  14. }   
       通过Runnable接口创建的另外一个线程 :

  1. class RunnableThread implements Runnable {  
  2.     // 自定义线程的run()方法  
  3.     public void run() {  
  4.         System.out.println("RunnableThread is running…"); // 输出字符串信息  
  5.         for (int i = 0; i < 10; i++) {  
  6.             System.out.println("RunnableThread : i=" + i); // 输出i  
  7.             try {  
  8.                 Thread.sleep((int) Math.random() * 1000); // 线程休眠  
  9.             } catch (InterruptedException e) { // 捕获异常  
  10.             }  
  11.         }  
  12.     }  
  13. }  
  14.   
  15. public class ThreadPriority {  
  16.     public static void main(String args[]) {  
  17.         // 用Thread类的子类创建线程  
  18.         InheritThread itd = new InheritThread();  
  19.         // 用Runnable接口类的对象创建线程  
  20.         Thread rtd = new Thread(new RunnableThread());  
  21.         itd.setPriority(5); // 设置myThread1的优先级5  
  22.         rtd.setPriority(5); // 设置myThread2的优先级5  
  23.         itd.start(); // 启动线程itd  
  24.         rtd.start(); // 启动线程rtd  
  25.     }  
  26. }  

在程序ThreadPriority.java中,线程 rtd 和 itd 具有相同的优先级,所以它们交互占用 CPU,宏观上处于并行运行状态。结果如图3. 

重新设定优先级: 

itd.setPriority(1);  //设置myThread1的优先级1 
rtd.setPriority(10); //设置myThread2的优先级10 
运行程序结果如图4所示。 


       图3相同优先级                                             图2  不同的优先级

从运行结构可以看出程序ThreadPriority.java修改后,由于设置了线程itd和 rtd 的优先级,并且 rtd的优先级较高,基本上是 rtd都优先抢占 CPU资源。 

4. 线程同步

        Java 应用程序中的多线程可以共享资源,例如文件、数据库、内存等。当线程以并发模式访问共享数据时,共享数据可能会发生冲突。Java引入线程同步的概念,以实现共享数据的一致性。线程同步机制让多个线程有序的访问共享资源,而不是同时操作共享资源。 

1  . 同步概念 

    在线程异步模式的情况下,同一时刻有一个线程在修改共享数据,另一个线程在读取共享数据,当修改共享数据的线程没有处理完毕,读取数据的线程肯定会得到错误的结果。如果采用多线程的同步控制机制,当处理共享数据的线程完成处理数据之后,读取线程读取数据。 
      通过分析多线程出售火车票的例子,可以更好得理解线程同步的概念。线程 Thread1 和线程 Thread2 都可以出售火车票,但是这个过程中会出现数据与时间信息不一致的情况。线程 Thread1 查询数据库,发现某张火车票 T 可以出售,所以准备出售此票;此时系统切换到线程Thread2执行, 它在数据库中查询存票, 发现上面的火车票T可以出售, 所以线程Thread2将这张火车票 T 售出;当系统再次切换到线程 Thread1 执行时,它又卖出同样的票 T。这是一个典型的由于数据不同步而导致的错误。 
     下面举一个线程异步模式访问数据的例子。 

  1. //文件:程序ThreadNoSynchronized.java   描述:多线程不同步的原因   
  2. class ShareData {  
  3.     public static String szData = ""// 声明,并初始化字符串数据域,作为共享数据  
  4.   
  5. }  
  6. class ThreadDemo extends Thread {  
  7.     private ShareData oShare; // 声明,并初始化ShareData 数据域  
  8.     ThreadDemo() {  
  9.     } // 声明,并实现ThreadDemo 构造方法  
  10.   
  11.     // 声明,并实现ThreadDemo 带参数的构造方法  
  12.     ThreadDemo(String szName, ShareData oShare) {  
  13.         super(szName); // 调用父类的构造方法  
  14.         this.oShare = oShare; // 初始化oShare域  
  15.     }  
  16.     public void run() {  
  17.         for (int i = 0; i < 5; i++) {  
  18.             if (this.getName().equals("Thread1")) {  
  19.                 oShare.szData = "这是第 1 个线程";  
  20.                 // 为了演示产生的问题,这里设置一次睡眠  
  21.                 try {  
  22.                     Thread.sleep((int) Math.random() * 100); // 休眠  
  23.                 } catch (InterruptedException e) { // 捕获异常  
  24.                 }  
  25.                 System.out.println(this.getName() + ":" + oShare.szData); // 输出字符串信息  
  26.             } else if (this.getName().equals("Thread2")) {  
  27.                 oShare.szData = "这是第 2 个线程";  
  28.                 // 为了演示产生的问题,这里设置一次睡眠  
  29.                 try {  
  30.                     Thread.sleep((int) Math.random() * 100); // 线程休眠  
  31.                 } catch (InterruptedException e) // 捕获异常  
  32.                 {  
  33.                 }  
  34.                 System.out.println(this.getName() + ":" + oShare.szData); // 输出字符串信息  
  35.             }  
  36.         }  
  37.     }  
  38. }  
  39.   
  40. public class ThreadNoSynchronized {  
  41.     public static void main(String argv[]) {  
  42.         ShareData oShare = new ShareData(); // 创建,初始化ShareData对象oShare  
  43.         ThreadDemo th1 = new ThreadDemo("Thread1", oShare); // 创建线程th1  
  44.         ThreadDemo th2 = new ThreadDemo("Thread2", oShare); // 创建线程th2  
  45.         th1.start(); // 启动线程th1  
  46.         th2.start(); // 启动线程th2  
  47.     }  
  48. }  

运行结果如下:

Thread1:这是第 2 个线程
Thread1:这是第 1 个线程
Thread1:这是第 1 个线程
Thread1:这是第 1 个线程
Thread1:这是第 1 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程
Thread2:这是第 2 个线程

        程序中预想的结果是:“Thead1:这是第1 个线程”或“Thead2:这是第2 个线程”,但是线程对数据的异步操作导致运行结果出现了差错。  上面程序是由于线程不同步而导致错误。 为了解决此类问题,Java 提供了“锁”机制实现线程的同步。 

        锁机制的原理是每个线程进入共享代码之前获得锁,否则不能进入共享代码区,并且在退出共享代码之前释放该锁,这样就解决了多个线程竞争共享代码的情况,达到线程同步的目的。Java中锁机制的实现方法是共享代码之前加入 synchronized 关键字。 

        在一个类中,用关键字 synchonized 声明的方法为同步方法。Java 有一个专门负责管理线程对象中同步方法访问的工具——同步模型监视器,它的原理是为每个具有同步代码的对象准备惟一的一把“锁”。当多个线程访问对象时,只有取得锁的线程才能进入同步方法,其他访问共享对象的线程停留在对象中等待,如果获得锁的线程调用wait方法放弃锁,那么其他等待获得锁的线程将有机会获得锁。当某一个等待线程取得锁,它将执行同步方法,而其他没有取得锁的线程仍然继续等待获得锁。 
       Java 程序中线程之间通过消息实现相互通信,wait()、notify()及 notifyAll()方法可完成线程间的消息传递。例如,一个对象包含一个 synchonized 同步方法,同一时刻只能有一个获得锁的线程访问该对象中的同步方法, 其他线程被阻塞在对象中等待获得锁。 当线程调用 wait()方法可使该线程进入阻塞状态,其他线程调用notify()或 notifyAll()方法可以唤醒该线程。 

2 .同步格式 

    当把一语句块声明为 synchornized,在同一时间,它的访问线程之一才能执行该语句块。

    1) 方法同步:用关键字 synchonized 可将方法声明为同步,格式如下。 

  1. class 类名{   
  2.      public synchonized 类型名称 方法名称(){   
  3.            ......   
  4.      }   
  5. }   
     2)语句块同步:  对于同步块,synchornized 获取的是参数中的对象锁。 
  1. synchornized(obj)   
  2. {    
  3.   //………………….    
  4. }   
     当线程执行到这里的同步块时,它必须获取 obj 这个对象的锁才能执行同步块;否则线程只能等待获得锁。必须注意的是obj对象的作用范围不同,控制情况不尽相同。示例如下。  
  1. public void method()   
  2. {    
  3.   Object obj= new Object(); //创建局部Object类型对象obj   
  4.   synchornized(obj)   //同步块   
  5.   {   
  6.       //……………..    
  7.   }    
  8. }    

      上面的代码创建了一个局部对象obj。由于每一个线程执行到 Object obj = new Object()时都会产生一个 obj 对象,每一个线程都可以获得创建的新的 obj对象的锁,不会相互影响,因此这段程序不会起到同步作用。

     3)同步类的属性:如果同步的是类的属性,情况就不同了。同步类的成员变量的一般格式如下。 

  1. class method   
  2. {    
  3.     Object o = new Object();  //创建Object类型的成员变量o   
  4. public void test()   
  5. {    
  6. synchornized(o)  //同步块   
  7. {    
  8.             //………………………   
  9.         }    
  10.     }    
  11. }   

     当两个并发线程访问同一个对象的 synchornized(o)同步代码块时,一段时间内只能有一个线程运行。另外的线程必须等到当前线程执行完同步代码块释放锁之后,获得锁的线程将执行同步代码块。

     有时可以通过下面的格式声明同步块。 

  1. public void method()   
  2. {    
  3. synchornized(this)  //同步块   
  4. {    
  5.     //………………………   
  6.     }    
  7. }   

    当有一个线程访问某个对象的 synchornized(this)同步代码块时,另外一个线程必须等待该线程执行完此代码块,其他线程可以访问该对象中的非 synchornized(this)同步代码。如果类中包含多个 synchornized(this)同步代码块,如果同步线程有一个访问其中一个代码块,则其他线程不能访问该对象的所有 synchornized(this)同步代码块。对于下面形式的同步块而言,调用 ClassName 对象实例的并行线程中只有一个线程能够访问该对象。 
  1. synchornized(ClassName.class)   
  2. {    
  3.     //…………………….   
  4. }   


5. 线程通信

1. 生产者与消费者

    生产者与消费者是个很好的线程通信的例子,生产者在一个循环中不断生产共享数据,而消费者则不断消费生产者生产的共享数据。程序必须保证有共享数据,如果没有,消费者必须等待生产新的共享数据。两者之间的数据关系如下:
1) 生产者生产前,如果共享数据没有被消费,则生产等待;生产者生产后,通知消费者消费。
2)消费者消费前,如果共享数据已经被消费完,则消费者等待;消费者消费后,通知生产者生产。
    为了解决生产者和消费者的矛盾,引入了等待/通知(wait/notify)机制。

  1. class Producer extends Thread {  
  2.     Queue q;  
  3.   
  4.     Producer(Queue q) {  
  5.         this.q = q;  
  6.     }  
  7.     public void run() {  
  8.         for (int i = 1; i < 5; i++) {  
  9.             q.put(i);  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. class Consumer extends Thread {  
  15.     Queue q; // 声明队列q  
  16.     Consumer(Queue q){   
  17.         this.q = q; // 队列q初始化  
  18.     }  
  19.     public void run() {  
  20.         while (true) {// 循环消费元素  
  21.             q.get(); // 获取队列中的元素  
  22.         }  
  23.     }  
  24. }  

    Producer 是一个生产者类,该生产者类提供一个以共享队列作为参数的构造方法,它的run 方法循环产生新的元素,并将元素添加于共享队列;Consumer 是一个消费者类,该消费者类提供一个以共享队列作为参数的构造方法,它的 run 方法循环消费元素,并将元素从共享队列删除。 

2.共享队列 

共享队列类是用于保存生产者生产、消费者消费的共享数据。共享队列有两个域:value(元素的数目)、isEmpty(队列的状态)。共享队列提供了put和 get 两个方法。 

  1. class Queue {  
  2.     int value = 0// 声明,并初始化整数类型数据域value  
  3.     boolean isEmpty = true// 声明,并初始化布尔类型数据域isEmpty,用于判断队列的状态  
  4.   
  5.     // 生产者生产方法  
  6.     public synchronized void put(int v) {  
  7.         // 如果共享数据没有被消费,则生产者等待  
  8.         if (!isEmpty) {  
  9.             try {  
  10.                 System.out.println("生产者等待");  
  11.                 wait(); // 进入等待状态  
  12.             } catch (Exception e) // 捕获异常  
  13.             {  
  14.                 e.printStackTrace(); // 异常信息输出  
  15.             }  
  16.         }  
  17.         value += v; // value值加v  
  18.         isEmpty = false// isEmpty赋值为false  
  19.         System.out.println("生产者共生产数量:" + v);  
  20.         notify();  
  21.     }  
  22.   
  23.     public synchronized int get() {  
  24.         if (isEmpty) {  
  25.             try {  
  26.                 System.out.println("消费者等待");  
  27.                 wait();  
  28.             } catch (Exception e) {  
  29.                 e.printStackTrace();  
  30.             }  
  31.         }  
  32.         value--;  
  33.         if (value < 1) {  
  34.   
  35.             isEmpty = true;  
  36.         }  
  37.         System.out.println("消费者消费一个,剩余:" + value);  
  38.         notify();  
  39.         return value;  
  40.     }  
  41. }  

    生产者调用put方法生产共享数据,如果共享数据不为空,生产者线程进入等待状态;否则将生成新的数据,然后调用notify方法唤醒消费者线程进行消费;
消费者调用get方法消费共享数据,如果共享数据为空,消费者进入等待状态,否则将消费共享数据,然后提调用notify方法唤醒生产者线程进行生产。

3. 运行生产者与消费者

  下面是生产者与消费者程序的主程序。
  1. public class ThreadCommunication {  
  2.     public static void main(String[] args) {  
  3.         Queue q = new Queue();  
  4.         Producer p = new Producer(q);  
  5.         Consumer c = new Consumer(q);  
  6.         c.start();  
  7.         p.start();  
  8.     }  
  9. }  

注意:考虑到程序的安全性,多数情况下使用 notifiAll(),除非明确可以知道唤醒哪一个线程。wait方法调用的前提条件是当前线程获取了这个对象的锁,也就是说 wait方法必须放在同步块或同步方法中。 


6. 线程死锁

      为了保证数据安全使用 synchronized同步机制, 当线程进入堵塞状态 (不可运行状态和等待状态)时,其他线程无法访问那个加锁对象(除非同步锁被解除),所以

一个线程会一直处于等待另一个对象的状态, 而另一个对象又会处于等待下一个对象的状态,以此类推,这个线程“等待”状态链会发生很糟糕的情形,即封闭环状态(也就是说最后那个对象在等待第一个对象的锁)。此时,所有的线程都陷入毫无止境的等待状态中,无法继续运行,这种情况就称为“死锁。虽然这种情况发生的概率很小,一旦出现,程序的调试变得困难而且查错也是一件很麻烦的事情。

    下面举一个死锁的例子。 

  1. public class ThreadLocked implements Runnable {  
  2.     public static boolean flag = true// 起一个标志作用  
  3.     private static Object A = new Object(); // 声明,并初始化静态Object数据域A  
  4.   
  5.     private static Object B = new Object(); // 声明,并初始化静态Object数据域B  
  6.   
  7.     public static void main(String[] args) throws InterruptedException {  
  8.         Runnable r1 = new ThreadLocked(); // 创建,并初始化ThreadLocked对象r1  
  9.         Thread t1 = new Thread(r1); // 创建线程t1  
  10.         Runnable r2 = new ThreadLocked(); // 创建,并初始化ThreadLocked对象r2  
  11.         Thread t2 = new Thread(r2); // 创建线程t2  
  12.         t1.start(); // 启动线程t1  
  13.         t2.start(); // 启动线程t2  
  14.     }  
  15.   
  16.     public void AccessA() {  
  17.         flag = false// 初始化域flag  
  18.         // 同步代码快  
  19.         synchronized (A) { // 声明同步块,给对象A加锁  
  20.             System.out.println("线程t1 : 我得到了A的锁"); // 输出字符串信息  
  21.             try {  
  22.                 // 让当前线程睡眠,从而让另外一个线程可以先得到对象B的锁  
  23.                 Thread.sleep(1000); // 休眠  
  24.             } catch (InterruptedException e) { // 捕获异常  
  25.                 e.printStackTrace(); // 异常信息输出  
  26.             }  
  27.             System.out.println("线程t1 : 我还想要得到B的锁");  
  28.             // 在得到A锁之后,又想得到B的锁  
  29.             // 同步块内部嵌套同步块  
  30.             synchronized (B) { // 声明内部嵌套同步块,指定对象B的锁  
  31.                 System.out.println("线程t1 : 我得到了B的锁"); // 输出字符串信息  
  32.             }  
  33.         }  
  34.     }  
  35.   
  36.     public void AccessB() {  
  37.         flag = true// 修改flag的值  
  38.         // 同步代码块  
  39.         synchronized (B) { // 指定同步块,给B加锁  
  40.             System.out.println("线程t2 : 我得到了B的锁"); // 输出字符串信息  
  41.             try {  
  42.                 // 让当前线程睡眠,从而让另外一个线程可以先得到对象A的锁  
  43.                 Thread.sleep(1000); // 休眠  
  44.             } catch (InterruptedException e) { // 捕获异常InterruptedException  
  45.                 e.printStackTrace(); // 异常信息输出  
  46.             }  
  47.             System.out.println("线程t2 : 我还想要得到A的锁"); // 字符串信息输出  
  48.             // 在得到B锁之后,又想得到A的锁  
  49.             // 同步块内部嵌套内部快  
  50.             synchronized (A) { // 指定同步块,给A加锁  
  51.                 System.out.println("线程t2 : 我得到了A的锁"); // 输出字符串信息  
  52.             }  
  53.         }  
  54.     }  
  55.   
  56.     public void run() {  
  57.         if (flag){ // 当flag为true,执行下面语句  
  58.             AccessA(); // 调用AccessA方法  
  59.         } else {  
  60.             AccessB(); // 调用AccessB方法  
  61.         }  
  62.     }  
  63.   
  64. }  

        程序 ThreadLocked.java中创建了两个线程 t1 和 t2,并且声明两个方法:AccessA和 AccessB。在运行过程中,线程t1 先获得了 A 的锁,然后又要求获得 B 的锁;而 t2
先获得B 的锁,然后又要求获得 A的锁,此时便进入了无休止的相互等待状态,即死锁。 

Java 语言本身并没有提供防止死锁的具体方法,但是在具体程序设计时必须要谨慎,以防止出现死锁现象。通常在程序设计中应注意,不要使用 stop()、suspend()、resume()以及 destroy()方法。 stop()方法不安全,它会解除由该线程获得的所有对象锁,而且可能使对象处于不连贯状态,如果其他线程此时访问对象,而导致的错误很难检查出来。suspend()/resume ()方法也极不安全,调用 suspend()方法时,线程会停下来,但是该线程并没有放弃对象的锁,导致其他线程并不能获得对象锁。调用destroy()会强制终止线程,但是该线程也不会释放对象锁。 


Java中的数据结构


一、数据结构的接口

在Java中所有类的鼻祖是Object类,但是所有有关数据结构处理的鼻祖就是Collection和Iterator接口,也就是集合与遍历。

1、Collection接口

Collection c = new Xx(); // c可以称为Collection接口回调对象,虽然它被声明为Collection类型,但是实例化时实现的是接口的实现类Xx。它的方法也是用来操作实现类的对象。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <span style="white-space:pre">    </span>//下面是Collection接口的一些常用方法  
  2.     boolean add(Object obj);    
  3.     boolean addAll(Collection c);  
  4.     boolean contains(Object o);  
  5.     boolean containsAll(Collection c);  
  6.     boolean isEmpty();  
  7.     int size();  
  8.     void clear();  
2、Iterator接口
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <span style="white-space:pre">    </span>Xx xx = new Xx();  
  2.     Iterator iterator = xx.iterator();  
iterator()是Collection接口中的方法,它会返回在xx的元素上进行迭代的迭代器。并且迭代置于集合的第一个元素前面。
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //下面是Iterator接口的一些常用方法  
  2. boolean hasNext();//判断迭代是否到达了集合末尾  
  3. Object next();//返回迭代的下一个元素  
  4. void remove();//移除迭代指向的最后一个元素  
next()方法可以理解为:迭代往下移一位,返回迭代条过的元素。Xx xx = (Xx)iterator.next() 取出来的元素类型为Object需要转换。

java数据结构库还提供了一个Iterator的子接口——ListIterator,可以实现将元素插入指定位置和逆向遍历等操作。详细查阅API。

二、数据结构的几个重要的类

1、链表(LinkedList)

解决了数组删除元素开销大的问题,但是查询元素需要从链表头遍历。每个元素有两个指针,next指向下一个元素,previous指向上一个元素。LinkedList类实现了Collection接口。

2、数组列表类(ArrayList)

数组列表可以根据程序运行时的需求动态的伸缩。ArrayList类实现了Collection接口。

3、哈希表(Hashtable)

通过散列码存取,实现快速查询特定元素的数据结构。以键—值的形式存储元素,必须保证键是唯一的,值可以不唯一。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <span style="white-space:pre">    </span>//构造器  
  2.     Hashtable(int initialCapacity, float loadFactor)  
  3.     //initialCapacity 表示初始化容量 缺省值为 11  
  4.     //loadFactor 表示装填因子 缺省值为0.75  
  5.           
  6.     //主要方法  
  7.     Object put(Object key, Object value);  
  8.     //加入一个元素  
  9.     //返回此哈希表中指定键的以前的值,如果不存在该值,则返回 null   
  10.     Object get(Object key);//返回此哈希表中指定键的值  
  11.     boolean containsKey(Object key);  
  12.     boolean containsValue(Object key);  
  13.     Object remove(Object key);//删除指定键元素,返回指定键映射的值  
  14.     Collection values();//返回此表所包含元素的集合    
4、哈希集(HashSet)

      与哈希表类似,只是Hashtable实现的是Set接口,而HashSet实现的是Map接口还有Collection接口。

      构造方法的默认初始容量是 16,加载因子是 0.75。

      另外很重要的一点是哈希表是线性同步的,而哈希集是非线性同步的。

5、树集(TreeSet)

有序的数据结构。

a、构造器: TreeSet()

此时,向树集添加字符串对象,因为它已经实现了Comparable接口,compareTo方法是按字典序比较字符串的。所以树集元素按字典序排列,当然也可以定义自己的排序方法:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class Student implements Comparable  
  2. {  
  3.     public int compareTo(Object other)  
  4.     {  
  5.     <span style="white-space:pre">    </span>//具体实现  
  6.     }     
  7. }  
b、构造器:TreeSet(Comparator c)

此时,可以通过实现Comparator接口中的compare方法,定义自己的比较方法。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class StudentComparator implements Comparator  
  2. {  
  3.     public compare(Object a, Object b)  
  4.     {  
  5.         //具体实现  
  6.     }  
  7. }  
  8. StudentComparator comp = new StudentComparator();  
  9. TreeSet set = new TreeSet(comp);  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值