简答/设计题:
- 给出需求描述、ADT的基本代码
- 开展设计和代码:绘图/建模、设计、修改代码、写新代码、写注释
- (AF/RI/Spec/Safety from Rep Exposure/Testing Strategy /Thread Safety Argument)
- 设计测试用例、改进/优化各项质量指标等
第一、二章5%:软件构造基础、过程
第三章40%:ADT+OOP
第四、五章30%:可复用性+可维护性
第六章15%:健壮性
第七章10%:并行
软件多维视图
Code:代码的逻辑组织
functions
classes
methods
interfaces
Component:代码的物理组织
files
directories
packages
libraries
Code Churn
因素之间相互影响、矛盾、相关
首要:正确性!
SCM ≥ VCS
添加文件:
git add xxx.xxx
提交文件:git commit -m "message"
push到远程仓库:git push origin master
从远程仓库pull:git pull origin master
新建分支:
git checkout -b branch_name
切换分支:git checkout branch_name
orgit checkout master
选择一个分支与当前分支合并:git merge branch_name2
(之前已有指令git checkout branch_name1
)
Object Graph
Commit
管理变化:
Git存储发生变化的文件(而非代码行),不变化的文件不重复存储
Commits: nodes in Object Graph
Primitives | Object Reference Types |
---|---|
int, long, byte, short, char, float, double, boolean | Classes, interfaces, arrays, enums, annotations |
只有值,没有ID (与其他值无法区分) | 既有ID,也有值 |
不可变 | 可变/不可变 |
在栈中分配内存 | 在堆中分配内存 |
Can’t achieve unity of expression | Unity of expression with generics |
代价低 | 代价昂贵 |
- 静态类型检查错误:
- 语法错误
- 类名/函数名错误
- 参数数目错误
- 参数类型错误
- 返回值类型错误
- 动态类型检查错误:
- 非法的参数值
- 非法的返回值
- 越界
- 空指针
不变对象:一旦被创建,始终指向同一个值
可变对象:拥有方法可以修改自己的值/引用
final
final
作为方法的输入参数、作为局部变量final
表明了程序员的一种“设计决策”final
类无法派生子类final
变量无法改变值/引用final
方法无法被子类重写
- String 不可变
- StringBuilder 可变
/* String部分 */
String s = "a"; //开辟一个存储空间,里面存着字符a,s指向这块空间,记为space1
String t = s; //让t指向s所指向的空间即space1
s = s.concat("b"); //把字符a和字符b连接,然后把“ab”放在一个新的存储空间,记为space2,最后让s指向这块空间
//我们可以看到,现在s和t所指向的是两块不同的空间,空间中的内容也不一样,因此s和t的效果是不一样的
/* StringBuilder部分 */
StringBuilder sb = new StringBuilder("a"); //开辟一个存储空间,里面存着字符a
StringBuilder tb = sb; //开辟一个存储空间,里面存着字符a
sb.append("b"); //取出a,然后与字符b连接,然后把“ab”仍然放在这块空间内,把原来的“a”覆盖了,sb的指向没变
//在这个情况下,由于从始至终只用到了一块存储空间,所以sb和tb的效果实际上是相同的
mutable 优点:
UnmodifiableCollections:Java设计有不可变的集合类提供使用
通过防御式拷贝,给客户端返回一个全新的对象(副本),客户端即使对数据做了更改,也不会影响到自己。例如:
return new Date(groundhogAnswer.getTime());
大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费
如果使用不可变类型,则节省了频繁复制的代价
运行时、代码层面、瞬时
例:用Snapshot表示String和StringBuilder的区别
集合类Snapshot图
程序和客户端达成的一致
作用:
要素:
- 契约:
前置条件满足了,后置条件必须满足;
前置条件不满足,后置条件不一定满足(输入错误,可以抛出异常)。
解:
val
在范围内时,两者返回相同;val
不在范围内时,前者返回arr.length
,后者返回-1
;@param
@return
@throws
一个好的Spec应该:
设计ADT:规格Spec–>表示Rep–>实现Impl
return
新对象String.concat()
List
的.size()
void
,则必然改变了对象内部状态(必然是mutator)ADT开发者关注表示空间R,client关注抽象空间A
抽象函数(AF):
表示不变性(RI):
- 检查RI:
随时检查RI是否满足
在所有可能改变rep的方法内都要检查
Observer方法可以不用,但建议也要检查,以防止你的“万一”
因为测试相当于client使用ADT,所以它也不能直接访问ADT内部的数据域,所以只能调用其他方法去测试被测试的方法。
针对creator:构造对象之后,用observer去观察是否正确
针对observer:用其他三类方法构造对象,然后调用被测observer,判断观察结果是否正确
针对producer:produce新对象之后,用observer判断结果是否正确
接口:定义ADT
类:实现ADT
Concrete class --> Abstract Class --> Interface
接口:
抽象类:
具体类:
- 类 & 类:继承
- 类 & 接口:实现、扩展
覆盖/重写Override
:
super
super()
复用了父类型中函数的功能,还可以对其进行扩展super()
严格继承:子类只能添加新方法,无法重写超类中的方法(方法带
final
关键字)
特殊多态:功能重载
public void changeSize(int size, String name, float pattern) {}
public void changeSize(int length, String pattern, float size) {}
:虽然参数名不同,但类型相同public boolean changeSize(int size, String name, float pattern) {}
:参数列表必须不同参数化多态:使用泛型?
编程
子类型多态:期望不同类型的对象可以统一处理而无需区分,遵循LSP原则
==
引用等价性
相同内存地址
对于:基本数据类型
equals()
对象等价性
对于:对象类型
在自定义ADT时,需要用@Override
重写Object.equals()
(在Object中实现的缺省equals()是在判断引用等价性)
如果用==
,是在判断两个对象身份标识 ID是否相等(指向内存里的同一段空间)
equals()
& hashCode()
equals()
的性质:自反、传递、对称、一致性
equals()
重写范例
Plane
为例 @Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Plane)) {
return false;
}
Plane plane = (Plane) o;
return Objects.equals(number, plane.number) && Objects.equals(strType, plane.strType)
&& intSeats == plane.intSeats && age == plane.age;
}
instanceof
:
- 判断类
- 仅在equals里使用
hashCode()
:
hashCode
hashCode
,但性能会变差对可变类型来说,往往倾向于实现严格的观察等价性, 但在有些时候,观察等价性可能导致bug,甚至可能破坏RI。
programming for reuse 面向复用编程:开发出可复用的软件
programming with reuse 基于复用编程:利用已有的可复用软件搭建应用系统
- 子类型多态:客户端可用统一的方式处理不同类型的对象
- 在可以使用父类的场景,都可以用子类型代替而不会有任何问题
父类型 → 子类型:
泛型类型是不支持协变的:
如
ArrayList
是List
的子类型,但List
不是List
的子类型
这是因为发生了类型擦除,运行时就不存在泛型了,所有的泛型都被替换为具体的类型。
但是在实际使用的过程中是存在能够处理不同的类型的泛型的需求的,如定义一个方法参数是List
类型的,但是要适应不同的类型的E,于是可使用通配符?
来解决这个需求:
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
}
public static void printList(List<? super A> list){
...}
public static void printList(List<? extends A> list){
...}
- “委托”发生在object层面
- “继承”发生在class层面
Types of Delegation:
class Duck {
//no field to keep Flyable object
public void fly(Flyable f) {
f.fly(); } //让这个鸭子以f的方式飞
public void quack(Quackable q) {
q.quack() }; //让鸭子以q的方式叫
}
Duck d = new Duck();
d.fly();
class Duck {
//这两种实现方式的效果是相同的
Flyable f = new FlyWithWings(); //写死在rep中
public Duck() {
f = new FlyWithWings(); } //写死在构造方法中
public void fly(){
f.fly(); }
}
Flyable f = new FlyWithWings();
Duck d = new Duck(f);
d.fly();
class Duck {
Flyable f; // 这个必须由构造方法传入参数绑定
public Duck(Flyable f) {
this.f = f; } // 在此传入
public void fly(){
f.fly(); }
}
适用场合:你已经有了一个类,但其方法与目前client的需求不一致。
根据OCP原则,不能改这个类,所以扩展一个adaptor和一个统一接口。
继承组合会引起组合爆炸/代码重复
Stack
对应上图Component
接口ArrayStack
对应ConcreteComponent
,基础类StackDecorator
对应Decorator
,装饰类(可以是抽象类)UndoStack
对应ConcreteDecoratorA
,装饰类的具体类//Stack接口,定义了所有的Stack共性的基础的功能
interface Stack {
void push(Item e);
Item pop();
}
//最基础的类,啥个性也没有的Stack,只有共性的实现
public class ArrayStack implements Stack {
... //rep
public ArrayStack() {
...}
public void push(Item e) {
...}
public Item pop() {
... }
}
//装饰器类,可以是一个抽象类,用于扩展出有各个特性方面的各个子类
public abstract class StackDecorator implements Stack {
protected final Stack stack; //用来保存delegation关系的rep
public StackDecorator(Stack stack) {
this.stack = stack; //建立稳定的delegation关系
}
public void push(Item e) {
stack.push(e); //通过delegation完成任务
}
public Item pop() {
return stack.pop(); //通过delegation完成任务
}
}
//一个有撤销特性功能的子类
public class UndoStack extends StackDecorator implements Stack {
private final UndoLog log = new UndoLog();
public UndoStack(Stack stack) {
super(stack); //调用父类的构造方法建立delegation关系
}
public void push(Item e) {
log.append(UndoLog.PUSH, e); //实现个性化的功能
super.push(e); //共性的功能通过调用父类的实现来完成
}
public void undo() {
//implement decorator behaviors on stack
}
...
}
new Class1(new Class2(new Class3(...)))
// 先创建出一个基础类对象
Stack s = new ArrayStack();
// 利用UndoStack中继承到的自己到自己的委派建立起从UndoStack到ArrayStack的delegation关系
// 这样,UndoStack也就能够实现最基础的功能,并且自身也实现了个性化的功能
Stack us = new UndoStack(s);
// 通过一层层的装饰实现各个维度的不同功能
Stack ss = new SecureStack(new SynchronizedStack(us));
JDK中装饰器模式的应用:
static List
unmodifiableList(List list)
static Set
synchronizedSet(Set set)
适用场合:有共性的算法流程,但算法各步骤有不同的实现典型的“将共性提升至超类型,将个性保留在子类型”
- 客户端希望遍历被放入容器/集合类的一组ADT对象,无需关心容器的具体类型
- 也就是说,不管对象被放进哪里,都应该提供同样的遍历方式
实现方式是在ADT类中实现Iterable
接口,该接口内部只有一个返回一个迭代器的方法,然后创建一个迭代器类实现Iterator接口,实现hasnext()
、next()
、remove()
这三个方法。
指标:
- 可维护性
- 可扩展性
- 灵活性
- 可适应性
- 可管理性
- 支持性
模块化编程:高内聚 & 低耦合
5 Rules of Modularity Design:
虚拟构造器
例子
- 一个UI,包含多个窗口控件,这些控件在不同的OS中实现不同
- 一个仓库类,要控制多个设备,这些设备的制造商各有不同,控制接口有差异
Abstract Factory vs Factory Method
Abstract Factory | Factory Method |
---|---|
创建多个类型对象 | 创建一个对象 |
多个factory方法 | 一个factory方法 |
使用组合/委派 | 使用继承/子类型 |
public class ProxyImage implements Image {
private Image realImage;
private String fileName;
public ProxyImage(String fileName){
this.fileName = fileName; // 不需要在构造的时候从文件装载
}
@Override
public void display() {
if(realImage == null) {
// 如果display的时候发现没有装载,则再委派
realImage = new RealImage(fileName); // Delegate到原来的类来成具体装载
}
realImage.display();
}
}
Image image = new ProxyImage("pic.jpg");
image.display();
image.display();
new ProxyImage("pic.jpg")
时仅仅是将文件名保存下来,没有加载真正的图片RealImage
;Image.display()
时,image
委派Rep中的realImage
进行加载,并显示;realImage.display()
显示Adaptor和Proxy区别
- Adaptor目的:消除不兼容,目的是B以客户端期望的统一的方式与A建立起联系。
- Proxy目的:隔离对复杂对象的访问,降低难度/代价,定位在“访问/使用行为
Update()
方法进行更新,即获取该ADT的状态变化Java里已经实现了该模式,提供了
Observable
抽象类(直接派生子类即可,构造“偶像”)
Java提供了Observer
接口,实现该接口,构造“粉丝”
//偶像类
public class Subject {
private List<Observer> observers = new ArrayList<Observer>();//维护一个粉丝列表
private int state;
public int getState() {
return state; }
public void attach(Observer observer){
observers.add(observer); } //粉丝关注偶像
public void setState(int state) {
this.state = state;
notifyAllObservers(); //状态有变化的时候广播给粉丝
}
private void notifyAllObservers(){
for (Observer observer : observers)
observer.update();
}
}
//粉丝类
public abstract class Observer {
//粉丝的抽象接口
protected Subject subject;
public abstract void update();
}
public class BinaryObserver extends Observer{
//粉丝的具体类
public BinaryObserver(Subject subject){
this.subject = subject; // 关注偶像
this.subject.attach(this); // 告知偶像自己关注了他
}
@Override
public void update() {
//被偶像回调,通知自己有新消息
//可能有不同的行为
System.out.println("Binary String: "+Integer.toBinaryString(subject.getState()));
}
}
//
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject(); //一个偶像
new HexaObserver(subject); //三个粉丝
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15); //偶像状态变化,虽然没有直接调用粉丝行为的代码,但确实有对粉丝的delegation
System.out.println("Second state change: 10");
subject.setState(10);
}
}
即:“我”(源ADT)允许(调用
this.accept()
)“你”(visitor
)来访问我的数据(在accept()
方法内委派visitor.visit()
)——数据源主动允许访问
使得访问方法可以变化
可以为源ADT预留功能
/* Abstract element interface (visitable) */
public interface ItemElement {
public int accept(ShoppingCartVisitor visitor); //埋下一个槽
}
/* Concrete element */
public class Book implements ItemElement{
private double price;
...
int accept(ShoppingCartVisitor visitor) {
visitor.visit(this); //把自己通过这个槽传过去
}
}
public class Fruit implements ItemElement{
private double weight;
...
int accept(ShoppingCartVisitor visitor) {
visitor.visit(this); //所有的子类都会实现这个槽
}
}
/* Abstract visitor interface */
public interface ShoppingCartVisitor {
int visit(Book book);
int visit(Fruit fruit);
}
public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
//一种实现
public int visit(Book book) {
int cost=0;
if(book.getPrice() > 50) cost = book.getPrice()-5;
else cost = book.getPrice();
System.out.println("Book ISBN::"+book.getIsbnNumber() + " cost ="+cost);
return cost;
}
public int visit(Fruit fruit) {
int cost = fruit.getPricePerKg()*fruit.getWeight();
System.out.println(fruit.getName() + " cost = "+cost);
return cost;
}
}
/* Client */
public class ShoppingCartClient {
public static void main(String[] args) {
ItemElement[] items = new ItemElement[]{
new Book(20, "1234"),
new Book(100, "5678"), new Fruit(10, 2, "Banana"), new Fruit(5, 5, "Apple")};
int total = calculatePrice(items);
System.out.println("Total Cost = " + total);
}
private static int calculatePrice(ItemElement[] items) {
ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
int sum=0;
for(ItemElement item : items)
sum = sum + item.accept(visitor);
return sum;
}
}
Visitor vs Iterator:
Strategy vs Visitor:
Caretaker
负责掌控全部的状态备份,客户端通过它来操纵ADT的状态备份与恢复3种最重要的操作:
x ::= y z
x matches y followed by zx ::= y*
x matches zero or more yx ::= y | z
x matches either y or z例:解析
URL:http://didit.csail.mit.edu:4949/
url ::= 'http://' hostname (':' port)? '/'
hostname ::= word '.' hostname | word '.' word
port ::= [0-9]+
word ::= [a-z]+
语法解析树:
- 面向健壮性的编程
- 处理未期望的行为和错误终止
- 即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
- 错误信息有助于进行debug
- Robustness principle (Postel’s Law):对自己的代码要保守,对用户的行为要开放
区别
- 正确性:永不给用户错误的结果
- 健壮性:尽可能保持软件运行而不是总是退出
- 正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)
由于程序员对Error通常无法预料无法解决,因此重点关注可被解决的Exception
Java中Exception可以被分为两个部分,蓝色的运行时异常和绿色的其他异常。
- Java’s exception handling consists of three operations:
- Declaring exceptions (
throws
)声明“本方法可能会发生XX异常”- Throwing an exception (
throw
)抛出XX异常- Catching an exception (
try, catch, finally
) 捕获并处理XX异常
Checked exception | Unchecked exception | |
---|---|---|
Basic | 必须被显式地捕获或者传递 (try-catch-finally-throw ),否则编译器无法通过,在静态类型检查时就会报错 |
异常可以不必捕获或抛出,编译器不去检查,不会给出任何错误提示 |
Class of Exception | 继承自Exception 类(上图中的绿色部分) |
继承自RuntimeException 类(上图中的蓝色部分) |
Handling | 从异常发生的现场获取详细的信息,利用异常返回的信息来明确操作失败的原因,并加以合理的恢复处理 | 简单打印异常信息,无法再继续处理 |
Appearance | 代码看起来复杂,正常逻辑代码和异常处理代码混在一起 | 清晰,简单 |
选取checked exception还是unchecked exception可遵循下面的原则:
- checked exception:如果客户端可以通过其他的方法恢复异常,而对开发者来说错误可预料但不可预防,它的出现已经脱离了程序能够掌控的范围。
- unchecked exception:如果客户端对出现的这种异常无能为力,而对开发者来说错误可预料可预防,它可以通过调整程序来避免出现。
可以选择创建自定义异常类型:
public class FooException extends Exception {
public FooException() {
super(); }
public FooException(String message) {
super(message); }
public FooException(String message, Throwable cause) {
super(message, cause); }
public FooException(Throwable cause) {
super(cause); }
}
throw
关键字抛出异常,如:throw new EOFException()
;String readData(Scanner in) throws EOFException // 声明:本函数可能发生该异常
{
. . .
while (. . .)
{
if (!in.hasNext()) // EOF encountered
{
if (n < len)
throw new EOFException(); // 异常在这里发生了
}
. . .
}
return s;
}
可以使用try-catch
语法对抛出的异常进行处理,也可以用throws
语法将异常抛给上一级调用,然后在上一级中使用try-catch
处理。
public static void fun() throws IOException {
// 已声明可能抛出的异常
...
}
public static void main(String args[]) {
try{
fun();
} catch (IOExeption e) {
// 延迟到此处捕获
e.printStackTrace();
}
}
所以,try-chtch
所捕获到的异常可能有两个来源,一是自己内部的代码产生的,二是调用了其他的方法,并且该方法未处理抛给了本方法。
- 本来catch语句下面是用来做exception handling的,但也可以在catch里抛出异常
- 这么做的目的是:更改exception的类型,更方便client端获取错误信息并处理
try {
...
}
catch (AException e) {
// 捕获到A异常
// 抛出B异常,并带上异常消息
throw new BException( " xxx error:" + e. getMessage());
}
所以形成了try-catch-finally
结构。不管程序是否碰到异常,finally
都会被执行。
目的还是为了能够让客户端能够用统一的方式处理不同类型的对象。
断言:
assertion
都会被disabled)assert condition : message
;
message
在发生错误时显示给用户,便于快速发现错误所在作用:
- 最高效、快速地找出/改正bug
- 提高可维护性
Assertion | Exception |
---|---|
提高“正确性” | 提高“健壮性” |
错误/异常处理是提高健壮性,处理外部行为;断言是提高正确性,处理内部行为 | 使用异常来处理你“预料到可以发生”的不正常情况;使用断言处理“绝不应该发生”的情况 |
内部行为 | 外部行为 |
处理“绝不应该发生”的情况 | 处理“可以预料到会发生”的情况 |
assert
使用场所:
assert x > 0
checkRep()
switch-case
的某个分支,则可以用断言直接在分支上assert false
;防御式编程–>测试–>调试
调试 Debug:
常用方法:假设-检验
Diagnose:
日志管理工具:
- JDK logging
- Apache Log4j
java.util.logging
通过设定日志级别来确定要log哪些信息,也可以通过Handler将日志存储在不同的地方。
日志级别(从高到低):SEVERE
、WARNING
、INFO
、CONFIG
、FINE
、FINER
、FINEST
设定级别:logger.setLevel(Level.INFO)
;
Logger
import java.util.logging.*;
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
//often using class name as logger name
//或者用这种方式
public class LogTest {
static String strClassName = LogTest.class.getName(); //get class name
static Logger myLogger = Logger.getLogger(strClassName);
// using class name as logger name
...
myLogger.info(“ XXXX ”);
}
Logging Handlers
StreamHandler
、ConsoleHandler
、FileHandler
、SocketHandler
、MemoryHandler
…logger.addHandler(new FileHandler(“test.txt”)
SimpleFormatter
、…fileHandler.setFormatter(new SimpleFormatter())
Static/Dynamic 测试
单元测试:
JUnit在测试方法前使用@Test
annotation来表明这是一个JUnit测试方法。如果要在测试开始之前做一些准备则在准备方法前添加@Before
annotation,如果要在测试结束后做一些收尾工作则在收尾方法前添加@After
annotation。
JUnit使用的是断言机制来完成测试,常用的有三种测试方法:assertEquals()
、assertTrue()
、assertFalse()
。
- 白盒测试:对程序内部代码结构的测试
- 黑盒测试:对程序外部表现出来的行为的测试
黑盒测试:
Equivalence Partitioning 等价类划分:
Boundary Value Analysis 边界值分析
覆盖划分的方法
例子:大整数乘法
例:Max()
Code Coverage代码覆盖度:已有的测试用例有多大程度覆盖了被测程序 。
- 代码覆盖度越低,测试越不充分;
- 代码覆盖度越高,测试代价越高。
测试覆盖种类:
测试效果:路径覆盖 > 分支覆盖 > 语句覆盖
测试难度:路径覆盖 > 分支覆盖 > 语句覆盖
100%语句覆盖是common(正常)目标
100%分支覆盖是desirable(令人满意的),arduous(很难实现),有些行业有更高标准
100%路径覆盖是infeasible(不可实行的)
并发:
Two Models for Concurrent Programming:
Process 进程
Threads 线程
Java中创建线程的两种方式
注意:要使用.start()
方法,而不是.run()
从Thread类派生出子类:
//从Thread类派生子类
public class HelloThread extends Thread {
public void run() {
//待做的事都放在这个方法中,所以这个方法必须实现
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
//两种启动线程的方式
HelloThread p = new HelloThread();
p.start(); //注意是使用start()方法来启动的
//或者可以直接这样
(new HelloThread()).start();
}
}
从Runnable接口构造Thread对象:
//实现Runnable接口
public class HelloRunnable implements Runnable {
public void run() {
//与方法一相同
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start(); //启动方式略有不同
}
}
//另一种实现方式是实现匿名类来实现Runnable接口
new Thread(new Runnable() {
public void run() {
System.out.println("Hello");
}
}).start();
Time slicing 时间分片
共享内存和消息传递都会带来交错和竞争
并行程序难以测试和调试:
利用某些方法调用来主动影响线程之间的交错关系
Thread.sleep(time)
:线程的休眠Thread.interrupt()
:向线程发出中断信号Thread.yield()
:使用该方法,线程告知调度器放弃CPU的占用权,从而可能引起调度器唤醒其他线程。尽量避免使用该方法。Thread.join()
:让当前线程保持执行,直到其执行结束。t.isInterrupted()检查t是否已在中断状态中
当某个线程被中断后,一般来说应停止其run()中的执行,取决于程序员在run()中处理
final
:变量只读,不可写
- 一般来说,JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。原因:threadsafe的类一般性能上受影响
synchronizedXXX
private static Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());
synchronizedMap(hashMap)
之后,不要再把参数hashMap
共享给其他线程,不要保留别名,一定要彻底销毁iterator
也是不安全的Principle:线程安全不应依赖于偶然
同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行。
Locks:
使用方法:
Object lock = new Object();
作为锁来使用,而拥有lock的线程可独占式的执行该部分代码。Object lock = new Object();
synchronized (lock) {
// 线程阻塞在这里,直到锁被释放
// 现在这个线程获得了这把锁
do1(); //这个块中的所有语句都不能被打断了
// 退出块的同时释放锁
}
// 此时在另一个线程里有如下代码
synchronized (lock) {
//要等待lock被释放才能开始执行
do2(); //与上面线程里的代码操作的是同一个数据
}
作用:
- 要互斥,必须使用同一个lock进行保护
- 对
synchronized
的方法,多个线程执行它时不允许interleave,也就是说“按原子的串行方式执行”
ADT加锁:
synchronized(this)
Monitor模式:
synchronized
相当于synchronized(this)
// 将synchronized关键字加在了方法声明里,效果与上面的写法相同
public synchronized void xxx(...){
...
}
在任何地方synchronized?
- No!
public static synchronized boolean findReplace(EditBuffer buf, ...)
static
方法意味着在class层面上锁,对性能带来极大损耗。所有关于threadsafe的设计决策也都要在ADT中记录下来
Locking discipline
synchronized(this)
所保护方法加
synchronized
关键字:将多个atomic的操作组合为更大的atomic操作
死锁:多个线程竞争lock,相互等待对方释放lock。
// T1:
synchronized(a){
//T1线程拿到了a锁
synchronized(b){
//T1线程等待T2线程释放b锁
...
}
}
// T2:
synchronized(b){
//T2线程拿到了b锁
synchronized(a){
//T2线程等待T1线程事放a锁
...
}
}
解决办法:
这个办法不是很常用,因为不是所有的对象都可以排序,而如果只是为了增加锁的功能而实现Comparable就太不划算了。
这两个办法的思想都是要让所有的线程在第一次申请锁的时候申请同一把锁,因此当一个线程先拿到一把锁的时候其他线程都被挂起了,所以这个线程就能顺利拿到后面所有的锁,因而避免了死锁。