第十六章数组
数组与容器之间的区别在三个方面:效率、类型(保持存放元素的类型)、保存基本类型的能力。但从Java SE5后泛型与自动装箱的出现,数组的优点就只是效率了。
无论使用哪种类型的数组,数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象又可能保存指向其他对象的引用。
新创建的数组未初始化时,如果存储的是对象,则所有元素自动初始化为null,基本类型初始化为0,字符型自动初始为(char)0,布尔型自动初始化为false。
不能创建泛型数组以及带类型参数的数组:
//T[] array = new T[SIZE]; // 不能创建泛型数组
//ArrayList<String> [] list = new ArrayList<String>[1]; // 也不能创建带类型参数的数组
但可以定义一个泛型数组的引用:
T[] array;
ArrayList<String> [] list;
虽然你不能创建泛型数组,但是可以创建非泛型数组然后将其转型:
public class ArrayOfGenerics {
public static void main(String[] args) {
List<String>[] ls;
List[] la = new List[10];
ls = (List<String>[])la; //会发生 "Unchecked" 警告
ls[0] = new ArrayList<String>();
//上面其实相当于下面一条语句
//List<String>[] ls1 = (List<String>[])new List[10];
// 编译时会产生错误:
//! ls[1] = new ArrayList<Integer>();
// 问题是: List<String> 是Object子类
Object[] objects = ls; //所以可以赋值给Object数组引用
// 编译与运行都没有错误,因为创建的List数组本身就是原生数组:
objects[1] = new ArrayList<Integer>();
}
}
Random实例对象可随机返回各种基本类型的数,可设置种子。但Math中的random方法只能返回[0.0, 1)之间的double型小数,实质上该方法就是调用Random实例的nextDouble()来实现的。
Arrays类提供了重载的equals()方法。数组相等的条件是元素个数必须相等,并且对应位置的元素也相等。
>>>动态创建数组<<<
现有如下应用,Employee[]数组满后扩容,该如何做?
Employee[] a = new Employee[100]
…
//arry is full
a = (Employee[])arrayGrow(a);
错误作法:
static Object[] arrayGrow (Object[] a) {
int newLength = a.length * 11 / 10 + 10;
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0, a.length);
return newArray;
}
上面在实际应用中会遇到一个问题,这段代码返回的数组类型是Object[],这是由于使用下面这行代码创建的数组:new Object[newLength]。如果现在我们要对Employee[]数组进行扩展时,则在扩充后我们不能将返回的Object[]类型的对象数组转换成Employee[]数组了(当然如果从Object[]类型对象数组中取出一个个元素后再强转为Employee是没问题)。将一个Employee[]临时地转换成Object[]数组,然后再把它转换回来是可以的,但一个从开始就是Object[]的数组却永远不能转换成Employee[]数组。为了编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。因此需用到反射包中的Array类的静态方法newInstance.:
static Object arrayGrow (Object a) {//参数是Object而不是Object[],因为整型数组类型int[]可以被转换成Object,但不能转换成对象数组
Class cl = a.getClass();
if (!cl.isArray()) {
return null;
}
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
int newLength = length * 11 / 10 + 10;
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, newLength);
return newArray;
}
另外,以下转换也是可以的:
String[] strArr = new String[10];
Object o = strArr;
strArr= (String[]) o;
第十七章容器的深入研究
可以使用Arrays.asList将数组或可变参数转换成AbstractList列表,其底层数据实现就是我们传进的参数数组,因此不能像其他List列表那样调整尺寸,如果你试图用add()或delete()方法在这种列表中添加或删除元素,就有可能会引发去修改数组尺寸的尝试,因此你将在运行时获得“Unsupported Operation(不支持的操作)”错误,该列表只支持读取与修改操作(另外,我们还可以通Collections.unmodifiableList(List)来对一个列表包装之后,只能进行读取操作,即使写是不行了)。
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}
public class AsListInference {
public static void main(String[] args) {
List<Snow> snow1 = Arrays.asList(
new Crusty(), new Slush(), new Powder());
// Won't compile:
// List<Snow> snow2 = Arrays.asList(
// new Light(), new Heavy());
// Compiler says:
// found : java.util.List<Powder>
// required: java.util.List<Snow>
// Collections.addAll() doesn't get confused:
List<Snow> snow3 = new ArrayList<Snow>();
Collections.addAll(snow3, new Light(), new Heavy());
// Give a hint using an
// explicit type argument specification:
List<Snow> snow4 = Arrays.<Snow>asList(
new Light(), new Heavy());
}
}
当试图创建snow2时,Arrays.asList()中只有Powder类型,因此它会创建List<Powder>而不是List<Snow>,尽管Collections.addAll()工作的很好,因为它从第一个参数中了解到了目标类型是什么。
正如你从创建snow4的操作中所看到的,可以在Arrays.asList()中间插入具体的类型,以告诉编译器对于由Arrays.asList()产生的List类型,实际的类型应该是什么。这称为显示类型参数说明。
Queue接口与List、Set同一级别,都是继承了Collection接口。LinkedList实现了Queue接口。Queue接口窄化了对LinkedList的方法的访问权限(即在方法中的参数类型如果是Queue时,就完全只能访问Queue接口所定义的方法了,而不能直接访问LinkedList的非Queue的方法),以使得只有恰当的方法才可以使用。
offer(),将一个元素插入到队尾,或者返回false。
peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,而element()会抛出NoSuchElementExcetption异常。
poll()和remove()方法将移除并返回队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementExcetption异常。
PriorityQueue:优先级队列。先进先出描述了最典型的队列规则,但优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。如果构建一个消息系统,某些消息比其他消息更重要,因而应该更快地得到外理,那么它们何时得到处理就与它们何时到达无关。PriorityQueue添加到JSE5中,是为了提供这种行为的一种自动实现。当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是你可能通过提供自己的Comparator来修改这个顺序。PriorityQueue可以确保当你调用peek()、poll()和remove()方法时,获取元素将是队列中优先级最高的元素。
Collection接口继承了Iterable接口,该接口包含一个能够产生java.util.Iterator的iterator()方法,可用于foreach语句中。
foreach语句可以用于数组或其他任何Iterable,但是这并不意味着数组是一个Iterable,数组与Iterable接口没有直接的关系。
你必须为散列存储和树型存储都创建一个equals()方法,但是hashCode()只有在这个类将会存放到HashMap、HashSet或者LinkedHashMap、LinkedHashSet中时才是必需的,因为这类Hash最终都是能过HashMap的containsKey方法使用if (e.hash == hash && eq(k, e.key))来实现对比的。
Set —— 存入Set的每个元素都必须是唯一的,因为Set不允许保存重复的元素。放入到Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
HashSet(优先选择) —— 底层以HashMap来实现。为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()。
TreeSet —— 保持次序的Set,底层为树结构。使用它可以从Set中读取有序的序列。放入的元素必须实现Comparable接口。
LinkedHashSet —— 底层以LinkedHashMap来实现。继承自HashSet,具有HashSet的查询速度,且内部使用链表维护元素顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素的插入次序显示。存入的元素也必须定义hashCode()方法。
HashMap(优先选择) —— Map基于散列表的实现(它取代了Hashtable)。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子,以调容器的性能。
LinkedHashMap —— 继承自HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最小使用(LRU)的次序。只比HashMap慢一点,而在迭代访问时反而更快,因为它使用链表维护内部次序。
TreeMap —— 基于红黑树的实现。查看“健”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于,所得到的结果是经过排序的。TreeMap是唯一带有subMap()方法的Map,它可以返回一个子树。
WeakHashMap —— 弱键映射,允许垃圾回收器回收无外界引用指向象Map中键,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此“键”可以被垃圾收集器回收。
ConcurrentHashMap —— 一种线程安全的Map,它不synchronized同步加锁,而是使用新的锁机制。尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。
IdentityHashMap —— 使用 == 代替equals()对“键”进行比较的散列映射。
看一个弱引用例子:
class WeakObject {
String name;
public WeakObject(String mwname) {
this.name = mwname;
}
public void finalize() {
System.out.println(name + "对象满足垃圾收集条件,被收集!");
}
public void show() {
System.out.println(name + "对象还可以使用!");
}
}
public class WeakReferenceTest {
public static void main(String[] args) {
System.out.println("---对象弱引用---");
WeakObject wo = new WeakObject("weakObject");
//包装成弱引用对象
WeakReference wr = new WeakReference(wo);
wo = null;
((WeakObject) wr.get()).show();
System.out.println("第一次垃圾收集!");
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (wr.get() != null) {
((WeakObject) wr.get()).show();
}
System.out.println("---弱引用map---");
WeakHashMap whm = new WeakHashMap();
WeakObject wo2 = new WeakObject("weakObjectKey");
//这里的值不会被回收
whm.put(wo2, new WeakObject("weakObjectValue"));
wo2 = null;
((WeakObject) whm.keySet().iterator().next()).show();
System.out.println("第二次垃圾回收!");
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
((WeakObject) whm.keySet().iterator().next()).show();
}
}
对于底层采用数组的ArrayList,无论列表的大小如何,这些访问都很快和一致。而对于LinkedList,访问时间对于较大的列表将明显增加。很显然,如果你需要执行大量的随机访问,链接链表不会是一种好的选择;
插入时,对于ArrayList,当列表变大时,其开销将变得很高昂,但是对于LinkedList,相对来说比较低廉,并且不随列表尺寸而发生变化,这是因为ArrayList在插入时,插入点后面所有元素将后移,如果在插入时超过了数组的最大容量,则会重新创建一个新的数组,并将所有元素复制到新的数组中(不过插入与扩容时使用的都是System.arraycopy方法,效率上比使用循环一个个移动要高),这会随ArrayList的尺寸增加而产生高昂的代价。LinkedList只需链接新的元素,而不必修改列表中剩余的元素,因此可以认为无论列表尺寸如何变化,其代价大致相同。
在LinkedList中的插入和移除代价相当低廉,并且不随列表尺寸发生变化,但是对于ArrayList插入操作代价特别高昂,并且其代价将随列表尺寸增加而增加。
HashSet的性能基本上总是比TreeSet好,特别是在添加和查询元素时,而这两个操作也是最重要的操作。TreeSet存在的唯一原因是它可以维持元素的排序状态;所以,只有当需要一个排好序的Set时,才应该使用TreeSet。因为其内部结构支持排序,并且因为迭代是我们更有可能执行的操作,所以,用TreeSet迭代通常比用HashSet要快。
除了IdentityHashMap,所有的Map实现的插入操作都会随着Map尺寸变大而明显变慢,但是,查找的代价通常比插入要小得多。
Hashtable的性能大体上与HashMap相当,因为HashMap是用来替代Hashtable的,因为它们使用了相同的底层存储和查找机制,这并没有什么令人奇怪的。
TreeMap通常比HashMap要慢。
LinkedHashMap在插入 时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表(以保持插入顺序),正是由于这个列表,使得其迭代速度更快一点。
IdentityHashMap则具有完全不同的性能,因为它使用==而不是equals()来比较元素。
负载因子小的Hash表产生冲突的可能性小,因此对于插入和查找都是最理想(但是会减慢使用迭代器进行遍历的过程,因为还有很多的空位置,这些空的位置也会在循环中遍历到)。当容量扩大时,现有的对象将重新分布到亲的桶位中(这被称为再散列)。HashMap使用的默认负载因子为0.75(只有当表的饱和度达到四分之三时,才进行再散列),这个因子在时间和空间代价之间达到平衡,更大的负载因子可以提高空间的利用率,但是会增加查找代价。
如果你知道将要在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将可以避免自动再散列的开销。
形如Collections.unmodifiableCollection(Collection<? extends T> c)一类方能产生只读容器。
形如Collections.synchronizedCollection(Collection<T> c, Object mutex)一类方能产生同步容器。
快速报错:Java容器有一种保护机制,能够防止多个进程(或直接通过容器本身而不是迭代器)同时修改同一个容器的内容。如果在你迭代遍历某个容器的过程中,另一个进程介入其中,并且插入、删除或修改此容器内的某个对象,那就会出现问题:也迭代过程已经处理过容器中的该元素,也许还没处理,也许在调用size()之后容器的尺寸缩小了等等,Java容器类类库采用快速报错机制。它会先检测容器上的任何除了你的进程所进行的操作或使用迭代器外的操作所引起的变化,一进发现改变,就会立刻抛ConcurrentModificationExcetion异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检测问题。所以应该在添加、删除、修改完所有元素之后,再获取迭代器。ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationExcetion的技术。
第十八章I/O 第十九章枚举
enum的values()方法返回enum实例的数组,而且该数组中的元素严格保持其在enum中声声明时的顺序。
创建enum时,编译器会为你生成一个相关的类,这个类继承自java.lang.Enum。
Enum类实例的ordinal()方法返回一个int值,这是每个enum实例在声明时的次序,从0开始。可以使用==来比较enum实例,编译器会自动为你提供equals()和hashCode()方法。
Enum类实现了Comparable接口,所以它具有compareTo()方法,同时,它还实现了Serializable接口。
name()方法返回enum实例声明的名字,与使用toString()方法效果一样。
valueOf(Class<T> enumType, String name)是在Enum中定义的static方法,它根据所给定的名字返回相应的enum实例,如果不存在给定名字的实例,将会抛出异常。
除了不能继承自一个enum之外,我们基本上可以将enum看作一个常规的类,也就是说,我们可以向enum中添加方法属性。
public enum OzWitch {
// enum实例必须定义在最前,并在方法之前:
WEST("west."), NORTH("north."),
EAST("east."), SOUTH("south.");//如果还有其他方法,则这个分号一定要写上
private String description;//可以定义属性
// 注,枚举的构造函数只能是包访问或private访问权限:
private OzWitch(String description) {
this.description = description;
}
//也可以有方法
public String getDescription() {
return description;
}
//还可以有main方法
public static void main(String[] args) {
for (OzWitch witch : OzWitch.values())
System.out.println(witch + ": " + witch.getDescription());
}
}
>>>values()的神秘之处<<<
public enum Explore {
HERE, THERE
}
使用javap反编译后:
public final class Explore extends java.lang.Enum{
public static final Explore HERE;
public static final Explore THERE;
public static final Explore[] values();// 编译器自动添加的方法
public static Explore valueOf(java.lang.String);// 编译器自动添加的方法
static {};
}
Enum类没有values()方法,values()是由编译器为enum添加的static方法,在创建Explore的过程中,编译器还为它创建了valueOf(String name)方法,这可能有点怪了,Enum类不是已经有valueOf(Class<T> enumType, String name)了吗?不过Enum中的valueOf()方法需要两个参数,而这个新增的方法只需一个参数。
由于values()方法是由编译器插入到enum定义中的static方法,所以,如果你将enum实例向上转型为Enum,那么values()方法就不可访问了。不过,在Class中有一个getEnumConstants()方法,所以即使Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有enum实例(注:只有是Enum类型的Class才能调用getEnumConstants方法,否则抛空指针异常):
enum Search { HITHER, YON }
public class UpcastEnum {
public static void main(String[] args) {
Search[] vals = Search.values();
Enum e = Search.HITHER; // 向上转型
// e.values(); // Enum中没有 values()方法
// 但我们可以通过Class对象的getEnumConstants()反射出enum实例
for(Enum en : e.getClass().getEnumConstants())
System.out.println(en);
}
}
>>>enum还可实现其它接口<<<
由于enum可以看是一个普通的类,所以他还可以实现其他接口。
enum CartoonCharacter implements Generator<CartoonCharacter> {
SLAPPY, SPANKY, BOB;
private Random rand = new Random(47);
public CartoonCharacter next() {
return values()[rand.nextInt(values().length)];
}
}
>>>枚举实例的“多态”表现<<<
enum LikeClasses {
WINKEN { void behavior() { System.out.println("Behavior1"); } },
BLINKEN { void behavior() { System.out.println("Behavior2"); } },
NOD { void behavior() { System.out.println("Behavior3"); } };
abstract void behavior();//抽象方法,由每个enum实例实现,当然也可是一个实体方法
public static void main(String[] args) {
for(LikeClasses en:values()){
en.behavior();//好比多态
}
}
// 但不能把枚举实例看作是类,因为它们只是一个实例
// void f1(LikeClasses.WINKEN instance) {}
}
经过反编译后,我们发现生成了四个类LikeClasses.class、LikeClasses$2.class、
LikeClasses$3.class、LikeClasses$1.class,而且LikeClasses$xx.class继承自LikeClasses.class类,并将各自的behavior方法代码放入到自己相应的类文件中。
第二十章注解
JavaSE5内置了三种标准注解,定义在java.lang中的注解:
@Override,表示当前的方法定义将覆盖超类中的方法。如果没有重写,编译器会发出错误提示。
@Deprecated,如果程序员使用了该注解注解过的元素,那么编译器会发出警告信息。
@SuppressWarnings,关闭不当的编译器警告信息。
元注解是负责注解其他的注解,有四种元注解:
@Target 表示该注解可以用于什么地方。可能的ElementType(枚举)参数包括:
CONSTRUCTOR:构造方法声明
FIELD:域声明(包括enum实例)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类,接口(包括注解类型)或enum声明
@Retention 表示需要在什么级别保存注解信息。可用的RetentionPolicy(枚举)参数包括:
SOURCE:注解将被编译地器丢弃。
CLASS:注解在class文件中可用,但会被VM丢弃。
RUNTIME:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documented 将此注解包含在JavaDoc中。
@Inherited 允许子类继承父类中的注解。
Class、Method、Field、Constructor类都实现了AnnotatedElement接口,他们的getAnnotation方法都返回指定类型的注解对象,如果没有该类型的注解,则返回null值,以下是这些方法的原型:
class. getAnnotation(Class<T> annotationClass)
method. getAnnotation(Class<T> annotationClass)
field. getAnnotation(Class<T> annotationClass)
constructor. getAnnotation(Class<T> annotationClass)
注解的定义看起来很像接口的定义,事实上,与其他任何Java接口一样,注解也会编译成class文件。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
* 这是一个简单的注解,我们可以用它来跟踪一个项目中的用例。如果一个方法
* 或一组方法实现了某个用例的需求,那么程序员可以为此方法加上该注解。
*/
//注解的定义
@Target(ElementType.METHOD)//该注解用于方法
@Retention(RetentionPolicy.RUNTIME)//注解信息保留到运行期
public @interface UseCase {
public int id();//int型的元素
public String description() default "no description";//String型的元素
}
//注解的使用
class PasswordUtils {
@UseCase(id = 47, description = "密码必须至少包含一个数字")
public boolean validatePassword(String password) {
return (password.matches("\\w*\\d\\w*"));
}
@UseCase(id = 48)//没有描述,使用默认的描述信息
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "新密码不能使用以前使用过的密码")
public boolean checkForNewPassword(
List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
//注解处理器
class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases, Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
// 通过反射获取某个方法特定的注解信息
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("找到用例:" + uc.id() +
" " + uc.description());
useCases.remove(new Integer(uc.id()));
}
}
for(int i : useCases) {
System.out.println("警告: 所缺用例-" + i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<Integer>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
/*
找到用例:47 密码必须至少包含一个数字
找到用例:48 no description
找到用例:49 新密码不能使用以前使用过的密码
警告: 所缺用例-50
*/
注解里的组成元素类型:
1、 所有基本类型
2、 String
3、 Class
4、 enum
5、 Annotation
6、 以上类型的数组
如果你使用了其他类型,那编译器就会报错。
注解组成元素的默认值限制:首先,元素不能有不确定的值,也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值,不允许即没给定默认值,在使用进也没指定值的情况出现。其次,对于非基本类型的元素,无论是在声明还是在使用时,都不能以null作为其值。
注解不支持继承。
第二十一章并发
Thread.yield():对线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议,它是在说“我已经执行完生命期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机”,即对线程的一种让步,暂停当前正在执行的线程,并执行其他线程。
>>>使用Executor<<<
Java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。
Executors:Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类的工厂和实用方法。
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。
public class LiftOff implements Runnable {
protected int countDown = 10; // Default
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run() {
while(countDown-- > 0) {
System.out.print(status());
Thread.yield();
}
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#2(9), #0(9), #4(9), #1(9), #3(9), #2(8), #0(8), #4(8), #1(8), #3(8), #2(7), #0(7), #4(7), #1(7), #3(7), #2(6), #0(6), #4(6), #1(6), #3(6), #2(5), #0(5), #4(5), #1(5), #3(5), #2(4), #0(4), #4(4), #1(4), #3(4), #2(3), #0(3), #4(3), #1(3), #3(3), #2(2), #4(2), #1(2), #3(2), #2(1), #4(1), #1(1), #3(1), #2(Liftoff!), #4(Liftoff!), #1(Liftoff!), #3(Liftoff!), #0(2), #0(1), #0(Liftoff!),
*/
Executors. newFixedThreadPool (int nThreads):可一次性预先执行代价高昂的线程分配,因而也就可以限制线程数量。不用为每个任务都固定地付出创建线程的开销。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPool {
public static void main(String[] args) {
// Constructor argument is number of threads:
ExecutorService exec = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#3(9), #4(9), #0(9), #2(9), #1(9), #3(8), #4(8), #0(8), #2(8), #1(8), #3(7), #4(7), #0(7), #2(7), #3(6), #4(6), #0(6), #2(6), #3(5), #4(5), #0(5), #2(5), #3(4), #4(4), #0(4), #2(4), #3(3), #4(3), #0(3), #2(3), #3(2), #4(2), #0(2), #2(2), #3(1), #4(1), #0(1), #2(1), #3(Liftoff!), #4(Liftoff!), #0(Liftoff!), #2(Liftoff!), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!),
*/
Executors. newSingleThreadExecutor (int nThreads):确保任意时刻在任何都只有唯一的任务在运行,你不需要在共享资源上处理同步,可以让你省去只是为了维持某些事物的原型而进行的各种协调努力。
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec =
Executors.newSingleThreadExecutor();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(Liftoff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),
*/
Thread.sleep(100);等同于TimeUnit.MILLISECONDS.sleep(100);
Daemom线程:后面线程不属于程序中不可缺少的部分,因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。返过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
如果一个线程是后台线程,那么它创建的任何线程将被自动设置成后台线程。
后台线程在不执行finally子句的情况下就会终止其run()方法,即后台线程的finally子句不一定执行。
在构造器中启动线程可能会有问题,因为线程可能会在构造器结束之前开始执行,这意味着该线程能够访问处于不稳定状态的对象。
异常不能跨线程传播给main(),所以你必须在了本地处理所有在线程内部产生的异常。
public class ExceptionThread implements Runnable {
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
try {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch(RuntimeException ue) {
// 这句将不会被执行,因为线程的异常是不会传递到调用它的线程的
System.out.println("Exception has been handled!");
}
}
}
Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在因未捕获的异常而临近死亡时被调用。为了使用它,我们创建了一个新类型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler,并将这个工厂传递给Exceutors创建新的ExcecutorService方法:
// 线程
class ExceptionThread2 implements Runnable {
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println("1.eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();//线程运行时一定会抛出运行异常
}
}
// 线程异常处理器
class MyUncaughtExceptionHandler implements
Thread.UncaughtExceptionHandler {
// 异常处理方法
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
// 线程工厂,创建线程时会调用该工厂
class HandlerThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {//线程创建工厂方法
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
//设置异常处理器
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("2.eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(
new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
} /*
HandlerThreadFactory@1a758cb creating new Thread
created Thread[Thread-0,5,main]
2.eh = MyUncaughtExceptionHandler@69b332
run() by Thread[Thread-0,5,main]
1.eh = MyUncaughtExceptionHandler@69b332
caught java.lang.RuntimeException
*/
如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器:Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
>>>使用Lock对象<<<
Lock对象必须被显式地创建、锁定和释放。因此,它与内建的锁形式相比,代码缺乏优雅性,但对于解决某些类型的问题来说,它更加灵活。
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
//…
} finally {
lock.unlock();
}
}
使用lock()和unlock()方法在next()内部创建了临界资源。还可以尝试获取锁:
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
//…
} finally {
if(captured)
lock.unlock();
}
}
>>>使用volatile对象<<<
原子操作是不能被线程调试机制中断的操作,一旦开始操作,那么它一定会在切换到其他线程前执行完毕。
原子操作可以应用于除long和double之外的所有基本类型之上的“简单操作”,对于读取和写入除long和double之外的基本类型变量这种的操作,可以保证它们会被当作原子操作来操作内存。但是JVM可以将64位(long和double变量)的读取和写入当作两个分离的32位操作来执行,这就可能会产生了在一个读取和写入操作中间切换线程,从而导致不同的线程看到不正确结果的可能性。但是,当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性,注:在Java SE5之前,volatile一直未能正确的工作。
volatile关键字还确保了应用中的可视性,如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改,即便使用了本地缓存,情况确实如此,volatile域会立即被写入到主存中。
在非volatile域上的操作没有刷新到主存中去,因此其他读取该域的线程将不能必看到这个新值。因此,如果多个线程同时访问了某个域,那么这个域就应该是volatile的,否则,这个域应该只能由同步来访问,同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来保护,那就不必将其设置为volatile了。
什么才属于原子操作呢?对域中的值做赋值和返回操作都是原子性的。但i++; i+=2; 这样的操作肯定不是原子性的,即线程有可能从语句的中间切换。下面来证明i++在java里不是原子性操作的:
class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static /* synchronized */int nextSerialNumber() {
// 不是线程安全,因为i++在Java里不是原子操作,
// 即使將serialNumber设置成了volatile
return serialNumber++;
}
}
class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
array = new int[size];
len = size;
// 初始化为-1
for (int i = 0; i < size; i++) {
array[i] = -1;
}
}
public synchronized void add(int i) {
array[index] = i;
// 如果数组满后从头开始填充,好比循环数组:
index = ++index % len;
}
public synchronized boolean contains(int val) {
for (int i = 0; i < len; i++) {
if (array[i] == val) {
return true;
}
}
return false;
}
}
public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials = new CircularSet(1000);
private static ExecutorService exec = Executors.newCachedThreadPool();
static class SerialChecker implements Runnable {
public void run() {
while (true) {
int serial = SerialNumberGenerator.nextSerialNumber();
if (serials.contains(serial)) {// 如果数组中存在则退出
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);// 如果不存在,则放入
}
}
}
public static void main(String[] args) throws Exception {
SerialChecker sc = new SerialChecker();
// 启动10线程
for (int i = 0; i < SIZE; i++) {
exec.execute(sc);
}
}
}
public class Increament extends Thread {
public static volatile int x = 0;
public void run() {
// synchronized (Increament.class) {
// x++与 x = x + 1都不是原子操作
x++;
// x = x + 1;
// }
}
public static void main(String[] args) throws Exception {
Thread threads[] = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Increament();
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
// 等待计算线程运行完
threads[i].join();
}
System.out.println("n=" + Increament.x);
}
}
如果对x的操作是原子级别的,最后输出的结果应该为x=10000,而在执行上面积代码时,很多时侯输出的x都小于10000,这说明x++ 不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用。
同一时刻只有一个线程能访问synchronized块,synchronized块并不是一下子要执行完毕,CPU调试可能从synchronized块中的某个语句切换到其它的线程,再其它线程执行完毕后再继续执行该同步块。切换到其他线程时是否释放synchronized块上的锁,这要看切换所采用的方式:如果是CPU自动或调用Thread.yeild切换,则不会释放;如果是调用wait,则会释放;如果是调用的Thread.sleep,则不会;如果是调用的thread.join,则要看synchronized块上的锁是否是thread线程对象,如果不是,则不会释放,如果是,则会释放。
只能在同步控制方法或同步控制块里调用wait()、notify()和notifyAll(),并且释放操作锁,但sleep()可以在非同步控制方法里调用,不会释放锁。
sleep、yield都是Thread的静态方法,join属于Thread的非静态方式,如果将它们放入在同步块中调用时都不会释放锁。但wait属于Object类的方法,在wait()期间对象锁是释放的。
在执行同步代码块的过程中,遇到异常而导致线程终止,锁会释放。
执行线程的suspend()方法会导致线程被暂停,并使用resume()可唤醒,但不会释放锁。
当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就绪状态,那么yield()方法将把当前运行的线程放到可运行池中并使用中另一线程运行。如果没有相同优先级的可运行进程,则该方法什么也不做。
sleep方法与yield方法都是Thread类的静态方法,都会使当前处于运行的线程放弃CPU,把运行机会让给另的线程。两都的区别:
1. sleep方法会给其他线程运行的机会以,而不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;yield方法只会给相同或更高优先级的线程一个运行的机会。
2. 当线程执行了sleep方法后,将转到阻塞状态。当线程执行了yield方法后,将转入就绪状态。
3. Sleep方法比yield方法具有更好的可移植性。不能依靠yield方法来提高程序的并发性能。对于大多数程序员来说,yield方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误。
thread.join():当前线程调用另一线程thread.join()时,则当前运行的线程将转到阻塞状态,并且等待thread线程运行结束后,当前线程程才会恢复运行(从阻塞状态到就绪状态)。比如有3个线程在执行计算任务,必须等三个线程都执行完才能汇总,那么这时候在主线程里面让三个线程join,最后计算结果既可:
public class JoinTest {
public static void main(String[] args) {
Rt[] ct = new Rt[3];
for (int i = 0; i < ct.length; i++) {
ct[i] = new Rt();
ct[i].start();
try {
//主线等待三个线程终止后再继续运行
ct[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int total = 0;
for (int j = 0; j < ct.length; j++) {
total += ct[j].getResult();
}
System.out.println("total = " + total);
}
}
class Rt extends Thread {
private int result;
public int getResult() {
return result;
}
public void run() {
try {
Thread.sleep(1000);
result = (int) (Math.random() * 100);
System.out.println(this.getName() + " result=" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
join()只能由线程实例调用,如果thread.join()在同步块中调用,并且同步锁对象也是thread对象,由于thread.join()是调用thread.wait()来实现的,wait会释放thread对象锁,则thread.join()与在同步块的锁也会一并释放;如果thread.join()在同步块的锁对象不是thread对象,则thread线程阻塞时不会释放锁:
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
JThread t = new JThread();
start(t, t);
System.out.println("--------");
t = new JThread();
start(t, JThread.class);
}
static void start(JThread t, Object lock) {
t.setLock(lock);
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果锁对象是JThread.class时,则主线程会一直阻塞
t.f();
}
}
class JThread extends Thread {
private Object lock;
void setLock(Object lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " - join before");
/*
* 当前线程阻塞,又要等待自己运行完,这是矛盾的,所以其实该线程永远不会恢复执
* 行,除非使用 join(long millis)方式。实际上我们看this.join()源码就会
* 看出,this.join()就是调用了this.wait()方法,因为了this.wait()会释放
* this对象上的锁,所以当lock对象是自身时,主线程不会被锁住,所以第一个线程
* 会打印 "main - f()"。第二个线程的锁对象是JThread的Class对象,由于join
* 时不会释放JThread.class对象上的锁, 第二个线程会一直阻塞,所以第二个线程
* 不会打印 "main - f()",
*
*/
this.join();
/*
* 这样可以正常结束整个程序,因为this线程一直会阻塞直到对方(也是this的线程)运行完
* 或者是对方没有运行完等 1 毫秒后thsi线程继续运行,所以以这样的方式一定不会出现死锁
* 现象
*/
//this.join(1);
System.out.println(Thread.currentThread().getName() + " - join after");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void f() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " - f()");
}
}
}
sleep与join都会使当前线程处于阻塞状态,而yield则是进行就绪状态。
同步的静态方法的锁对象是方法所属类的Class对象,而同步的非静态方法的锁对象是所调用方法实例所对应的this对象。
继承Runnable与Thread的区别:Thread类本身也实现了Runnable接口。 因此除了构造 Runnable对象并把它作为构造函数的参数传递给Thread类之外,你也可以生成Thread类的一个子类,通过覆盖这个run方法来执行相应的操作。不过,通常最好的策略是把Runnable接口当作一个单独的类来实现,并把它作为参数传递给个Thread的构造函数。通过将代码隔离在单独的类中可以使你不必担心Runnable类中使用的同步方法和同步块与在相应线程类中所使用的其他任何方法之间的潜在操行所带来的影响。更一般地说,这种分离允许独立控制相关的操作和运行这些操作的上下文,同一个Runnable对象既可以传递给多个使用不同方式初抬化的Thread对象,也可以传递给其他的轻量级执行者(executor)。同样需要注意的是,继承了Thread类的刘象不能再同时继承其他类了。
如果线程被启动并且没有终止,调用方法isAlive将返回true。如果线程仅仅是因为某个原因阻塞,该方法也会返回true。
通过调用线程t的join方法将调用者挂起,直到目标线程t结束运行:t.join方法会在当t.isAlive方法的结果为false时返回。
有一些Thread类的方法只能应用于当前正在运行的那个线程中(也就是,调用Thread静态方法的线程),为了强制实施,这些方法都被声明为static:Thread.currentThread、Thread.interrupted、Thread.sleep、Thread.yield。
Thread.yield:仅仅是一个建议——放弃当前线程去运行其他的线程,JVM可以使用自己的方式理解这个建议。尽管缺少保证,但yield方法仍旧可以在一些单CPU的JVM实现上起到相应的效果,只要这些实现不使用分时抢占式的调用机制,在这种机制下,只有当一个线程阻塞时,CPU才会切换到其他线程上执行。如果在系统中线程执行了耗时的非阻塞计算任务的会占有更多的CPU时间,因而降低了应用程序的响应,为了安全起见,当执行非阻塞的计算任务的方法时,则可以在执行过程中插入yield方法(甚至是sleep方法)。为了减少不必要的影响,可以只在偶尔的情况下调用yield方法,比如一个包含如下语句的循环:
if(Math.random() < 0.01) Thread.yield();
使用抢占式调度机制的JVM实现,特别是在多处理器的情况下,yield才可能显得没有什么意义。
数组与容器之间的区别在三个方面:效率、类型(保持存放元素的类型)、保存基本类型的能力。但从Java SE5后泛型与自动装箱的出现,数组的优点就只是效率了。
无论使用哪种类型的数组,数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象又可能保存指向其他对象的引用。
新创建的数组未初始化时,如果存储的是对象,则所有元素自动初始化为null,基本类型初始化为0,字符型自动初始为(char)0,布尔型自动初始化为false。
不能创建泛型数组以及带类型参数的数组:
//T[] array = new T[SIZE]; // 不能创建泛型数组
//ArrayList<String> [] list = new ArrayList<String>[1]; // 也不能创建带类型参数的数组
但可以定义一个泛型数组的引用:
T[] array;
ArrayList<String> [] list;
虽然你不能创建泛型数组,但是可以创建非泛型数组然后将其转型:
public class ArrayOfGenerics {
public static void main(String[] args) {
List<String>[] ls;
List[] la = new List[10];
ls = (List<String>[])la; //会发生 "Unchecked" 警告
ls[0] = new ArrayList<String>();
//上面其实相当于下面一条语句
//List<String>[] ls1 = (List<String>[])new List[10];
// 编译时会产生错误:
//! ls[1] = new ArrayList<Integer>();
// 问题是: List<String> 是Object子类
Object[] objects = ls; //所以可以赋值给Object数组引用
// 编译与运行都没有错误,因为创建的List数组本身就是原生数组:
objects[1] = new ArrayList<Integer>();
}
}
Random实例对象可随机返回各种基本类型的数,可设置种子。但Math中的random方法只能返回[0.0, 1)之间的double型小数,实质上该方法就是调用Random实例的nextDouble()来实现的。
Arrays类提供了重载的equals()方法。数组相等的条件是元素个数必须相等,并且对应位置的元素也相等。
>>>动态创建数组<<<
现有如下应用,Employee[]数组满后扩容,该如何做?
Employee[] a = new Employee[100]
…
//arry is full
a = (Employee[])arrayGrow(a);
错误作法:
static Object[] arrayGrow (Object[] a) {
int newLength = a.length * 11 / 10 + 10;
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0, a.length);
return newArray;
}
上面在实际应用中会遇到一个问题,这段代码返回的数组类型是Object[],这是由于使用下面这行代码创建的数组:new Object[newLength]。如果现在我们要对Employee[]数组进行扩展时,则在扩充后我们不能将返回的Object[]类型的对象数组转换成Employee[]数组了(当然如果从Object[]类型对象数组中取出一个个元素后再强转为Employee是没问题)。将一个Employee[]临时地转换成Object[]数组,然后再把它转换回来是可以的,但一个从开始就是Object[]的数组却永远不能转换成Employee[]数组。为了编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。因此需用到反射包中的Array类的静态方法newInstance.:
static Object arrayGrow (Object a) {//参数是Object而不是Object[],因为整型数组类型int[]可以被转换成Object,但不能转换成对象数组
Class cl = a.getClass();
if (!cl.isArray()) {
return null;
}
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
int newLength = length * 11 / 10 + 10;
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, newLength);
return newArray;
}
另外,以下转换也是可以的:
String[] strArr = new String[10];
Object o = strArr;
strArr= (String[]) o;
第十七章容器的深入研究
可以使用Arrays.asList将数组或可变参数转换成AbstractList列表,其底层数据实现就是我们传进的参数数组,因此不能像其他List列表那样调整尺寸,如果你试图用add()或delete()方法在这种列表中添加或删除元素,就有可能会引发去修改数组尺寸的尝试,因此你将在运行时获得“Unsupported Operation(不支持的操作)”错误,该列表只支持读取与修改操作(另外,我们还可以通Collections.unmodifiableList(List)来对一个列表包装之后,只能进行读取操作,即使写是不行了)。
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}
public class AsListInference {
public static void main(String[] args) {
List<Snow> snow1 = Arrays.asList(
new Crusty(), new Slush(), new Powder());
// Won't compile:
// List<Snow> snow2 = Arrays.asList(
// new Light(), new Heavy());
// Compiler says:
// found : java.util.List<Powder>
// required: java.util.List<Snow>
// Collections.addAll() doesn't get confused:
List<Snow> snow3 = new ArrayList<Snow>();
Collections.addAll(snow3, new Light(), new Heavy());
// Give a hint using an
// explicit type argument specification:
List<Snow> snow4 = Arrays.<Snow>asList(
new Light(), new Heavy());
}
}
当试图创建snow2时,Arrays.asList()中只有Powder类型,因此它会创建List<Powder>而不是List<Snow>,尽管Collections.addAll()工作的很好,因为它从第一个参数中了解到了目标类型是什么。
正如你从创建snow4的操作中所看到的,可以在Arrays.asList()中间插入具体的类型,以告诉编译器对于由Arrays.asList()产生的List类型,实际的类型应该是什么。这称为显示类型参数说明。
Queue接口与List、Set同一级别,都是继承了Collection接口。LinkedList实现了Queue接口。Queue接口窄化了对LinkedList的方法的访问权限(即在方法中的参数类型如果是Queue时,就完全只能访问Queue接口所定义的方法了,而不能直接访问LinkedList的非Queue的方法),以使得只有恰当的方法才可以使用。
offer(),将一个元素插入到队尾,或者返回false。
peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,而element()会抛出NoSuchElementExcetption异常。
poll()和remove()方法将移除并返回队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementExcetption异常。
PriorityQueue:优先级队列。先进先出描述了最典型的队列规则,但优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。如果构建一个消息系统,某些消息比其他消息更重要,因而应该更快地得到外理,那么它们何时得到处理就与它们何时到达无关。PriorityQueue添加到JSE5中,是为了提供这种行为的一种自动实现。当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是你可能通过提供自己的Comparator来修改这个顺序。PriorityQueue可以确保当你调用peek()、poll()和remove()方法时,获取元素将是队列中优先级最高的元素。
Collection接口继承了Iterable接口,该接口包含一个能够产生java.util.Iterator的iterator()方法,可用于foreach语句中。
foreach语句可以用于数组或其他任何Iterable,但是这并不意味着数组是一个Iterable,数组与Iterable接口没有直接的关系。
你必须为散列存储和树型存储都创建一个equals()方法,但是hashCode()只有在这个类将会存放到HashMap、HashSet或者LinkedHashMap、LinkedHashSet中时才是必需的,因为这类Hash最终都是能过HashMap的containsKey方法使用if (e.hash == hash && eq(k, e.key))来实现对比的。
Set —— 存入Set的每个元素都必须是唯一的,因为Set不允许保存重复的元素。放入到Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
HashSet(优先选择) —— 底层以HashMap来实现。为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()。
TreeSet —— 保持次序的Set,底层为树结构。使用它可以从Set中读取有序的序列。放入的元素必须实现Comparable接口。
LinkedHashSet —— 底层以LinkedHashMap来实现。继承自HashSet,具有HashSet的查询速度,且内部使用链表维护元素顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素的插入次序显示。存入的元素也必须定义hashCode()方法。
HashMap(优先选择) —— Map基于散列表的实现(它取代了Hashtable)。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子,以调容器的性能。
LinkedHashMap —— 继承自HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最小使用(LRU)的次序。只比HashMap慢一点,而在迭代访问时反而更快,因为它使用链表维护内部次序。
TreeMap —— 基于红黑树的实现。查看“健”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于,所得到的结果是经过排序的。TreeMap是唯一带有subMap()方法的Map,它可以返回一个子树。
WeakHashMap —— 弱键映射,允许垃圾回收器回收无外界引用指向象Map中键,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此“键”可以被垃圾收集器回收。
ConcurrentHashMap —— 一种线程安全的Map,它不synchronized同步加锁,而是使用新的锁机制。尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。
IdentityHashMap —— 使用 == 代替equals()对“键”进行比较的散列映射。
看一个弱引用例子:
class WeakObject {
String name;
public WeakObject(String mwname) {
this.name = mwname;
}
public void finalize() {
System.out.println(name + "对象满足垃圾收集条件,被收集!");
}
public void show() {
System.out.println(name + "对象还可以使用!");
}
}
public class WeakReferenceTest {
public static void main(String[] args) {
System.out.println("---对象弱引用---");
WeakObject wo = new WeakObject("weakObject");
//包装成弱引用对象
WeakReference wr = new WeakReference(wo);
wo = null;
((WeakObject) wr.get()).show();
System.out.println("第一次垃圾收集!");
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (wr.get() != null) {
((WeakObject) wr.get()).show();
}
System.out.println("---弱引用map---");
WeakHashMap whm = new WeakHashMap();
WeakObject wo2 = new WeakObject("weakObjectKey");
//这里的值不会被回收
whm.put(wo2, new WeakObject("weakObjectValue"));
wo2 = null;
((WeakObject) whm.keySet().iterator().next()).show();
System.out.println("第二次垃圾回收!");
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
((WeakObject) whm.keySet().iterator().next()).show();
}
}
对于底层采用数组的ArrayList,无论列表的大小如何,这些访问都很快和一致。而对于LinkedList,访问时间对于较大的列表将明显增加。很显然,如果你需要执行大量的随机访问,链接链表不会是一种好的选择;
插入时,对于ArrayList,当列表变大时,其开销将变得很高昂,但是对于LinkedList,相对来说比较低廉,并且不随列表尺寸而发生变化,这是因为ArrayList在插入时,插入点后面所有元素将后移,如果在插入时超过了数组的最大容量,则会重新创建一个新的数组,并将所有元素复制到新的数组中(不过插入与扩容时使用的都是System.arraycopy方法,效率上比使用循环一个个移动要高),这会随ArrayList的尺寸增加而产生高昂的代价。LinkedList只需链接新的元素,而不必修改列表中剩余的元素,因此可以认为无论列表尺寸如何变化,其代价大致相同。
在LinkedList中的插入和移除代价相当低廉,并且不随列表尺寸发生变化,但是对于ArrayList插入操作代价特别高昂,并且其代价将随列表尺寸增加而增加。
HashSet的性能基本上总是比TreeSet好,特别是在添加和查询元素时,而这两个操作也是最重要的操作。TreeSet存在的唯一原因是它可以维持元素的排序状态;所以,只有当需要一个排好序的Set时,才应该使用TreeSet。因为其内部结构支持排序,并且因为迭代是我们更有可能执行的操作,所以,用TreeSet迭代通常比用HashSet要快。
除了IdentityHashMap,所有的Map实现的插入操作都会随着Map尺寸变大而明显变慢,但是,查找的代价通常比插入要小得多。
Hashtable的性能大体上与HashMap相当,因为HashMap是用来替代Hashtable的,因为它们使用了相同的底层存储和查找机制,这并没有什么令人奇怪的。
TreeMap通常比HashMap要慢。
LinkedHashMap在插入 时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表(以保持插入顺序),正是由于这个列表,使得其迭代速度更快一点。
IdentityHashMap则具有完全不同的性能,因为它使用==而不是equals()来比较元素。
负载因子小的Hash表产生冲突的可能性小,因此对于插入和查找都是最理想(但是会减慢使用迭代器进行遍历的过程,因为还有很多的空位置,这些空的位置也会在循环中遍历到)。当容量扩大时,现有的对象将重新分布到亲的桶位中(这被称为再散列)。HashMap使用的默认负载因子为0.75(只有当表的饱和度达到四分之三时,才进行再散列),这个因子在时间和空间代价之间达到平衡,更大的负载因子可以提高空间的利用率,但是会增加查找代价。
如果你知道将要在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将可以避免自动再散列的开销。
形如Collections.unmodifiableCollection(Collection<? extends T> c)一类方能产生只读容器。
形如Collections.synchronizedCollection(Collection<T> c, Object mutex)一类方能产生同步容器。
快速报错:Java容器有一种保护机制,能够防止多个进程(或直接通过容器本身而不是迭代器)同时修改同一个容器的内容。如果在你迭代遍历某个容器的过程中,另一个进程介入其中,并且插入、删除或修改此容器内的某个对象,那就会出现问题:也迭代过程已经处理过容器中的该元素,也许还没处理,也许在调用size()之后容器的尺寸缩小了等等,Java容器类类库采用快速报错机制。它会先检测容器上的任何除了你的进程所进行的操作或使用迭代器外的操作所引起的变化,一进发现改变,就会立刻抛ConcurrentModificationExcetion异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检测问题。所以应该在添加、删除、修改完所有元素之后,再获取迭代器。ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationExcetion的技术。
第十八章I/O 第十九章枚举
enum的values()方法返回enum实例的数组,而且该数组中的元素严格保持其在enum中声声明时的顺序。
创建enum时,编译器会为你生成一个相关的类,这个类继承自java.lang.Enum。
Enum类实例的ordinal()方法返回一个int值,这是每个enum实例在声明时的次序,从0开始。可以使用==来比较enum实例,编译器会自动为你提供equals()和hashCode()方法。
Enum类实现了Comparable接口,所以它具有compareTo()方法,同时,它还实现了Serializable接口。
name()方法返回enum实例声明的名字,与使用toString()方法效果一样。
valueOf(Class<T> enumType, String name)是在Enum中定义的static方法,它根据所给定的名字返回相应的enum实例,如果不存在给定名字的实例,将会抛出异常。
除了不能继承自一个enum之外,我们基本上可以将enum看作一个常规的类,也就是说,我们可以向enum中添加方法属性。
public enum OzWitch {
// enum实例必须定义在最前,并在方法之前:
WEST("west."), NORTH("north."),
EAST("east."), SOUTH("south.");//如果还有其他方法,则这个分号一定要写上
private String description;//可以定义属性
// 注,枚举的构造函数只能是包访问或private访问权限:
private OzWitch(String description) {
this.description = description;
}
//也可以有方法
public String getDescription() {
return description;
}
//还可以有main方法
public static void main(String[] args) {
for (OzWitch witch : OzWitch.values())
System.out.println(witch + ": " + witch.getDescription());
}
}
>>>values()的神秘之处<<<
public enum Explore {
HERE, THERE
}
使用javap反编译后:
public final class Explore extends java.lang.Enum{
public static final Explore HERE;
public static final Explore THERE;
public static final Explore[] values();// 编译器自动添加的方法
public static Explore valueOf(java.lang.String);// 编译器自动添加的方法
static {};
}
Enum类没有values()方法,values()是由编译器为enum添加的static方法,在创建Explore的过程中,编译器还为它创建了valueOf(String name)方法,这可能有点怪了,Enum类不是已经有valueOf(Class<T> enumType, String name)了吗?不过Enum中的valueOf()方法需要两个参数,而这个新增的方法只需一个参数。
由于values()方法是由编译器插入到enum定义中的static方法,所以,如果你将enum实例向上转型为Enum,那么values()方法就不可访问了。不过,在Class中有一个getEnumConstants()方法,所以即使Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有enum实例(注:只有是Enum类型的Class才能调用getEnumConstants方法,否则抛空指针异常):
enum Search { HITHER, YON }
public class UpcastEnum {
public static void main(String[] args) {
Search[] vals = Search.values();
Enum e = Search.HITHER; // 向上转型
// e.values(); // Enum中没有 values()方法
// 但我们可以通过Class对象的getEnumConstants()反射出enum实例
for(Enum en : e.getClass().getEnumConstants())
System.out.println(en);
}
}
>>>enum还可实现其它接口<<<
由于enum可以看是一个普通的类,所以他还可以实现其他接口。
enum CartoonCharacter implements Generator<CartoonCharacter> {
SLAPPY, SPANKY, BOB;
private Random rand = new Random(47);
public CartoonCharacter next() {
return values()[rand.nextInt(values().length)];
}
}
>>>枚举实例的“多态”表现<<<
enum LikeClasses {
WINKEN { void behavior() { System.out.println("Behavior1"); } },
BLINKEN { void behavior() { System.out.println("Behavior2"); } },
NOD { void behavior() { System.out.println("Behavior3"); } };
abstract void behavior();//抽象方法,由每个enum实例实现,当然也可是一个实体方法
public static void main(String[] args) {
for(LikeClasses en:values()){
en.behavior();//好比多态
}
}
// 但不能把枚举实例看作是类,因为它们只是一个实例
// void f1(LikeClasses.WINKEN instance) {}
}
经过反编译后,我们发现生成了四个类LikeClasses.class、LikeClasses$2.class、
LikeClasses$3.class、LikeClasses$1.class,而且LikeClasses$xx.class继承自LikeClasses.class类,并将各自的behavior方法代码放入到自己相应的类文件中。
第二十章注解
JavaSE5内置了三种标准注解,定义在java.lang中的注解:
@Override,表示当前的方法定义将覆盖超类中的方法。如果没有重写,编译器会发出错误提示。
@Deprecated,如果程序员使用了该注解注解过的元素,那么编译器会发出警告信息。
@SuppressWarnings,关闭不当的编译器警告信息。
元注解是负责注解其他的注解,有四种元注解:
@Target 表示该注解可以用于什么地方。可能的ElementType(枚举)参数包括:
CONSTRUCTOR:构造方法声明
FIELD:域声明(包括enum实例)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类,接口(包括注解类型)或enum声明
@Retention 表示需要在什么级别保存注解信息。可用的RetentionPolicy(枚举)参数包括:
SOURCE:注解将被编译地器丢弃。
CLASS:注解在class文件中可用,但会被VM丢弃。
RUNTIME:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documented 将此注解包含在JavaDoc中。
@Inherited 允许子类继承父类中的注解。
Class、Method、Field、Constructor类都实现了AnnotatedElement接口,他们的getAnnotation方法都返回指定类型的注解对象,如果没有该类型的注解,则返回null值,以下是这些方法的原型:
class. getAnnotation(Class<T> annotationClass)
method. getAnnotation(Class<T> annotationClass)
field. getAnnotation(Class<T> annotationClass)
constructor. getAnnotation(Class<T> annotationClass)
注解的定义看起来很像接口的定义,事实上,与其他任何Java接口一样,注解也会编译成class文件。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
* 这是一个简单的注解,我们可以用它来跟踪一个项目中的用例。如果一个方法
* 或一组方法实现了某个用例的需求,那么程序员可以为此方法加上该注解。
*/
//注解的定义
@Target(ElementType.METHOD)//该注解用于方法
@Retention(RetentionPolicy.RUNTIME)//注解信息保留到运行期
public @interface UseCase {
public int id();//int型的元素
public String description() default "no description";//String型的元素
}
//注解的使用
class PasswordUtils {
@UseCase(id = 47, description = "密码必须至少包含一个数字")
public boolean validatePassword(String password) {
return (password.matches("\\w*\\d\\w*"));
}
@UseCase(id = 48)//没有描述,使用默认的描述信息
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "新密码不能使用以前使用过的密码")
public boolean checkForNewPassword(
List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
//注解处理器
class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases, Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
// 通过反射获取某个方法特定的注解信息
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("找到用例:" + uc.id() +
" " + uc.description());
useCases.remove(new Integer(uc.id()));
}
}
for(int i : useCases) {
System.out.println("警告: 所缺用例-" + i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<Integer>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
/*
找到用例:47 密码必须至少包含一个数字
找到用例:48 no description
找到用例:49 新密码不能使用以前使用过的密码
警告: 所缺用例-50
*/
注解里的组成元素类型:
1、 所有基本类型
2、 String
3、 Class
4、 enum
5、 Annotation
6、 以上类型的数组
如果你使用了其他类型,那编译器就会报错。
注解组成元素的默认值限制:首先,元素不能有不确定的值,也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值,不允许即没给定默认值,在使用进也没指定值的情况出现。其次,对于非基本类型的元素,无论是在声明还是在使用时,都不能以null作为其值。
注解不支持继承。
第二十一章并发
Thread.yield():对线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议,它是在说“我已经执行完生命期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机”,即对线程的一种让步,暂停当前正在执行的线程,并执行其他线程。
>>>使用Executor<<<
Java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。
Executors:Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类的工厂和实用方法。
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。
public class LiftOff implements Runnable {
protected int countDown = 10; // Default
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run() {
while(countDown-- > 0) {
System.out.print(status());
Thread.yield();
}
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#2(9), #0(9), #4(9), #1(9), #3(9), #2(8), #0(8), #4(8), #1(8), #3(8), #2(7), #0(7), #4(7), #1(7), #3(7), #2(6), #0(6), #4(6), #1(6), #3(6), #2(5), #0(5), #4(5), #1(5), #3(5), #2(4), #0(4), #4(4), #1(4), #3(4), #2(3), #0(3), #4(3), #1(3), #3(3), #2(2), #4(2), #1(2), #3(2), #2(1), #4(1), #1(1), #3(1), #2(Liftoff!), #4(Liftoff!), #1(Liftoff!), #3(Liftoff!), #0(2), #0(1), #0(Liftoff!),
*/
Executors. newFixedThreadPool (int nThreads):可一次性预先执行代价高昂的线程分配,因而也就可以限制线程数量。不用为每个任务都固定地付出创建线程的开销。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPool {
public static void main(String[] args) {
// Constructor argument is number of threads:
ExecutorService exec = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#3(9), #4(9), #0(9), #2(9), #1(9), #3(8), #4(8), #0(8), #2(8), #1(8), #3(7), #4(7), #0(7), #2(7), #3(6), #4(6), #0(6), #2(6), #3(5), #4(5), #0(5), #2(5), #3(4), #4(4), #0(4), #2(4), #3(3), #4(3), #0(3), #2(3), #3(2), #4(2), #0(2), #2(2), #3(1), #4(1), #0(1), #2(1), #3(Liftoff!), #4(Liftoff!), #0(Liftoff!), #2(Liftoff!), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!),
*/
Executors. newSingleThreadExecutor (int nThreads):确保任意时刻在任何都只有唯一的任务在运行,你不需要在共享资源上处理同步,可以让你省去只是为了维持某些事物的原型而进行的各种协调努力。
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec =
Executors.newSingleThreadExecutor();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(Liftoff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),
*/
Thread.sleep(100);等同于TimeUnit.MILLISECONDS.sleep(100);
Daemom线程:后面线程不属于程序中不可缺少的部分,因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。返过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
如果一个线程是后台线程,那么它创建的任何线程将被自动设置成后台线程。
后台线程在不执行finally子句的情况下就会终止其run()方法,即后台线程的finally子句不一定执行。
在构造器中启动线程可能会有问题,因为线程可能会在构造器结束之前开始执行,这意味着该线程能够访问处于不稳定状态的对象。
异常不能跨线程传播给main(),所以你必须在了本地处理所有在线程内部产生的异常。
public class ExceptionThread implements Runnable {
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
try {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch(RuntimeException ue) {
// 这句将不会被执行,因为线程的异常是不会传递到调用它的线程的
System.out.println("Exception has been handled!");
}
}
}
Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在因未捕获的异常而临近死亡时被调用。为了使用它,我们创建了一个新类型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler,并将这个工厂传递给Exceutors创建新的ExcecutorService方法:
// 线程
class ExceptionThread2 implements Runnable {
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println("1.eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();//线程运行时一定会抛出运行异常
}
}
// 线程异常处理器
class MyUncaughtExceptionHandler implements
Thread.UncaughtExceptionHandler {
// 异常处理方法
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
// 线程工厂,创建线程时会调用该工厂
class HandlerThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {//线程创建工厂方法
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
//设置异常处理器
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("2.eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(
new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
} /*
HandlerThreadFactory@1a758cb creating new Thread
created Thread[Thread-0,5,main]
2.eh = MyUncaughtExceptionHandler@69b332
run() by Thread[Thread-0,5,main]
1.eh = MyUncaughtExceptionHandler@69b332
caught java.lang.RuntimeException
*/
如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器:Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
>>>使用Lock对象<<<
Lock对象必须被显式地创建、锁定和释放。因此,它与内建的锁形式相比,代码缺乏优雅性,但对于解决某些类型的问题来说,它更加灵活。
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
//…
} finally {
lock.unlock();
}
}
使用lock()和unlock()方法在next()内部创建了临界资源。还可以尝试获取锁:
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
//…
} finally {
if(captured)
lock.unlock();
}
}
>>>使用volatile对象<<<
原子操作是不能被线程调试机制中断的操作,一旦开始操作,那么它一定会在切换到其他线程前执行完毕。
原子操作可以应用于除long和double之外的所有基本类型之上的“简单操作”,对于读取和写入除long和double之外的基本类型变量这种的操作,可以保证它们会被当作原子操作来操作内存。但是JVM可以将64位(long和double变量)的读取和写入当作两个分离的32位操作来执行,这就可能会产生了在一个读取和写入操作中间切换线程,从而导致不同的线程看到不正确结果的可能性。但是,当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性,注:在Java SE5之前,volatile一直未能正确的工作。
volatile关键字还确保了应用中的可视性,如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改,即便使用了本地缓存,情况确实如此,volatile域会立即被写入到主存中。
在非volatile域上的操作没有刷新到主存中去,因此其他读取该域的线程将不能必看到这个新值。因此,如果多个线程同时访问了某个域,那么这个域就应该是volatile的,否则,这个域应该只能由同步来访问,同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来保护,那就不必将其设置为volatile了。
什么才属于原子操作呢?对域中的值做赋值和返回操作都是原子性的。但i++; i+=2; 这样的操作肯定不是原子性的,即线程有可能从语句的中间切换。下面来证明i++在java里不是原子性操作的:
class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static /* synchronized */int nextSerialNumber() {
// 不是线程安全,因为i++在Java里不是原子操作,
// 即使將serialNumber设置成了volatile
return serialNumber++;
}
}
class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
array = new int[size];
len = size;
// 初始化为-1
for (int i = 0; i < size; i++) {
array[i] = -1;
}
}
public synchronized void add(int i) {
array[index] = i;
// 如果数组满后从头开始填充,好比循环数组:
index = ++index % len;
}
public synchronized boolean contains(int val) {
for (int i = 0; i < len; i++) {
if (array[i] == val) {
return true;
}
}
return false;
}
}
public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials = new CircularSet(1000);
private static ExecutorService exec = Executors.newCachedThreadPool();
static class SerialChecker implements Runnable {
public void run() {
while (true) {
int serial = SerialNumberGenerator.nextSerialNumber();
if (serials.contains(serial)) {// 如果数组中存在则退出
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);// 如果不存在,则放入
}
}
}
public static void main(String[] args) throws Exception {
SerialChecker sc = new SerialChecker();
// 启动10线程
for (int i = 0; i < SIZE; i++) {
exec.execute(sc);
}
}
}
public class Increament extends Thread {
public static volatile int x = 0;
public void run() {
// synchronized (Increament.class) {
// x++与 x = x + 1都不是原子操作
x++;
// x = x + 1;
// }
}
public static void main(String[] args) throws Exception {
Thread threads[] = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Increament();
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
// 等待计算线程运行完
threads[i].join();
}
System.out.println("n=" + Increament.x);
}
}
如果对x的操作是原子级别的,最后输出的结果应该为x=10000,而在执行上面积代码时,很多时侯输出的x都小于10000,这说明x++ 不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用。
同一时刻只有一个线程能访问synchronized块,synchronized块并不是一下子要执行完毕,CPU调试可能从synchronized块中的某个语句切换到其它的线程,再其它线程执行完毕后再继续执行该同步块。切换到其他线程时是否释放synchronized块上的锁,这要看切换所采用的方式:如果是CPU自动或调用Thread.yeild切换,则不会释放;如果是调用wait,则会释放;如果是调用的Thread.sleep,则不会;如果是调用的thread.join,则要看synchronized块上的锁是否是thread线程对象,如果不是,则不会释放,如果是,则会释放。
只能在同步控制方法或同步控制块里调用wait()、notify()和notifyAll(),并且释放操作锁,但sleep()可以在非同步控制方法里调用,不会释放锁。
sleep、yield都是Thread的静态方法,join属于Thread的非静态方式,如果将它们放入在同步块中调用时都不会释放锁。但wait属于Object类的方法,在wait()期间对象锁是释放的。
在执行同步代码块的过程中,遇到异常而导致线程终止,锁会释放。
执行线程的suspend()方法会导致线程被暂停,并使用resume()可唤醒,但不会释放锁。
当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就绪状态,那么yield()方法将把当前运行的线程放到可运行池中并使用中另一线程运行。如果没有相同优先级的可运行进程,则该方法什么也不做。
sleep方法与yield方法都是Thread类的静态方法,都会使当前处于运行的线程放弃CPU,把运行机会让给另的线程。两都的区别:
1. sleep方法会给其他线程运行的机会以,而不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;yield方法只会给相同或更高优先级的线程一个运行的机会。
2. 当线程执行了sleep方法后,将转到阻塞状态。当线程执行了yield方法后,将转入就绪状态。
3. Sleep方法比yield方法具有更好的可移植性。不能依靠yield方法来提高程序的并发性能。对于大多数程序员来说,yield方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误。
thread.join():当前线程调用另一线程thread.join()时,则当前运行的线程将转到阻塞状态,并且等待thread线程运行结束后,当前线程程才会恢复运行(从阻塞状态到就绪状态)。比如有3个线程在执行计算任务,必须等三个线程都执行完才能汇总,那么这时候在主线程里面让三个线程join,最后计算结果既可:
public class JoinTest {
public static void main(String[] args) {
Rt[] ct = new Rt[3];
for (int i = 0; i < ct.length; i++) {
ct[i] = new Rt();
ct[i].start();
try {
//主线等待三个线程终止后再继续运行
ct[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int total = 0;
for (int j = 0; j < ct.length; j++) {
total += ct[j].getResult();
}
System.out.println("total = " + total);
}
}
class Rt extends Thread {
private int result;
public int getResult() {
return result;
}
public void run() {
try {
Thread.sleep(1000);
result = (int) (Math.random() * 100);
System.out.println(this.getName() + " result=" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
join()只能由线程实例调用,如果thread.join()在同步块中调用,并且同步锁对象也是thread对象,由于thread.join()是调用thread.wait()来实现的,wait会释放thread对象锁,则thread.join()与在同步块的锁也会一并释放;如果thread.join()在同步块的锁对象不是thread对象,则thread线程阻塞时不会释放锁:
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
JThread t = new JThread();
start(t, t);
System.out.println("--------");
t = new JThread();
start(t, JThread.class);
}
static void start(JThread t, Object lock) {
t.setLock(lock);
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果锁对象是JThread.class时,则主线程会一直阻塞
t.f();
}
}
class JThread extends Thread {
private Object lock;
void setLock(Object lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " - join before");
/*
* 当前线程阻塞,又要等待自己运行完,这是矛盾的,所以其实该线程永远不会恢复执
* 行,除非使用 join(long millis)方式。实际上我们看this.join()源码就会
* 看出,this.join()就是调用了this.wait()方法,因为了this.wait()会释放
* this对象上的锁,所以当lock对象是自身时,主线程不会被锁住,所以第一个线程
* 会打印 "main - f()"。第二个线程的锁对象是JThread的Class对象,由于join
* 时不会释放JThread.class对象上的锁, 第二个线程会一直阻塞,所以第二个线程
* 不会打印 "main - f()",
*
*/
this.join();
/*
* 这样可以正常结束整个程序,因为this线程一直会阻塞直到对方(也是this的线程)运行完
* 或者是对方没有运行完等 1 毫秒后thsi线程继续运行,所以以这样的方式一定不会出现死锁
* 现象
*/
//this.join(1);
System.out.println(Thread.currentThread().getName() + " - join after");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void f() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " - f()");
}
}
}
sleep与join都会使当前线程处于阻塞状态,而yield则是进行就绪状态。
同步的静态方法的锁对象是方法所属类的Class对象,而同步的非静态方法的锁对象是所调用方法实例所对应的this对象。
继承Runnable与Thread的区别:Thread类本身也实现了Runnable接口。 因此除了构造 Runnable对象并把它作为构造函数的参数传递给Thread类之外,你也可以生成Thread类的一个子类,通过覆盖这个run方法来执行相应的操作。不过,通常最好的策略是把Runnable接口当作一个单独的类来实现,并把它作为参数传递给个Thread的构造函数。通过将代码隔离在单独的类中可以使你不必担心Runnable类中使用的同步方法和同步块与在相应线程类中所使用的其他任何方法之间的潜在操行所带来的影响。更一般地说,这种分离允许独立控制相关的操作和运行这些操作的上下文,同一个Runnable对象既可以传递给多个使用不同方式初抬化的Thread对象,也可以传递给其他的轻量级执行者(executor)。同样需要注意的是,继承了Thread类的刘象不能再同时继承其他类了。
如果线程被启动并且没有终止,调用方法isAlive将返回true。如果线程仅仅是因为某个原因阻塞,该方法也会返回true。
通过调用线程t的join方法将调用者挂起,直到目标线程t结束运行:t.join方法会在当t.isAlive方法的结果为false时返回。
有一些Thread类的方法只能应用于当前正在运行的那个线程中(也就是,调用Thread静态方法的线程),为了强制实施,这些方法都被声明为static:Thread.currentThread、Thread.interrupted、Thread.sleep、Thread.yield。
Thread.yield:仅仅是一个建议——放弃当前线程去运行其他的线程,JVM可以使用自己的方式理解这个建议。尽管缺少保证,但yield方法仍旧可以在一些单CPU的JVM实现上起到相应的效果,只要这些实现不使用分时抢占式的调用机制,在这种机制下,只有当一个线程阻塞时,CPU才会切换到其他线程上执行。如果在系统中线程执行了耗时的非阻塞计算任务的会占有更多的CPU时间,因而降低了应用程序的响应,为了安全起见,当执行非阻塞的计算任务的方法时,则可以在执行过程中插入yield方法(甚至是sleep方法)。为了减少不必要的影响,可以只在偶尔的情况下调用yield方法,比如一个包含如下语句的循环:
if(Math.random() < 0.01) Thread.yield();
使用抢占式调度机制的JVM实现,特别是在多处理器的情况下,yield才可能显得没有什么意义。