文章目录
1、不可变对象
A.优点:
线程安全,不需要同步操作,更安全;
设计、实现、使用方式更简单,不易出错。
B.特性:
不提供可改变状态的方法;
类不可被继承——为final类;
所有的成员都是final(不可修改的);
所有的成员都是private(私有不对外开放);
保证成员对象引用不指向任何可变对象存储路径(保证非静态成员的不可变性,如下)。
C.如何保证其非静态对象成员的不可变性:
方法一:对象成员定义为不可变类;
方法二:对于为可变类的对象成员,使用参数的深度拷贝初始化该对象成员。并且当需要进行参数有效性检查时,需要保证先完成参数对象的深度拷贝,再做有效性检查( time-of-check/time-of-use or TOCTOU attack)。为了避免多线程中某个线程的检查参数过程与参数赋值过程的漏洞窗口期间,另一个线程对该线程的参数进行修改造成了的问题。
class Period {
private final Date start;//Date类的对象为可变对象。Date类提供了对外方法修改本身。可按照方法一,使用Instance和java.time中class,因为他们不提供对外方法修改对象,其实力为不可变对象
private final Date end;
Period(Date start, Date end) {
// if(start.compareTo(end)>0){
// throw new IllegalArgumentException("exceed");
// }
this.start = new Date(start.getTime());//对象的深度拷贝。不是简单地对象地址的拷贝,而是生成并指向对新生成的对象。因此,不会受恶意形参的值在完成实例初始化后的改变,而改变实例的值。
this.end = new Date(end.getTime());//反序列化readObject()也应该做相同的深度拷贝,当然不可为final 因为readDefaultObject()会首先进行初始化。因为待反序列化二进制字符串的格式的规范,第三方客户可以获取二进制代表的对象的内部成员,包括私有成员。所以,当服务端反序列化得到的对象若不进行深度拷贝,则其成员可以被其攻击者修改值。这样可以被任意修改的特点,会产生问题。
if(this.start.compareTo(this.end)>0){//检查在拷贝之后,保证新生成的对象符合业务逻辑
throw new IllegalArgumentException("exceed");
}
}
}
//反序列化的恶意修改问题
public class MutablePeriod {
// A period instance
public final Period period;
// period's start field, to which we shouldn't have access
public final Date start;
// period's end field, to which we shouldn't have access
public final Date end;
public MutablePeriod() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
// Serialize a valid Period instance
out.writeObject(new Period(new Date(), new Date()));
/*
* Append rogue "previous object refs" for internal
* Date fields in Period. For details, see "Java
* Object Serialization Specification," Section 6.4.
*/
byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
bos.write(ref); // The start field
ref[4] = 4; // Ref # 4
bos.write(ref); // The end field
// Deserialize Period and "stolen" Date references
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();
end = (Date) in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new AssertionError(e);
}
}
}
//To see the attack in action, run the following program:
public static void main(String[] args) {
MutablePeriod mp = new MutablePeriod();
Period p = mp.period;
Date pEnd = mp.end;
// Let's turn back the clock
pEnd.setYear(78);
System.out.println(p);
// Bring back the 60s!
pEnd.setYear(69);
System.out.println(p);
}
D.创建不可变对象的方式:
通过静态工厂创建:优于构造器。因为不可变对象线程安全,所以利用缓存将其共享而不需要进行进行防御拷贝(不可变类的对象拷贝结果与被拷贝对象一致,所以拷贝对于不可变对象来说是没有意义的操作,因此也不应该提供克隆方法)。因为静态工厂可以使用缓存,避免减少内存占用和垃圾回收。
2、使用访问器访问public修饰的类
对于是public 修饰的类,其可变成员对象应设置为private并对外通过提供访问器(accessor)作为访问方式。因为这样能使可变对象的使用更加灵活,例如可以在不改变api的方式改变对象呈现的方式、强制可变对象为不可变、提供复杂的处理动作。这些都是直接访问成员对象没有的优点。
当然,对于访问范围为包、私有的类,那么他们的可变成员访问只能在包范围内,客户端用户无法访问,那么客户端无法对类成员进行修改,所以无所谓对象的访问方式是否通过accessor.
3、可创建实例的方法都不应该直接或间接的调用可重写方法
A.具体是指哪些方法
任何可以创建实例的方法:构造函数、实现了Clonable接口的clone()、实现了Serializable接口的readObject()都不应该直接或者间接调用可以被重写的方法。
调用私有方法(private)、静态方法(static)、最终方法(final)都是安全可行的,因为他们都不可被重写。
B.原因
因为当父类构造函数中 调用了被子类重写的方法时,若实例化该子类,那么,父类调用该方法时 实际被调用方法为子类中被重写的方法,而由于子类的初始化在父类之后,也就是说,父类需要调用还未完成初始化子类实例的方法。这样的调用可能会有空指针错误。
C.注意
父类中的方法调用,被调用方法被子类重写。注意该方法是否是有意留给调用子类的实现。
因此,所有可重写方法的调用都应该引起注意。
SuperClass A = new SubClass();
//若存在SuperClass定义了:private MethodOne,调用了public MethodTwo
//且SubClass 重写了public MethodTwo
//此时执行以下代码会引起混乱
A.methodOne();
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
// Blank final, set by constructor
private final Instant instant;
Sub() {
instant = Instant.now();
}
// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(instant);//父类实际调用了这里的实现。不报错的原因是,println允许参数为null
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
4、递归三要素
编写递归时,最重要的有以下三点:
递归一定有一种可以退出程序的情况——方法的第一条语句总是包含一个return的条件语句
递归调用总是尝试解决一个规模更小的子问题,这样递归才能收敛到最简单的情况。
递归调用的父问题和尝试解决的子问题之间不应该有交集。
5、死锁产生的四个必要条件条件——破坏任意条件来避免死锁
6、分布式系统注意点
6.1.削峰保护
- 技术手段
- 首选:消息队列、答题、分层过滤
- 不得已的措施:限流、机器负载保护
- 业务手段
消息队列 —— 缓存请求
- 用户请求的异步处理,避免服务器处理不过来请求而造成内存溢出
将请求放到缓存中,服务器从缓存中主动获取请求进行处理(服务器只有空闲时才会主动去获取)。
答题 —— 减少请求
- 过滤排除机器请求(攻击)
- 把题目生成为图片格式,并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题,它要求只有人才能理解题目本身的含义
- 拉长峰值持续时间但大大降低了峰值的大小
每答一个题都需要花几秒的时间,造成请求按时间分片。大大降低峰值压力。 - 答题的验证逻辑
- 验证问题的答案;
- 用户本身身份的验证,例如是否已经登录、用户的Cookie是否完整、用户是否重复频繁提交等。
- 提交答案的时间
例如从开始答题到接受答案要超过1s,因为小于1s是人为操作的可能性很小,这样也能防止机器答题的情况
分层过滤
- 适用于读系统
- 尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前。利用分层校验,在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求
- 分层校验的基本原则是:
- 将动态请求的读数据缓存(Cache)在Web端,过滤掉无效的数据读;
- 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
- 对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
- 对写请求做限流保护,将超出系统承载能力的请求过滤掉;
- 对写数据进行强一致性校验,只保留最后有效的数据。
假如请求分别经过CDN、前台读系统(如商品详情系统)、后台系统(如交易系统)和数据库这几层,那么:
- 大部分数据和流量在用户浏览器或者CDN上获取,这一层可以拦截大部分数据的读取;
- 经过第二层(即前台系统)时数据(包括强一致性的数据)尽量得走Cache,过滤一些无效的请求;
- 再到第三层后台系统,主要做数据的二次检验,对系统做好保护和限流,这样数据量和请求就进一步减少;
- 最后在数据层完成数据的强一致性校验。
这样就像漏斗一样,尽量把数据量和请求量一层一层地过滤和减少了。
6.2、适用场景
其中,队列缓冲方式更加通用,它适用于内部上下游系统之间调用请求不平缓的场景,由于内部系统的服务质量要求不能随意丢弃请求,所以使用消息队列能起到很好的削峰和缓冲作用。
而答题更适用于秒杀或者营销活动等应用场景,在请求发起端就控制发起请求的速度,因为越到后面无效请求也会越多,所以配合后面介绍的分层拦截的方式,可以更进一步减少无效请求对系统资源的消耗。
分层过滤非常适合交易性的写请求,比如减库存或者拼车这种场景,在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的,所以读的数据是否一定要非常准确呢?
其实不一定,你可以放一些请求过去,然后在真正减的时候再做强一致性保证,这样既过滤一些请求又解决了强一致性读的瓶颈。
6.3、利用业务手段达到“削峰”
例如在零点开启大促的时候由于流量太大导致支付系统阻塞,这个时候可以采用发放优惠券、发起抽奖活动等方式,将一部分流量分散到其他地方,这样也能起到缓冲流量的作用。
7、request 与 session、cookie、tocken的关系
request 用户请求的业务包装体,用于接收请求参数。
session 保存在服务器上;可以通过 request.getSession() 获取session对象。
cookie 保存在浏览器上,默认值就是服务器端session的id值;位于请求的HEADER中。可以通过 request.getHeader(“cookie”) 获取cookie值(类型为String)。
tocken 保存在浏览器上,和cookie类似,可以保存在HEADER中,也可以作为请求参数。一般存有编码后的userID和用于编码的秘钥。
一般登陆校验有两种方法:
1、校验 sessionId = cookie值 的session是否存在
session不存在就是用户没有登陆或者已经退出登陆状态。
因为采用校验session的方法时,要求
- 用户登陆后
- 服务端持久化session到缓存或数据库中;
- 浏览器端保存cookie信息(保存被持久化的session的id到cookie中)。
- 用户退出登陆
- 服务端从缓存或数据库中删除session;
- 浏览器端删除cookie信息。
缺点:跨域请求的不会携带cookie信息
2、校验 tocken
获取 tocken 的 编码过的userID 和 秘钥,服务端利用编码规则对编码过的userID 进行解码,若解码后得到的签名正确,则说明用户已经登录。当需要获取登陆用户的信息时,则需要通过userID去缓存或者数据库中获取。
登录时:
服务端签发tocken 给前端,之后的每个请求只需要携带这个tocken值就可以了。
tocken 的编码规则:
tocken 的反编码——如何获取登陆用户的id
tocken的好处是:不需要服务端存储和维护登陆用户的信息(分布式系统中维护比较复杂)。
以服务端多进行几步操作为代价 避免维护用户信息。
8、 常见内部 ip 地址
网段/CIDR | 范围 | 解释 |
---|---|---|
10/8 | 10.0.0.0 ~ 10.255.255.255 | A类地址的私有网络ip地址(Private-Use Networks) |
172.16/12 | 172.16.0.0 ~ 172.31.255.255 | B类地址的私有网络ip地址(Private-Use Networks) |
192.168/16 | 192.168.0.0 ~ 192.168.255.255 | C类地址的私有网络ip地址(Private-Use Networks) |
169.254/16 | 169.254.0.0 ~ 169.254.255.255 | 常见于Windows系统的主机,主机自动分配的ip地址。这是“链接本地”块。它被分配用于单个链路上的主机之间的通信。 主机通过自动配置获取这些地址,例如可能找不到DHCP服务器 1。(Link Local) |
127/8 | 127.0.0.1 ~ 127.255.255.255 | 代表本地换回地址。都表示本机,可以使用不同的ip模拟一台虚拟机,就像localhost与127.0.0.1之间的区别一样。(LOOPBACK) |
9、集合类型的操作集合
集合类型不一定支持所有的操作。如java.util.Arrays.ArrayList
不支持removeAll
、add(index, elem)
的操作。
所以若方法内有复杂的集合对象操作,可以先将集合类型入参进行转换如
List<String> fileListOfValidList = new ArrayList<>(fileList); // 转换类型,避免乱七八糟的类型(如Arrays.ArrayList)不支持部分集合操作,如removeAll(),add(index, elem)
10、String.format NPE问题
MessageFormat.format:参数为空时不会NPE
如下列代码,第一行 ParamException.isTrue执行时,就会执行String.format代码,所以若name
为空,则会NPE报错。
所以,若name
可能为空,则应该换成MessageFormat。
当然String.format 写起来好像更方便。
ParamException.isTrue(index>1, String.format("姓名【%s】index过大", name)); // 可能NPE
// ParamException.isTrue(index>1, MessageFormat.format("姓名【{0}】index过大", name)); // 不会NPE
final List<String> tempList = listFile(parentDirName);
List<String> fileList = new ArrayList<>();