Java编程思想-读书笔记

第一章

  • public,private,protected(继承的类可以访问)访问权限

  • 没有声明访问权限时的,默认访问权限(包访问权限)——类可以访问在同一个包(库构建)中的其他类的成员,但是在包之外,这些成员如同指定了private权限

  • 如果组合是动态发生的,那么它通常被称为聚合

  • 继承的类包含了父类的所有成员,尽管private成员被隐藏起来,并且不可访问

  • 在JAVA中,创建对象是在堆上进行,并且只有这一种创建对象的方式

  • JAVA的垃圾回收器被设计用来处理对象的内存释放问题,单不包括清理对象的其他方面。换言之,可能会有一种情况,一个对象没有被使用后,它所占的内存被释放,但是它没有被销毁,因为可以用来作为这类对象下次创建时的缓存。

  • 脚本语言 ,只是作为HTML页面一部分的简单文本。

第二章

1. 常量存储?

ROM(只读存储器),这种储存区的一个例子死字符串池,所有字面常量字符串和具有字符串值的常量表达式都自动是内存限定的,并且会置于特殊的储存区中。

2. 什么是持久化对象?

数据库存储

3. 如果在某个方法啊定义中有

    int x;

那么变量x得到的可能是任意值,而不会被自动化初始为零,但如果是作为类的成员变量,即使没有初始化,JAVA也会确保它获得一个默认值,即零。

4. 调用一个类中的静态(static)方法或变量时,是没有创建任何默认对象的,他们也不与任何对象关联在一起。

5. 使用类名是引用static变量和方法的首选方式,这样有利于编译器的优化

第三章

1 整数除法会直接去掉结果的小数位,而不是四舍五入的圆整结果

2 equal()的默认行为是比较引用

3 按位操作符用来操作基本数据类型中的单个二进制位,按位操作符会对两个参数中对应的位执行布尔代数运算

第四章

1 for()循环括号内的递增表达式(如:i++),在代码执行到循环末尾之前,递增表达式不会执行。

2 break和continue语句只是控制跳出最内层的循环。

3 在java里需要使用标签的唯一理由就是因为有循环嵌套的存在,而且想从多层嵌套中break或者continue

第五章

1. JAV中构造方法与类名同名的这种方式,是沿用了C++中采用的方式

2. 在类的定义中,默认情况下编译器会自动帮你创建无参构造方法,当类中已经定义了一个有参数的public构造方法,编译器就不会帮你创建无参构造方法。private的构造方法,只有在该类为另一个类的内部类时,可以在外部类中调用,否则,在其他的类中不能调用

3. 用new创建对象时,new表达式确实返回了对新对象的引用,但是构造方法本身并没有任何返回值

4. 调用方法传递参数时,如果传入的实际数据类型,小于方法中声明的形式参数类型,实际数据类型就会被自动提升,char类型略有不同,如果没有找到恰好接受char参数的方法,就会把char直接提升至int。

5. 不能根据返回值来区分方法重载

6. this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用

7. 用this()表示可在构造方法中调用另一个构造方法,但只能在构造方法中调用,不允许在其他方法中调用

8. JAVA垃圾回收器只知道释放那些经由new分配的内存??

9. finalize()的作用在于通过某种创建对象以外的方式(例如java代码中调用了C或C++语言的本地方法)为对象分配了内存,可释放内存。但不一定被垃圾回收器调用到。未被调用时,对象不会被清理,内存不会被释放。

10. 如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的内存占用,则随着程序的退出,那些资源也会全部交还给操作系统。GC本身也有开销,要是不使用它,那就不用支付这部分开销了。

11. 垃圾回收只与内存有关。

12. Java不允许(在堆栈上)创建局部对象。

  1. java本地程序(本地代码),可以提高程序运行效率??

14. JIT即时编译器,这种技术可以把程序的全部或者部分翻译成本地机器码(这本来是java虚拟机的工作),程序运行速度会得到提升。用这种方法编译所有代码的缺点,加载时间长,编译后的可执行代码长度增加。

  1. 类-对象加载顺序:①类加载:分配内存 加载类(静态)变量、类静态函数、成员函数 ② 对象加载:new对象 分配新内存 加载对象成员变量 成员函数引用 ③成员函数调用:分配新内存 加载成员函数局部变量
image.png

16. 类的成员变量的自动初始化,实在构造方法被调用之前进行的。但是方法内部的局部变量不会自动初始化。

17. 对于 Dog d = new Dog()这一语句,第一个Dog关键字声明将完成下图中的前2个步骤,关键字new将完成第三,第四,第五步骤,关键字Dog()将完成第六步骤

image.png

18. 静态或实例初始化子句,这种语法对于支持匿名内部类的初始化是必须的,它也使得你可以保证无论调用哪个显式构造方法,某些操作都会执行。

  1. 整数型数组和String型数组的内存分配原理有何不同??

第六章

1. 在java中,每一个.java文件是一个编译单元。当编译一个.java文件时,在.java文件中的每个类都会有一个输出文件,而该输出文件的名称与.java文件中每个类的名称相同,只是多了一个后缀名.class。因此在编译少量的.java文件之后,可能会得到大量的.class文件。

2. java构造函数的执行 http://blog.csdn.net/yidinghe/article/details/3839483

3. 如果一个编译单元中,可以完全不包含public类,这样.java文件可以随意取名。所以也就是说.java文件名并不是一定要与文件中的某个类名一样。

4. 类的访问权限既不可以是private,额不可以是protected,内部类是特例。如果不希望任何人对该类有访问权限,可以把所有的构造方法都指定为private,从而阻止任何人创建该类的对象。但是也有一个例外,就是你在该类的static成员内部可以创建对象。(单例模式的原理)

5. 如果一个类的访问权限是包访问权限,意味着,该类的对象可以由任何同一个包内的其他类创建,但是包外的类则不行,但如果该类的某个static成员是public的话,则包外的类仍然可以调用该static成员,只是不能创建该类的对象

6. 通过import引入一个包,非同一个包内的其他类,也只能创建该包内的public类的对象

第七章

1. 每一个非基本类型的对象,都有一个toString()方法,而且当编译器需要一个String,而你却只有一个对象的时候,就会调用这个方法。

2. 即使一个类不是public,只有包访问权限,其public main()方法仍然可以被调用。

3. 当创建一个导出类的对象时,该对象包含了一个基类的子对象,这个子对象与你用基类直接创建的对象是一样的,二者的差别在于,后者来自于外部,而基类的子对象被包含在导出类的对象内部。

4.
image.png

如果C类中的B类成员不是static,那么初始化的顺序是A-B-C,反之则是B-A-C,说明创建对象的初始化顺序,静态成员初始化-父类子对象初始化-成员变量初始化-执行构造方法体

5. 在选择使用组合还是继承时,一个最清晰的判断方法就是确实是否需要从子类到父类的向上转型。

6. java中的编译时常量,必须是基本数据类型,而且以final关键字表示,在对这个常量进行定义的时候,必须对其进行赋值。一个既是static又是final的域只占据一段不能改变的存储空间.

7. 对于基本数据类型,final使数值恒定不变,而对于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身是可以被修改的,java并未提供使任何对象恒定不变的途经,但是可以自己编写类以取得使对象恒定不变的效果。声明为final的域只能在定义时或者构造方法中被初始化,不能在成员方法中初始化。

8. final方法的主要原因是价格方法锁定,以防任何继承类修改它的定义。这是出于设计的考虑,想要确保在继承中确保方法行为保持不变,并且不会被覆盖。另一个原因是,在方法体代码不多的情况下(如getter和setter),将方法声明为final,可以将该方法的调用转为内嵌调用,以消除方法调用的开销。但是方法体过大时,会导致程序代码膨胀,从而看不到内嵌带来的性能提高。

9. 如果一个类的设计过于复杂、庞大,通过将现有类拆分为更小的部分(几个类)而添加更多的对象,通常会有所帮助。因为此时,在使用到这个类的地方,无需将整个类都加载,拆分后使用,只需要加载有用的部分。

第八章

1. 将一个方法调用与一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。而后期绑定则是在运行时根据对象的类型进行绑定。在java中除了static方法和final方法(private方法也属于final方法)之外,其他所有方法都是后期绑定。Java中所有方法都是通过动态绑定来实现多态的。

2.
image.png

在问题所述的情况下,无论是在基类的main方法中执行还是在导出类的main方法中执行,结果都是执行导出类覆盖的方法。

3. 如果你直接访问某个成员变量(域),这个访问就将在编译期进行解析。当子类对象向上转型为父类引用时,任何域访问操作都将由编译器解析,因此不是多态的。多态的动态绑定是在运行期进行。如果某个方法是static的,它的行为就具有多态性。因为静态方法是与类,而非与单个的对象相关联的。

4. 基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器有一个特殊的任务:检查对象是否被正确地构造。

5. 编写构造方法时有一条有效的准则:用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。在构造器内唯一能够安全调用的那些方法是基类中的final方法,这些方法不会被覆盖。

6. 在java SE5以后,子类覆盖父类的方法时,参数和返回值类型可以和父类不同,但必须是父类方法的相应参数和返回值类型的子类型。

7. 组合更加灵活,因为它可以动态选择类型;相反,继承在编译时就需要知道确切类型。用继承表达行为间的差异,用字段(组合)表达状态上的变化。

第九章

  1. 阻止产生一个类的任何对象:将该类定义为抽象类或者接口,将构造方法都定义为私有,...??

2. 抽象类并不需要所有方法都是抽象方法,甚至没有一个方法是抽象方法。

3. 可以选择在接口中显式的将方法声明为public的,但即使你不这么做,他们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被声明为public的。编译器不允许方法在被继承的过程中,访问权限降低。在接口中定义的域(成员变量)默认是static和final的。

4. 设计模式——策略:创建一个能够根据所传递的参数对象的不同,而具有不同行为的方法。这类方法中包含索要执行的算法中固定不变的部分,而"策略"包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。

5. 使用接口的核心原因:①为了能够向上转型为多个基类类型,由此带来的灵活性。②与使用抽象基类相同:防止创建该类的对象。所以,如果知道某事物应该成为一个基类,那么第一选择应该是使他成为一个接口。

6. 图中所示的接口CanFight和父类ActionCharacter中都包含一个public权限的fight()方法,在子类Hero同时继承ActionCharacter类和CanFight接口时,子类可以不用实现fight()方法,因为从父类继承的方法,实现了接口方法。

image.png

7. 下图中最后注释的两行代码编译时会报错,原因在于,编译器无法仅通过返回值类型来区分重载的方法。

image.png

8. 接口中的域,不是接口的一部分,它们的值被存储在该接口的静态存储区域。

9. 当实现某个接口时,并不需要实现嵌套在其内部的接口。而且,private接口不能在定义它的类之外被实现。

10. 任何抽象性都应该是应真正的需求而产生的。当必需时,你应该重构接口而不是到处添加额外级别的间接性(新接口),并由此带来额外的复杂性。恰当的原则应该是优先选择类而不是接口。从类开始,如果接口的必需性变得非常明确,那么就进行重构。

第十章

1. 当生成一个内部类的对象时,此对象与制造它的外围对象之间就有了一种联系。所以他能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权,包括私有成员。

2. 当某个外围类创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个其外围类对象的引用。然后,在你通过内部类对象访问其外围类对象的成员时,就是用那个引用来选择外围类的成员的。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。

3. 从实现了摸一个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。

4. 在一个方法中定义的一个内部类,在此方法之外的地方不能被访问。而且在方法中定义的内部类并不意味着一旦方法执行完毕,内部类就不可用了,方法可能被多次调用。

  1. 如果满足下面的一些条件,使用匿名内部类是比较合适的:

①只用到类的一个实例。 ②类在定义后马上用到。 ③类非常小(SUN推荐是在4行代码以下) ④给类命名并不会导致你的代码更容易被理解。

在使用匿名内部类时,要记住以下几个原则:

①匿名内部类不能有构造方法。 ②匿名内部类不能定义任何静态成员、方法和类。 ③匿名内部类不能是public,protected,private,static。 ④只能创建匿名内部类的一个实例。

·一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。

6. 嵌套类:如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static。这通常叫做嵌套类。普通的内部类对象总是隐式的保存了一个引用,指向创建它的外围类对象。然而,当内部类是static时,就不是这样了。嵌套类意味着:①要创建嵌套类的对象,并不需要其外围类的对象。②不能从嵌套类对象中访问非静态的外围类对象。

嵌套类与普通的内部类还有一个区别,普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static的字段和static的方法,也不能包含嵌套类。但是嵌套类可以包含这些东西。

7. 正常情况下,不能在接口内部放置任何代码,但嵌套类可以作为接口的一部分。放在接口中的任何类都自动是static和public的。你甚至可以在内部类中实现其外围接口。如果你想要创建某些公共代码,使得他们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得比较方便。

8. 一个内部类被嵌套多少层不重要。它能透明的访问所有他所嵌入的外围类的所有成员。

9. 每一个内部类都能独立地继承自一个接口的实现,所以无论外围类是否已经继承了某个接口的实现,对内部类都没有影响。

10. 设计模式总是将变化的事物与保持不变的事物分离开。

**第十一章 **

1. java中常用的容器类:List、Set、Queue和Map。因为他们都是Collection的子类,所以通常统称他们为"容器"

2. 通过使用泛型,可以在编译期就防止将错误类型的对象放置在容器中。

3. 编译期错误是指程序员在写代码时,代码下的红线提示的错误。运行时错误是程序运行起来以后的报错包括异常。

  1. 11-1

5. Arrays.asList()方法的限制是它对所产生的List的类型做出了最理想的假设,而并没有注意你会对它赋予什么样的类型。有时这就会引发问题:

11-2

6. Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素。Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序。

  1. HashMap提供了最快的查找速度,没有按照任何明显的顺序保存其元素。TreeMap按照比较结果的升序保存键,而LinkedHashMap则按照插入顺序来保存键,同时还保留了HashMap的查找速度。

8. 有两种类型的List:

  • 基本的ArrayList,它长于随机访问元素,但是在List的中间插入和删除元素时较慢

  • LinkedList,他通过代价较低的在list中间插入和删除元素,通过了优化的顺序访问。但LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList更大。LinkedList还添加了可以使其用作栈,队列或双向队列的方法。

9. subList()所产生的列表的幕后就是初始列表(的引用),因此,对subList()所返回列表的修改都会反映到初始列表当中去,反之亦然。

10. java中的Iterator(迭代器)只能单向移动,这个Iterator只能用来

  • 使用Iterator()方法要求容器返回一个Iterator,Iterator将准备好返回序列的第一个元素。

  • 使用next()方法获得序列中的下一个元素

  • 使用hasNext()检查序列中是否还有元素

  • 使用remove()将迭代器新近返回的元素删除

11. Set最常被使用的是测试归属性,你可以很容易的询问某个对象是否在某个Set中。正因为如此,查找,就成为了Set中最重要的操作,因此你通常都会选择一个HashSet的实现,它专门对快速查找进行了优化。Set是基于对象的值来确定归属性的。TreeSet将元素存储在红-黑树数据结构中,其结果是排序的。而HashSet使用的是散列函数。LinkedHashSet因为查找速度的原因也使用了散列,但是它使用了链表来维护元素的插入顺序。

12. Queue,队列常被当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中非常重要,因为他们可以安全的将对象从一个任务传输到另一个任务。LinkedList实现了queue接口。因为LinkedList可以看作是Queue的一种实现。


11-3

13. 用LinkedList来实现Queue或者Stack都是一种可靠的方案。各种Queue以及Stack的行为,有LinkedList提供支持。新程序中不应该使用过时的Vector、HashTable和Stack。

14. 用foreach遍历map http://www.cnblogs.com/fczjuever/archive/2013/04/07/3005997.html

15. 容器不能持有基本数据类型,但是自动包装机制会仔细地执行基本类型到容器内所持有的包装器类型之间的双向转换。

16.
11-4

第十二章

1. 当抛出异常后,有这么几件事会随之发生。首先,同java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径被终止,并且从当前环境中弹出异常对象的引用,传递给throw。此时异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式执行,要么继续运行下去。

2. 通常对异常来说,最重要的就是类名。

3. 在异常处理程序中,调用了在Throwable类声明的printStackTrace()方法,他将打印"从方法调用处直到异常抛出处"的方法调用序列,并将信息输出到标准错误流。printStackTrace()方法所提供的信息可以通过getStackTrace()方法直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组。其中每一个元素都表示栈中的一帧。元素0是栈顶元素,并且是调用序列中的最后一个方法调用(这个Throwable被创建和抛出的地方),数组中的最后一个元素和栈底是调用序列中的第一个方法调用。

4. 只有从RuntimeException继承的异常,可以在没有异常说明的情况下被抛出。它们会自动被java虚拟机抛出。如果RuntimeException没有被捕获而直达main(),那么在程序退出前将调用异常的printStackTrace()方法。请务必记住,只能在代码中忽略RuntimeException及其子类的异常,其他类型异常的处理都是由编译器强制执行。究其原因,RuntimeException代表的是编译错误:

  • 无法预料的错误,比如null引用。

  • 作为程序员,应该在代码中进行检查的错误。

5. 异常链,如果想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称作异常链。现在所有Throwable的子类在构造器中都可以接受一个cause对象作为参数。而这个cause对象用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出新的异常,也能通过异常链追踪到异常最初发生的位置。

6. Error用来表示编译时和系统错误,除特殊情况外,一般不用关心。Exception是可以被抛出的异常。

7. 当要把除内存之外的资源恢复到它们的初始状态时,就可以用到finally子句。这种需要清理的资源可能包括:已经打开的文件或网络资源,在屏幕上画的图形,某个功能的控制开关等。

8. 甚至在异常没有被当前的异常处理程序捕获的情况下,异常处理机制也会在跳到更高一层的异常处理程序之前,执行finally子句。在涉及到break和continue语句的时候,finally子句也会得到执行。请注意,如果把finally子句和带标签的break和continue配合使用,在java里面就没必要使用沟通语句了。finally子句会在break和continue中断程序之前执行。

while(true) {

try {

System.out.print("before break;");  

break; //此处换成return或continue 也一样  finally始终会执行

}finally

{ System.out.print("after break;"); }

}

9. 当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。

第十三章

1. 用于String的"+"和"+="是java中仅有的两个重载过的操作符,而java并不允许程序员重载任何操作符。

2. 如果你已经知道最终的字符串大概有多长,那预先指定StringBuilder的大小可以避免多次重新分配缓冲。

3. StringBuilder和StringBuffrer的比较:StringBuilder是java SE5引入的,在之前java用的是StringBuffer。后者是线程安全的,因此开销也会大一些。所以使用StringBuilder进行字符串操作会更快一些。

4. String类中的方法都会返回一个新的String对象。同时,如果内容没有发生改变,String的方法只会返回指向原对象的引用。这可以节约存储空间以及避免额外的开销。

5. Scanner类,http://lavasoft.blog.51cto.com/62575/182467/

第十四章

1. 类是程序的一部分,每个类都有一个Class对象。换言之,每次编写并编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的JVM将使用被称为"类加载器"的子系统。

类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,他是JVM实现的一部分。原生类加载器加载的是所谓的可信类,包括java API类,他们通常是从本地磁盘加载的。在这条加载链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持web服务器应用,或者在网络上下载类)。那么你有一种方式,可以挂接额外的类加载器。

2. 所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。因此,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。因此,java程序在他开始运行之前并非被完全加载, 其各个部分是在必要时才加载的。

类加载器首先检查这个类的Class对象是否已经被加载,如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能就会在数据库里查找字节码)。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良的JAVA代码。

3. Class的newInstance()方法是实现"虚拟构造器"的一种途径,虚拟构造器允许你声明:“我不知道你的确切类型,但是无论如何要正确地创建你自己。”当你通过这种方式创建新实例时,会得到Object引用。使用newInstance()来创建的类,必须带有默认的构造器。

14-1

5. Class引用总是指向某个Class对象,它可以制造类的实例,并包含可做用于这些实例的所有方法代码,它还包含该类的静态成员,因此,Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。

6. 尽管泛型类引用只能赋值为指向其声明的类型,但是普通的类引用可以被重新赋值为指向任何其他的Class对象,通过使用泛型语法,可以让编译器强制执行额外的类型检查。

14-2

同时,对于上图所示的代码,用Class优于平凡的Class,即便他们是等价的。并且平凡的Class如你所见,不会产生编译器警告信息。Class的好处是它表示你并非是碰巧或者由于疏忽,使用了一个非具体的类引用,而是你就是选择了非具体的版本。

7. 向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误,稍后立即就会发现。

8. 对instanceof有比较严格的限制,只可将其与命名类型进行比较,而不能与Class对象进行比较。Instanceof保持了类型的概念,它指的是"你是这个类吗,或者你是这个类的派生类吗",而如果用==比较实际的Class对象,就没有考虑继承,你或者是这个确切的类型,或者不是。

9. 当通过反射与一个未知类型的对象打交道时,JVM只是简单的检查了这个对象,看它属于哪个特定的类(就像RTTI那样),在用它做其他事情之前必须加载那个类的Class对象。因此,那个类的.class文件必须是可获取的。要么在本地机器上,要么可以从网络上获取。

10. RTTI和反射之间的真正区别只在于,对于RTTI来说,编译器在编译时打开和检查.class文件。换句话说,我们可以用"普通"方式调用对象的所有方法。而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

11. 反射详解【http://www.cnblogs.com/rollenholt/archive/2011/09/02/2163758.html】

12. java动态代理比代理的思想更向前迈进一步,因为它可以动态的创建代理并动态的处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。

第十五章

1. 泛型方法使得该方法可以独立于类而产生变化。以下是一个基本的指导原则;无论何时,只要你能做到,就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static需要使用泛型能力,就必须使其成为泛型方法。

2. 当使用泛型类时,必须在创建对象的时候指定类型参数的值,而调用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找到具体的类型。

3. 在泛型代码的内部,无法获得任何有关泛型参数类型的信息

因此,你可以知道诸如类型参数标识符和泛型类型边界这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数。java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。

15-1

4. 只有当你希望使用的类型参数比某个具体类型(以及它所有的子类型)更加“泛化”时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。因此,类型参数和他们在比较有用的泛型代码中的应用,通常比简单的类替换要更复杂。

5. 必须查看所有的代码,并确定它是否“足够复杂”到必须使用泛型的程度。

第十六章

1. 数组与其他种类的容器之间的区别有三个方面:效率,类型和保存基本类型的能力。在java中,数组是一种效率最高的保存和随机访问对象引用序列的方式。数组就是一个简单的线性序列,这使得元素访问非常迅速。

2. 相比之下的ArrayList,它可以通过创建一个新实例,然后把旧实例中的所有引用移到新实例中,从而实现更多空间的自动分配。尽管通常应该首选ArrayList而不是数组,但是这种弹性需要开销,因此ArrayList的效率比数组低很多。

3. 数组和容器都可以保证你不能滥用它们。无论你是使用的数组还是容器,如果越界,都会得到一个表示程序员错误的RuntimeException异常。

4. 无论哪种类型的数组,数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个数组对象用以保存指向其他对象的引用。可以作为数组初始化语法的一部分隐式地创建此对象,或者用new表达式显式地创建。

5. java标准类库中提供有static方法System.arraycopy(),用它来复制数组比用for循环复制要快得多。

第十八章

1. 字节存放顺序:不同的机器可能使用不同的字节排序方法来存储数据。"big endian"(高位优先)将最重要的字节存放在地址最低的存储器单元。而"little endian"(低位优先)则将最重要的字节放在地址最高的存储器单元。但存储量大于一个字节时,像int,float等,就要考虑字节的顺序问题了。ByteBuffer是以高位优先的形式存储的,并且数据在网上传送时也常常使用高位优先的形式
18-1

2. 文件加锁机制,允许我们同步访问某个作为共享资源的文件。不过,竞争同一文件的两个线程可能在不同的java虚拟机上,或者一个是java线程,另一个是操作系统中其他的某个本地线程。文件锁对其他的操作系统进程是可见的,因为java的文件加锁直接映射到了本地操作系统的加锁工具。

3. 通过对FileChannel调用tryLock()或Lock(),就可以获得整个文件的FileLock(SocketChannel、DatagramChannel和ServerSocketChannel不需要加锁,因为他们是从单进程实体继承而来;我们通常不在两个进程之间共享网络socket)。tryLock()是非阻塞式的,他设法获取锁,但是如果不能获得(当其他一些进程已经持有相同的锁,并且不共享时),它将直接从方法调用返回。lock()则是阻塞式的,他要阻塞进程直至锁可以获得,或调用lock()的线程中断,或调用lock()的通道关闭。使用FileLock.release()可以释放锁。

4. 尽管无参数的加锁方法将根据文件尺寸的变化而变化,但是具有固定尺寸的锁不随文件尺寸的变化而变化。如果你获得了某一个区域(从position到position+size)上的锁,当文件增大超过position+size之外的部分不会被锁定。无参数的加锁方法会对整个文件进行加锁,甚至文件变大后也是如此。

5. 对独占锁和共享锁的支持必须由底层的操作系统提供。如果操作系统不支持共享锁并为每一个请求都创建一个锁,那么他就会使用独占锁。锁的类型(共享或独占)可以通过FileLock.isShared()进行查询。

6. java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并且能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。也就是说,可以在运行windows系统的计算机上创建一个对象,将其序列化,通过网络发送给一个运行Unix系统的计算机,然后在那里准确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心字节顺序和其他任何细节。

7. 可以利用对象序列化实现轻量级的持久性。持久性意味着一个对象的生命周期并不取决于程序是否正在运行。它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久性的效果。之所以称其为“轻量级”,是因为不能用某种“persistent”(持久)关键字来简单的定义一个对象,并让系统自动维护其他细节问题。相反,对象必须在程序中显式地序列化和反序列化还原。如果需要一个更严格的持久化机制,可以考虑像hibernate这样的工具。

第二十一章

java线程安全的浅析【http://blog.csdn.net/xiao__gui/article/details/8934832】

1. java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

2. 当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并没有什么特殊之处——他不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器,或者使用Excutor.

3. FixedThreadPool,可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量。这可以节省时间,因为你不用为每个任务都固定的付出创建线程的开销。此外在任何线程池中,现有线程在可能的情况下,都会被自动复用。

4. CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。

5. SingleThreadExcutor就像是线程数量为1的FixedThreadPool。如果向SingleThreadExcutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。SingleThreadExcutor会序列化所有提交给它的任务,并会维护他自己(隐藏)的悬挂任务队列。

6. Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在java SE5中引入的Callable是一种具有类型参数的泛型,他的类型参数表示的是方法call()中返回的值,并且必须使用ExcutorService.submit()调用它。submit()方法会产生Future对象,他用Callable返回结果的特定类型进行了参数化,你可以用isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,他可以调用get()方法来获取返回结果。你也可以不用isDone()进行检查就直接调用get(),在这种情况下,get()将阻塞,直至结果准备就绪。你还可以在试图调用get()获取到结果之前,先调用具有超时的get(),或者是调用isDone()来查看任务是否完成。

7. 线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先级最高的线程先执行。然而,这并不是意味着优先级低的线程将得不到执行(也就是说,优先级不会导致死锁),优先级较低的线程仅仅是执行的频率较低。在绝大多数时间里,所有的线程都应该以默认的优先级运行。试图操纵线程优先级是一种错误。

8. 尽管JDK有10个优先级,但它与多数操作系统都不能映射得很好,比如,windows有7个优先级且不是固定的,所以这种映射关系也是不确定的。唯一可移植的方法是当调整优先级的时候,只使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三种级别。

9. 如果知道已经完成了在run()方法中的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示:我的工作已经做的差不多了,可以让别的线程使用CPU了。这个暗示将通过调用yield()方法来做出(不过这只是个暗示,没有任何机制保证它一定会被采纳)。当调用yield()时,你也是在建议具有相同优先级的其他线程可以运行。但是,大体上,对于任何重要的控制或者调整应用是,都不能依赖于yield()。实际上,yield()经常被误用。

10. 共享资源一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口,或者是打印机。要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问这个对象的方法标记为synchronized。如果某个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。

11. 所有对象都自动还有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,此对象都会被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁以后才能被调用。对于前面的方法,如果某个任务对对象调用了f(),对于同一个对象而言,就只能等到f()调用结束并释放了锁之后,其他任务才能调用f()和g()。所以,对于某个特定的对象来说,其所有的synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。

12. 在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。

13. 一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一个对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁(即锁被完全释放),其计数变为0。在任务第一次给对象加锁的时候,计数变为1。每当这个相同的任务在这个对象获得锁时,计数都会递增。显然,只有首先获得了锁的任务才能被允许继续获得多个锁。每当任务离开一个synchronized的方法,计数递减,当计数为零的时候,锁被完全释放,此时别的任务就可以使用该对象(资源)。

14. 针对每个类,也有一个锁(作为类的Class对象的一部分),所有synchronized static方法可以在类的范围内防止对static数据的并发访问。

15. 使用同步的原则:如果你正在写一个变量(改变变量的值),它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,而且,读写线程都必须使用相同的监视器锁同步。

如果在你的类中有超过一个方法在处理临界数据,那么你必须同步所有相关的方法。如果只同步一个方法,那么其他方法将会随意地忽略这个对象锁,并可以在没有任何惩罚的情况下被调用。这是很重要的一点:每个访问临界共享资源的方法都必须被标记为synchronized,否则它们就不会正确的工作。

16. 尽管try-finally所需的代码比synchronized关键字要多,但是这也代表了显式的使用Lock对象的优点之一。如果在使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常。但是你就没有机会去做任何清理工作,以维护系统使其处于良好状态。有了显示的Lock对象,你就可以使用finally子句将系统维护在正确的状态了。

大体上,当你使用synchronized关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的lock对象。例如,用synchronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃他它,要实现这些,你必须使用concurrent类库。

ReentrantLock允许你尝试着获取但最终未获取锁,这样如果其他线程已经获取了这个锁,那你就可以决定离开去执行其他一些事情,而不是等待直至这个锁被释放。显式的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,还赋予你更细粒度的控制力。这对于实现专有同步结构是很有用的,例如用于遍历链接列表中的节点的节节传递的加锁机制(也称为锁耦合),这种遍历代码必须在释放当前节点的锁之前捕获下一个节点的锁。

17. 原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程执行)执行完毕。一个常见的错误认识是:原子操作不需要进行同步控制。

18. 使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。再次提醒,你的第一选择应该是使用synchronized关键字,这是最安全的方式,而尝试其他任何方式都是有风险的。

19. synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。

20. 通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。采用同步控制块,对象不加锁的时间更长。这也是宁愿使用同步控制块而不是对整个方法进行同步控制的原因:使得其他线程能更多地访问(在安全的情况下尽可能多)。synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是使用其方法正在被调用的当前对象:synchronized(this)。在这种方式中,如果获得了synchronized块上的锁,阿么该对象其他的synchronized方法和临界区就不能被调用了。因此,如果是在this上同步,临界区的效果就会直接缩小在同步的范围内。

21. sleep()是使任务从执行状态变为阻塞状态。

22. 线程状态

  • 新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必需的资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变了可运行或阻塞状态。

  • 就绪(Runnable):这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行,这不同于死亡和阻塞状态

  • 阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。

  • 死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

23. 线程进入阻塞状态,可能有如下原因:

  • 通过调用sleep()使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。

  • 通过调用wait()使线程挂起。直到线程得到了notify()或notifyAll()消息,线程才会进入就绪状态。

  • 任务在等待某个输入/输出完成

  • 人物试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。

24. Thread类包含interrupt()方法,因此你可以终止被阻塞的任务。这个方法将设置线程的中断状态,如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将复位,Thread.interrupt()提供了离开run()循环而不抛出异常的第二种方式。

【Java Thread.interrupt 中断JAVA线程】

25. 为了调用interrupt(),你必须持有Thread对象。但是,新的concurrent类库似乎在避免对Thread对象的直接操作,转而尽量通过Excutor来执行所有的操作。如果你在Excutor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的所有线程。

26. 有时你也会希望只中断某个单一的任务。如果使用Excutor,那么如果通过调用submit()而不是excutor()来启动任务,就可以持有该任务的上下文。submit()将返回一个泛型Futrue,其中有一个未修饰的参数,因为你永远都不会在其上调用get()——持有这种Future的关键在于你可以在其上调用cancel(),并因此可以使用它来中断某个特定的任务。如果你将true传递给cancel(),那么它就会拥有在该线程上调用interrupt()以停止这个线程的权限。因此,cancel()是一种中断由Excutor启动的单个线程的方式。

27. 你能够中断对sleep()的调用(或者任何要求抛出InterruptedException的调用)。但是,你不能中断生在试图获取synchronized锁或者试图执行I/O操作的线程。这有点令人烦恼,特别是在创建执行I/O的任务时,因为这意味着I/O具有锁住你的多线程程序的潜在可能。特别是对于基于web的程序,这更是关乎厉害。对于这类问题,有一个略显笨拙但是有时确实行之有效的解决方案,即关闭任务在其上发生阻塞的底层资源。

28. 注意,当你在线程上调用interrupt()时,中断发生的唯一时刻在任务要进入到阻塞操作中,或者已经在阻塞操作内部时(除了不可中断的I/O或被阻塞的synchronized方法之外,在其他的例外情况下,你无所事事)。但是如果根据程序运行的环境,你已经编写了可能产生这种阻塞调用的代码,那又该怎么办呢?如果你只能通过在阻塞调用上抛出异常来退出。那么你就无法总是可以离开run()循环。因此,如果你调用interrupt()以停止某个任务,那么在run()循环碰巧没有产生任何阻塞调用的情况下,你的任务将需要第二种方式来退出。

这种机会是由中断状态来表示,其状态可以通过调用interrupt()来设置。你可以通过调用interrupted()来检查中断状态,这不仅可以告诉你interrupt()是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断这个问题通知你两次,你可以经由单一的InterruptedException或单一的成功的Thread.interrupted()测试来得到这种通知。如果想要再次检查以了解是否被中断,则可以在调用Thread.interrupted()时将结果保存起来。

29. wait()是你可以等待某个条件发生变化,而改变这个条件超过了当前方法(任务)的控制能力。通常,这种条件将由另一个任务来改变。wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生时,即表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所产生的变化。因此,wait()提供了一种在任务之间对活动同步的方式。

30. 调用sleep()或yield()的时候锁并没有被释放,理解这一点很重要。另一方面,当一个任务在方法里遇到了对wait()的调用的时候,线程的执行被挂起,对象上的锁被释放。因为wait()将释放锁,这就意味着另一个任务将可以获得锁,因此在该对象(现在是未锁定的)中的其他synchronized方法可以在wait()期间被调用。这一点至关重要,因为这些其他的方法通常将会产生改变,而这种改变正是使被挂起的任务唤醒锁感兴趣的变化。

31. 对于wait()而言:

  • 在wait()期间,对象锁是释放的

  • 可以通过notify(),notifyAll(),或者令时间到期,从wait()中恢复执行。

32. wait()、notify()以及notifyAll()这些方法是基类Object的一部分,而不是属于Thread的一部分。实际上,只能在同步控制方法或同步控制块里调用wait(),notify()和nnotifyAll()(因为不用操作锁,所以sleep()可以在非同步控制方法里调用)。如果在非同步控制方法里调用这些方法,程序能通过编译,但运行的时候,将得到IllegalMonitorStateException异常,并伴随着一些含糊的消息,比如"当前线程不是拥有者"。消息的意思是,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须“拥有”(获取)对象的锁。

33. 可以让另一个对象执行某种操作以维护其自己的锁。要这么做的话,必须首先得到对象的锁。比如,如果遥想对象x发送notifyAll(),那么就必须在能够取得x的锁的同步控制块中这么做:

synchronized(x){

x.notifyAll();

34. 你必须用一个检查感兴趣的条件的while循环包围wait()。这很重要,因为:

  • 你可能有多个任务出于相同的原因在等待同一个锁,而第一个被唤醒的任务可能会改变这种状况(即使你没有这么做,有人也会通过继承你的类去这么做)。如果属于这种情况,那么这个任务应该被再次挂起,直至其感兴趣的条件发生变化。

  • 在这个任务从其wait()中被唤醒的时刻,有可能会有某个其他的任务已经做出了改变,从而使这个任务在此时不能执行,或者执行其操作已显得无关紧要。此时,应该通过再次调用wait()来将其重新挂起。

  • 也有可能某些任务出于不同的原因在等待你的对象上的锁(在这种情况下必须使用notifyAll())。在这种情况下,你需要检查是否已经由正确的原因唤醒,如果不是,jiu再次调用wait()

    因此,其本质就是要检查锁感兴趣的特定条件,并在条件不满足的情况下返回到wait()中。惯用的方法就是使用while来编写这种代码。

  1. 错失的信号:当两个线程使用notify()/wait()或notityAll()/wait()进行协作时,有可能错过某个信号。

【http://m.blog.csdn.net/blog/oliveevilo/8481525】

36. 只有一个任务需要从wait()唤醒时,使用notify()而不是notifyAll(),是一种优化。使用notify()时,在众多等待同一个锁的任务中只会有一个被唤醒,因此如果你希望使用nofity(),就必须保证被唤醒的是恰当的任务。另外为了使用notify(),所有任务必须等待相同的条件,因为如果你有多个任务在等待不同的条件,那么你就不会知道是否唤醒了恰当的任务。如果使用notify(),当条件发生变化时,必须只有一个任务能够从中受益。最后,这些限制对所有可能存在的子类都必须总是起作用的。如果这些规则中有任何一条不满足,那么你就必须使用notifyAll()而不是notify()。

37. 调用notifyAll(),并不意味着在程序中任何地方,任何处于wait()状态的任务都将被任何对notifyAll()的调用唤醒。当notifyAll因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。

38. wait()和notifyAll()方法是以一种非常低级的方式解决了任务互操作的问题,即每次交互时都握手。在许多情况下,你可以瞄向更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。BlockingQueue接口中提供了这个队列,这个接口有大量的标准实现。如果消费者任务试图从队列中获取对象,而该队列此时为空,那么这些队列还可以挂起消费者任务,并且当有更多的元素可用时恢复消费者任务。阻塞队列可以非常大量的问题,而其方式与wait()和notifyAll()相比,则简单可靠得多。

39. 要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:

  • 互斥条件。任务使用的资源中至少有一个是不能共享的。比如,一根筷子一次只能被一个人使用。

  • 至少有一个任务它必须持有资源并且正在等待获取一个当前被别的任务持有的资源。也就是说,要发生死锁,某个人必须拿着一根筷子并且等待另一根。

  • 资源不能被任务抢占,任务必须把资源释放当作普通事件。就餐者很有礼貌,他们不会从其他人那里抢筷子。

  • 必须循环等待,这时,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。

因为要发生死锁的话,所有这些条件必须全部满足,所以要防止死锁的话,只需要破坏其中一个即可。通常,防止死锁最容易的方法是破坏第四个条件。

40. 新类库中的构件

  • CountDownLatch 他被用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作的完成。

  • CyclicBarrier 适用于这样的情况:你希望创建一组任务,他们并行地执行工作,然后在进行下一个步骤之前等待,直至所有任务都完成(看起来有些像join())

  • DelayQueue 这是一个无界的BlockingQueue,用于放置实现了Delay接口的对象 ,其中的对象只能在其到期时才能从队列中取走。

  • PriorityBlockingQueue 这是一个很基础的优先级队列,它具有可阻塞的读取操作。

  • ScheduledExcutor 提供了解决这类问题的服务:每个任务都是在预定时间运行的任务。

  • Semaphore 有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。

  • Exchanger 是在两个任务之间交换对象的栅栏。

  • SynchronousQueue 这是一种没有内部容量的阻塞队列,因此每一个put()都必须等待一个take(),反之亦然。

41. 三种经典的多线程并发问题的仿真:银行出纳员问题,饭店订单问题,汽车组装生产线问题。

42. 做多线程并发代码设计时,应该以synchronized关键字入手,只有在性能调优时才替换成Lock对象这种做法,是具有实际意义的。最显而易见的,就是提高代码的可读性,这在整个软件项目的开发和维护过程也是至关重要的。

43. 当你在自己的并发程序中可以使用Atomic类时,这肯定非常好,但是要意识到的是,Atomic对象只有在非常简单的情况下才有用,这些情况通常包括你只有一个要被修改的Atomic对象,并且这个对象独立于其他所有的对象。更安全的做法是:以更加传统的互斥方式入手,只有在性能方面的需求能够明确指示时,再替换为Atomic。

44. 早期java中的容器如Vector和HashTable具有许多synchronized方法,当他们用于非多线程的应用程序中,便会导致不可接受的开销。在JAVA SE5之后出现了免锁容器。这些免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改的过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。

45. 乐观加锁,这意味着当你执行某项计算时,实际上没有使用互斥,但是在这项计算完成,并且准备更新这个Atomic对象时,你需要使用一个称为compareAndSet()的方法。你将旧值和新值一起提交给这个方法,如果旧值与它在Atomic对象中发现的值不一样,那么这个操作就失败——这意味着某些其他的任务已经于此操作执行期间修改了这个对象。记住,我们在正常情况下将使用互斥(synchronized或Lock)来防止多个任务同时修改一个对象,这是这里我们是"乐观的",因为我们保持数据为未锁定状态,并希望没有任何其他任务插入修改它。所有这些又都是以性能的名义执行的——通过使用Atomic来代替synchronized或Lock,可以获得性能上的好处。

46. ReadWriteLock对数据结构相对不频繁的写入,但是有多个任务要经常读取这个数据结构的这类情况进行优化。ReadWriteLock使得你可以同时有多个读取者,只要它们都不试图写入即可。如果这个写锁已经被其他任务持有,那么任何读取者都不能读取,直至这个写锁被释放为止。只有当你在搜索可以提高性能的方法时,才应该想到用它,应该优先选择更直观的同步。

47. 线程的一个额外的好处是,他们提供了轻量级的执行上下文切换(大约100条指令),而不是重量级的进程上下文切换(要上千条指令)。因为一个给定进程内的所有线程共享相同的内空间,轻量级的上下文切换只是改变了程序的执行序列和局部变量。进程切换(重量级的上下文切换)必须改变所有内存空间。

你可能感兴趣的:(Java编程思想-读书笔记)