Fork me on GitHub

Java

ParameterizedType 意为参数化类型(泛型)

ParameterizedType是Type的子接口,可以通过ParameterizedType获取泛型参数Class类型

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* ParameterizedType 表示参数化类型,例如 Collection<String>。
* 参数化类型在反射方法第一次需要时创建,如本包中所指定。 创建参数化类型 p 时,解析 p 实例化的泛型类型声明,并递归创建 p 的所有类型参数。 有关类型变量创建过程的详细信息,请参阅TypeVariable 。 重复创建参数化类型无效。
* 实现此接口的类的实例必须实现一个 equals() 方法,该方法等同于共享相同泛型类型声明并具有相同类型参数的任何两个实例。
*
* @since:1.5
*/
public interface ParameterizedType extends Type {

/**
* 返回一个Type对象数组,表示该类型的实际类型参数。
* 请注意,在某些情况下,返回的数组为空。 如果此类型表示嵌套在参数化类型中的非参数化类型,则会发生这种情况。
*
* @return: 表示此类型的实际类型参数的Type对象数组
* @throws TypeNotPresentException 如果任何实际类型参数引用不存在的类型声明
* @throws MalformedParameterizedTypeException 如果任何实际类型参数引用了由于任何原因无法实例化的参数化类型
* @since 1.5
*/
Type[] getActualTypeArguments();

/**
* 返回表示声明此类型的类或接口的Type对象。
*
* @return 表示声明此类型的类或接口的Type对象
* @since 1.5
*/
Type getRawType();

/**
* 返回一个Type对象,表示该类型所属的类型。 例如,如果此类型为O<T>.I<S> ,则返回O<T> 。
* 如果此类型是顶级类型,则返回null 。
*
* @return 一个Type对象,表示该类型所属的类型。 如果此类型是顶级类型,则返回null。
* @throws 如果所有者类型引用不存在的类型声明
* @throws 如果所有者类型引用了由于任何原因无法实例化的参数化类型
* @since 1.5
*/
Type getOwnerType();
}
什么是BASE柔性事务?

BASE是基于可用、柔性状态和最终一致性这三个要素

  • 基本可用(Basically Available)保证分布式事务参与方不一定要同时在线;
  • 柔性状态(Soft state)则允许系统状态更新有一定的延迟,这个延时对客户来说不一定能够察觉到;
  • 最终一致性(Eventually consistent)通常是通过消息传递的方式保证系统的最终一致性;
TCC 分布式事务

你原本的一个接口,要改造为 3 个逻辑,Try(尝试)-Confirm(确定)-Cancel(取消)

  • 先是服务调用链路依次执行 Try 逻辑。
  • 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。
  • 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。

这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时:

  • 某个服务的数据库宕机了。
  • 某个服务自己挂了。
  • 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。
  • 某些资源不足了,比如说库存不够这些。

TCC 分布式事务框架(国内):ByteTCC,TCC-transaction,Himly。

最终一致性分布式事务如何保障实际生产中 99.99% 高可用?
SOA、分布式、微服务之间的关系和区别

1.分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的
2.SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
3.微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构

ReentrantLock中tryLock()和lock方法的区别

1.tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false

2.lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

ReentrantLock中的公平锁和非公平锁的底层实现

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

类加载分几步?

按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括7个阶段

加载(Loading)-> 链接(Linking) [ 验证(Verification)->准备(Preparation)->解析(Resolution) ] -> 初始化(Initialization)->使用(Using)->卸载(Unloading)

其中

1.第一过程的加载(Loading)也称为装载

2.验证,准备,解析3个部分统称为链接(Linking)

类的初始化

使用static + final 修饰的成员变量,称为:全局变量

什么时候在链接阶段的准备环节,给此全局常量赋的值是字面量或常量。不涉及到方法或构造器的调用,除此之外,都是在初始化环节赋值的

为什么要自定义类加载器

隔离加载类

修改类加载的方式

扩展加载源

防止源码泄漏

虚拟机栈大小

如何设置栈内存大小? -Xss size(-XX : ThreadStackSize)

栈的大小直接决定了函数调用的最大可达深度

一般默认为512k-1024k,取决于操作系统

jdk 5.0之前,默认栈大小:256k

jdk5.0之后,默认栈大小:1024k (Linux/mac/windows)

方法和栈帧之间存在怎么的关系

1.在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)

2.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

栈帧内部结构

局部变量表

操作数栈

动态链接

方法返回地址

如何设置堆空间大小

-Xms 用于表示起始堆大小,等价于-XX:InitialHeapSize

-Xmx 用于表示堆区的最大内存,等价于-XX:MaxHeapSize

超出堆区的内存最大时,将会抛出OutOfMemoryError:heap异常

通常会将 -Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能

Java中有哪些类加载器

JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
BootstrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA HOME%lib下的jar包和class文件
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA HOME%/lib/ext文件夹下的jar包和class类。
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。

JVM有哪些垃圾回收算法

1.标记清除算法:
a.标记阶段:把垃圾内存标记出来
b.清除阶段:直接将垃圾内存回收
c.这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。
2.复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
3.标记压缩算法:为了解:决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。

JVM中哪些是线程共享区

堆区和方法区是所有线程共享的,栈,本地方法栈,程序计数器是每个线程独有的

Java语言中,GC Roots 包括那些元素

1.虚拟机栈中引用的对象

2.类静态属性引用的对象

3.方法区中常量引用的对象

4.所有被同步锁synchronization持有的对象

5.Java虚拟机内部的引用

6.反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等

小技巧

由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root

Java中内存泄漏的8种情况

1.静态集合类

如HashMap,LinkedList等,如果这些容器为静态的,那么它们的生命周期与JVM程序一致。长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收

2.单例模式

原因和静态集合类似。如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏**

3.内部类持有外部类

4.各种连接,如数据库连接,网络连接和IO连接等

5.变量不合理的作用域

6.改变哈希值

7.缓存泄漏

8.监听器和回调

STW

STW(Stop-The-World),指在GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

GC评估指标

吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间

内存占用:Java堆区所占的内存的大小

垃圾收集开销:相对于应用程序的执行,收集操作发生的频率

收集频率:相对于应用程序的执行,收集操作发生的频率

快速:一个对象从诞生到被回收所经历的时间

现在JVM调优标准:在最大吞吐量优先的情况下,降低停顿时间

OOM示例

堆溢出

元空间溢出

GC overhead limit exceeded(死循环)

线程溢出(在windows测试会死机,建议在虚拟机尝试)

栈上分配

发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配(JDK1.6后默认使用逃逸分析,提高性能)

JAVA中的逃逸分析,其实优化的点就在于对栈上分配的对象进行标量替换

如何优雅关闭Spring Boot?

四种优雅关闭Spring Boot应用程序的方案。

第一种是通过调用activator的shutdown接口,但需要配置认证和权限控制。

需添加依赖

shutdown接口默认关闭,需开启

第二种是调用应用程序上下文的close方法,也需要处理认证和权限问题。

第三种是调用Spring Application的方法来触发关闭钩子函数。

第四种是直接杀进程,可以在启动时将进程ID写入固定文本文件,然后执行脚本自动关闭应用程序。

Mybatis的优缺点

优点:
1.基于SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML 标
签,支持编写动态 SQL语句,并可重用。
2.与JDBC 相比,减少了 50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接:
3.很好的与各种数据库兼容(因为 MyBatis 使用JDBC 来连接数据库,所以只要JDBC 支持的数据库 MyBatis 都支持)。
4.能够与 Spring 很好的集成。
5.提供映射标签,支持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, 支持对象关系组件维护。

缺点:
1.SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
2.SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

Redis和MySQL如何保证数据一致

1.先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不一致
2.先删除Redis缓存数据,再更新Msl,再次查询的时候在将数据添加到缓存中,这种方案能解决1方案的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了Redis缓存数据,正在更新Mysgl,此时另外一个查询再查询,那么就会把Mvsal中老数据又查到Redis中
3.延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,有其他线程读了Mysl,把老数据读到了Redis中,那么也会被删除掉,从而把数据保持一致

#{}和${}的区别是什么?

#{}是预编译处理、是占位符,${}是字符串替换、是拼接符
Mybatis在处理**#{}时,会将sql中的#{}替换为?号,调用 PreparedStatement 来赋值
Mybatis在处理
${}时,会将sql中的${}**替换成变量的值,调用 Statement 来赋值
使用#{}可以有效的防止 SQL注入,提高系统安全性

Spring AOP常见的失效场景

1.当前类没有被 Spring 容器所管理。Spring 的 AOP 是在 Bean 创建的初始化后阶段进行的,如果当前类没有被 Spring 容器所管
理,那么它的 Spring AOP 功能肯定会失效。

2.同一个类中方法的调用

3.内部类方法的调用。(该方式会直接调用内部类实例对象的方法,同样没有使用代理对象,所以 AOP 会失效)

4.私有方法。(私有方法,代理对象是无法调用的,所以 AOP 会失效)

5.static 修饰的方法。(因为 static 修饰的方法属于类对象,而不属于对象实例,所以无法被代理对象调用。)

6.final 修饰的方法。(因为被 final 修饰的方法是无法被重写的,所以代理对象也是无法调用的。)

Java面向对象的三大特征以及理解

1.封装

Java中的封装是指一个类把自己内部的实现细节进行隐藏,只暴露对外的接口(setter和getter方法)。封装又分为属性的封装和方法的封装。把属性定义为私有的,它们通过setter和getter方法来对属性的值进行设定和获取。

2.继承

Java中的继承是指在一个现有类(父类)的基础上在构建一个新类(子类),子类可以拥有父类的成员变量以及成员方法(但是不一定能访问或调用,例如父类中private私有的成员变量以及方法不能访问和调用)。继承的作用就是能提高代码的复用性。子类拥有父类中的一切(拥有不一定能使用),它可以访问和使用父类中的非私有成员变量,以及重写父类中的非私有成员方法。

3.多态

多态就是指多种状态,就是说当一个操作在不同的对象时,会产生不同的结果。

在Java中,实现多态的方式有两种,一种是编译时的多态,另外一种是运行时多态,编译时的多态是通过方法的重载实现的,而运行时多态是通过方法的重写实现的。

方法的重载是指在同一个类中,有多个方法名相同的方法,但是这些方法有着不同的参数列表,在编译期我们就可以确定到底调用哪个方法。

方法的重写,子类重写父类中的方法(包括接口的实现),父类的引用不仅可以指向父类的对象,而且还可以指向子类的对象。当父类的引用指向子类的引用时,只有在运行时才能确定调用哪个方法。

其实在运行时的多态的实现,需要满足三个条件:1.继承(包括接口的实现)2.方法的重写 3.父类的引用指向子类对象

String 为什么不可变?不可变有什么好处?

为什么不可变?

1)value使用final修饰

2)没有暴露成员变量

3)内部方法不会改动 value

一旦初始化之后,String 类中的方法就不会去改动 value 中的元素,需要的话都是直接新建一个 String 对象。

4)类使用final修饰,不可继承

这个设计主要是避免有人定义一个子类继承 String,然后重写 String 的方法,将这个子类设计成可变对象。我们知道在 java 中,有父类引用指向子类对象这种用法,这种情况下,我们需要一个String 对象,可能返回的是String 子类的对象,这会导致 String 看起来是可变的。所以 java 直接将 String定义成不可继承,避免出现这种情况。

不只是 String 类,其实所有的不可变类大致的设计思想都是按这四步来。后续如果我们自己想要设计一个不可变类,也可以按这四点来设计。

不可变的好处?为什么这么设计?

1)安全性

String 是 Java 中最基础也是最长使用的类,经常用于存储一些敏感信息,例如用户名、密码、网络连接等。因此,String 类的安全性对于整个应用程序至关重要。

2)节省空间——字符串常量池

通过使用常量池,内容相同的字符串可以使用同一个对象,从而节省内存空间。如果 String 是可变的,试想一下,当字符串常量池中的某个字符串对象被很多地方引用时,此时修改了这个对象,则所有引用的地方都会改变,这可能会导致预期之外的情况。

典型的使用字符串常量池的场景:json 工具类,fastjson、jackson 等。

3)线程安全
String 对象是不可修改的,如果线程尝试修改 String 对象,会创建新的 String,所以不存在并发修改同一个对象的问题。

4)性能
String 被广泛应用于 HashMap、HashSet 等哈希类中,当对这些哈希类进行操作时,例如 HashMap 的 get/put,hashCode 会被频繁调用。

由于不可变性,String 的 hashCode 只需要计算1次后就可以缓存起来,因此在哈希类中使用 String 对象可以提升性能。

IO流

1.什么是IO流

Java对数据的操作是通过流的方式,IO是java中实现输入输出的基础,它可以很方便的完成数据的输入输出操作,Java把不同的输入输出抽象为流,通过流的方式允许Java程序使用相同的方式来访问不同的输入、输出。

IO又分为流IO(java.io)和块IO(java.nio),Java.io是大多数面向数据流的输入/输出类的主要软件包。此外,Java也对块传输提供支持,在核心库 java.nio中采用的便是块IO。流IO的好处是简单易用,缺点是效率较低。块IO效率很高,但编程比较复杂。

2. IO流原理

IO流是基于流的概念,它将数据的输入和输出看作是一个连续的流。数据从一个地方流向另一个地方,流的方向可以是输入(读取数据)或输出(写入数据)。Java中的IO流分为字节流和字符流两种类型,分别用于处理字节数据和字符数据。

IO流的原理是通过流的管道将数据从源头传输到目标地。源头可以是文件、网络连接、内存等,而目标地可以是文件、数据库、网络等。IO流提供了一组丰富的类和方法来实现不同类型的输入和输出操作。

3.IO流分类
Java中的IO流可以按照数据的类型和流的方向进行分类。

1.按数据类型分类
字节流(Byte Stream):以字节为单位读写数据,适用于处理二进制数据,如图像、音频、视频等。常见的字节流类有InputStream和OutputStream。

字符流(Character Stream):以字符为单位读写数据,适用于处理文本数据。字符流会自动进行字符编码和解码,可以处理多国语言字符。常见的字符流类有Reader和Writer。

2 按流的方向分类
输入流(Input Stream):用于读取数据。输入流从数据源读取数据,如文件、网络连接等。常见的输入流类有FileInputStream、ByteArrayInputStream、SocketInputStream等。

输出流(Output Stream):用于写入数据。输出流将数据写入到目标地,如文件、数据库、网络等。常见的输出流类有FileOutputStream、ByteArrayOutputStream、SocketOutputStream等。

4.IO流的使用场景
IO流主要用于处理输入和输出操作,适用于以下场景:

读写文件:IO流可以方便地读取和写入文件中的数据,从而实现文件的读写操作,例如读取配置文件、处理日志文件、读取用户上传的文件等。

网络通信:IO流可以用于处理网络通信中的数据输入和输出,例如通过Socket进行网络通信时,可以使用IO流来传输数据。

数据库操作:IO流可以将数据从程序中传输到数据库中,或者从数据库中读取数据到程序中,从而实现数据库的读写操作。

内存操作:IO流也可以用于处理内存中的数据输入和输出,例如通过ByteArrayInputStream和ByteArrayOutputStream可以在内存中读写数据。

用户交互:IO流可以用于处理用户输入和输出,例如从控制台读取用户输入的数据,或者向控制台输出提示信息和结果。

ArrayList线程不安全的几种表现,怎么解决?

一、线程不安全的三种表现

1.空指针异常

2.数组越界异常

3.并发修改异常

二、解决方法

1.将ArrayList替换成Vector

1
Vector<Integer> arrayList = new Vector<>();

2.Collections.synchronizedList()

1
List<Integer> arrayList = Collections.synchronizedList(new ArrayList<>());

3.使用CopyOnWriteArrayList

1
List<Integer> arrayList1 = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList是一个线程安全的ArrayList,其实现原理是读写分离,其对写操作使用ReentrantLock来上锁,对读操作则不加锁;CopyOnWriteArrayList在写操作的时候,会将list中的数组拷贝一份副本,然后对其副本进行操作(如果此时其他线程需要读的事,那么其他线程读取的是原先的没有修改的数组,如果其他写操作的线程要进行写操作,需要等待正在写的线程操作完成,释放ReentrantLock后,去获取锁才能进行写操作),写操作完成后,会讲list中数组的地址引用指向修改后的新数组地址。

总结
1、本文介绍了ArrayList在多线程的情况下可能会出现的三种异常,并分析了原因,结尾给出了三种解决ArrayList线程不安全的方案,一和二两种方法都是将所有的方法都加锁,那会导致效率低下,只能一个线程操作完,下一个线程获取到锁才能操作。

2、CopyOnWriteArrayList由于写时进行复制,内存里面同时存在两个对象占用内存,如果对象过大容易发送YongGc和FullGc,如果使用场景的写操作十分频繁的话,建议还是不要实现CopyOnWriteArrayList。

SpringMVC是如何处理一个请求
Spring中的Bean的生命周期有哪些步骤
Spring的Aop的完整实现流程

以 JavaConfig为主

当@EnableAspectJAutoProxy 会通过@Import注册一个BeanPostProcessor处理AOP

1.解祈切面:在Bean创建之前的第一个Bean后置处理器会去解析切面(解析切面中通知、切点,一个通知就会解析成一个advisor(通知、切点))

2.创建动态代理:正常的Bean初始化后调用BeanPostProcessor 拿到之前缓存的advisor,再通过advisor中poitcut判断当前Bean是合微功点表达式业配,如果匹配,就会为Bean创建动态代理(创建方式1.jdk动态代理2.cglib)。

3.调用:拿到动态代理对象,调用方法就会判断当前方法是否增强的方法,就会通过调用链的方式依次去执行通知.

springIOC容器的加载过程

从概念态到定义态的过程(1-6)

1、实例化一个ApplicationContext的对象

2、调用bean工厂后置处理器完成扫描

3、循环解析扫描出来的类信息;(就是有写@component 注解)

4、实例化一个BeanDefinition对象来存储解析出来的信息 (存入一个Map)

5、把实例化好的beanDefinition对象put到beanDefinitionMap当中缓存起来,以便后面实例化bean;

6、再次调用其他bean工厂后置处理器;

从定义态到纯净态(7-9)

7、当然spring还会干很多事情,比如国际化,比如注册BeanPostProcessor等等,如果我们只关心如何实例化一个bean的话那么这一步是spring调用finishBeanFactoryInitialization方法来实例化单例的bean,实例化之前spring要做验证,需要遍历所有扫描出来的类,依次判断这个bean是否Lazy,是否prototype,是否abstract等等;

8、如果验证完成spring在实例化一个bean之前需要推断构造方法,因为spring实例化对象是通过构造方法反射,故而需要知道用哪个构造方法;

9、推断完构造方法之后spring调用构造方法反射实例化一个对象;注意我这里说的是对象、对象、对象;这个时候对象已经实例化出来了,但是并不是一个完整的bean,最简单的体现是这个时候实例化出来的对象属性是没有注入,所以不是一个完整的bean

从纯净态到成熟态

10、spring处理合并后的beanDefinition

11、判断是否需要完成属性注入

12、如果需要完成属性注入,则开始注入属性

初始化

[

​ 13、判断bean的类型回调Aware接口

​ 14、调用生命周期回调方法 (13,14如果需要AOP就创建AOP动态代理)

​ 15、如果需要代理则完成代理

]

创建完成

16、put到单例池——bean完成——存在spring容器当中

Spring IOC的扩展点

1.执行BeanFactoryPostProcessor的postProcessBeanFactory方法(作用:在注册BeanDefinition的可以对beanFactory进行扩展,调用时机:IOC加载时注册BeanDefinition的时候会调用) 后

2.执行BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry方法(作用:动态注册BeanDefinition,调用时机:IOC加载时注册BeanDefinition的时候会调用) 先

3.加载BeanPostProcessor实现类:在Bean的生命周期会调用9次Bean的后置处理器

Spring IOC的实现机制

简单来说就是:简单工厂+反射

什么是工厂模式:很简单,就是调用一个方法(工厂方法)根据传入的参数返回一个对象。

IOC的优点
1、集中管理对象,方便维护

2、减低耦合度

3、IOC容器支持懒汉式和饿汉式的方式加载, 默认单例

springboot项目哪里用到了 AOP?怎么用的?

AOP(Aspect-Oriented Programming:面向切面编程) 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,提高系统可拓展性和可维护性。

一般项目主要有下面这些地方用到了 AOP

1.基于 AOP 实现统一的日志管理。
2.基于 Redisson + AOP 实现了接口防刷,一个注解即可限制接口指定时间内单个用户可以请求的次数。
3.基于 Spring Security 提供的 @PreAuthorize 实现权限控制,其底层也是基于 AOP。

日志记录
利用 AOP 方式记录日志,只需要在 Controller 的方法上使用自定义 @Log 日志注解,就可以将用户操作记录到数据库。

1
2
3
4
5
6
@Log(description = "新增用户")
@PostMapping(value = "/users")
public ResponseEntity create(@Validated @RequestBody User resources){
checkLevel(resources);
return new ResponseEntity(userService.create(resources),HttpStatus.CREATED);
}

AOP 切面类 LogAspect用来拦截带有 @Log 注解的方法并处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class LogAspect {

private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

// 定义切点,拦截带有 @Log 注解的方法
@Pointcut("@annotation(com.example.annotation.Log)") // 这里需要根据你的实际包名修改
public void logPointcut() {
}

// 环绕通知,用于记录日志
@Around("logPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//...
}
}

限流
利用 AOP 方式对接口进行限流,只需要在 Controller 的方法上使用自定义的 @RateLimit 限流注解即可。

1
2
3
4
5
6
7
/**
* 该接口 60 秒内最多只能访问 10 次,保存到 redis 的键名为 limit_test,
*/
@RateLimit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit")
public int test() {
return ATOMIC_INTEGER.incrementAndGet();
}

AOP 切面类 RateLimitAspect用来拦截带有 @RateLimit 注解的方法并处理:

1
2
3
4
5
6
7
8
9
@Slf4j
@Aspect
public class RateLimitAspect {
// 拦截所有带有 @RateLimit 注解的方法
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
//...
}
}

关于限流实现,并没有自己写 Redis Lua 限流脚本,而是利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。

权限控制
Spring Security 使用 AOP 进行方法拦截。在实际调用 update 方法之前,Spring 会检查当前用户的权限,只有用户权限满足对应的条件才能执行。

1
2
3
4
5
6
7
@Log(description = "修改菜单")
@PutMapping(value = "/menus")
// 用户拥有 `admin`、`menu:edit` 权限中的任意一个就能能访问`update`方法
@PreAuthorize("hasAnyRole('admin','menu:edit')")
public ResponseEntity update(@Validated @RequestBody Menu resources){
//...
}
SpringBoot是如何启动Tomcat的

1.首先,SpringBoot在启动时会先创建一个Spring容器
2.在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean
3.Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat

SpringBoot中常用注解及其底层实现

1.@SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
a.@SpringBootConfiquration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类
b.@EnableAutoConfiguration:向Spring容器中导入了一个selector,用来加载Classpath下springFactories中所定义的自动配置类,将这些自动加载为配置Bean
c.@ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
2.@Bean注解:用来定义Bean,类似于XML中的< bean >标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象
3.@Controller、@Service、@ResponseBody、@Autowired等常见注解

java反射的原理

反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

1
2
Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

上面这样子进行类对象的初始化,我们可以理解为「正」。

而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。

这时候,我们使用 JDK 提供的反射 API 进行反射调用:

1
2
3
4
5
Class clz = Class.forName("com.chenshuyi.reflect.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);

上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(Apple),而第二段代码则是在运行时通过字符串值才得知要运行的类(com.chenshuyi.reflect.Apple)。

所以说什么是反射?

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

@Component和@Bean的区别在哪里?

1.用途不用

@Component用于标识普通类

@Bean是在配置类中声明和配置Bean对象

2.使用方法不同

@Component是一个类级别的注解,Spring通过@ComponentScan注解扫描并注册为Bean

@Bean通过方法级别的注解使用,在配置类中手动声明和配置Bean

3.控制权不同

@Component注解修饰的类是由Spring框架来创建和初始化的

@Bean注解允许开发人员手动控制Bean的创建和配置过程

SpringBoot字段注入和构造函数注入的区别

在使用Spring开发项目时,我们经常需要使用依赖注入来管理对象之间的依赖关系。Spring提供了多种依赖注入方式,如构造函数注入、Setter方法注入和字段注入等。这些方式各有优缺点,需要根据具体情况选择合适的注入方式。

在本文中,我将分享我在开发过程中遇到的一些问题,以及我对这些问题的思考和解决方法。主要涉及以下几个方面:

  1. 字段注入和构造函数注入的区别和联系
  2. 为什么字段注入和Setter方法注入不会导致循环依赖的问题,而构造函数注入会导致循环依赖的问题
  3. 为什么Spring不推荐使用字段注入,而推荐使用构造函数注入

什么是字段注入和构造函数注入?
在SpringBoot中,我们可以使用@Autowired注解来实现依赖注入,即让Spring容器自动为我们的类提供所需的对象。有三种常见的注入方式:字段注入,Setter方法注入和构造函数注入。

  1. 字段注入:直接在类的属性上使用@Autowired注解,无需编写额外的代码。
  2. Setter方法注入:在类的Setter方法上使用@Autowired注解,需要编写相应的Setter方法。
  3. 构造函数注入:在类的构造函数上使用@Autowired注解,需要编写相应的构造函数。

下面是一个简单的例子,假设我们有一个UserService接口和一个UserServiceImpl实现类,以及一个UserController类,我们想要在UserController中使用UserService对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// UserService接口
public interface UserService {
void saveUser(User user);
}

// UserServiceImpl实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public void saveUser(User user) {
// 保存用户到数据库
}
}

// UserController类
@Controller
public class UserController {
// 字段注入
@Autowired
private UserService userService;

// Setter方法注入
// private UserService userService;
// @Autowired
// public void setUserService(UserService userService) {
// this.userService = userService;
// }

// 构造函数注入
// private final UserService userService;
// @Autowired
// public UserController(UserService userService) {
// this.userService = userService;
// }

public void createUser(User user) {
userService.saveUser(user);
// 其他逻辑
}
}

这两种方式有什么区别?
这两种方式在功能上没有区别,都可以实现依赖注入。但是在一些细节上有一些差异,主要有以下几点 :

  1. 可读性:字段注入的代码更简洁,依赖项被隔离在一个地方,更容易阅读。构造函数注入的代码更冗长,当有多个依赖项时,构造函数可能会变得臃肿。
  2. 不变性:构造函数注入支持不变性,即可以将依赖项声明为final类型,保证对象创建后不会被修改。这有利于线程安全性,状态安全性和可读性。字段注入不支持不变性,无法将依赖项声明为final类型。
  3. 状态安全性:构造函数注入保证了对象被实例化为完整状态或完全不被实例化。如果使用者使用new关键字创建对象,则必须提供所有依赖项作为参数。字段注入无法保证状态安全性,如果使用者使用new关键字创建对象,则无法设置对象的状态。唯一的选择是使用反射设置私有字段。
  4. 循环依赖:循环依赖是指两个或多个类相互依赖对方,导致无法正常创建对象。例如,如果A类依赖B类,B类依赖A类,则会产生循环依赖。循环依赖是一种不良的设计模式,应该避免。

字段注入和Setter方法注入的联系
字段注入和Setter方法注入都是通过反射来实现的,它们都可以在类的属性上使用@Autowired注解来标注依赖关系。它们的区别在于,字段注入是直接在属性上使用@Autowired注解,而Setter方法注入是在属性对应的Setter方法上使用@Autowired注解。

字段注入和Setter方法注入的联系有以下几点:

  1. 它们都是基于名称或者类型来匹配依赖关系的。如果属性名字或者Setter方法名字与Bean定义中的id或者name相同,则按照名称匹配;否则按照属性类型或者Setter方法参数类型匹配。
  2. 它们都不支持不变性,即无法将依赖项声明为final类型。这可能会导致线程安全性,状态安全性和可读性的问题。
  3. 它们都可以避免循环依赖的问题,因为它们是在对象创建后才进行依赖注入的,而不是在对象创建时。这样可以避免构造函数注入时可能出现的循环依赖异常。

为什么字段注入和Setter方法注入不会导致循环依赖的问题?
循环依赖是指两个或多个类相互依赖对方,导致无法正常创建对象。例如,如果A类依赖B类,B类依赖A类,则会产生循环依赖。循环依赖是一种不良的设计模式,应该避免。

在Spring中,循环依赖主要发生在构造函数注入的情况下,因为构造函数注入是在对象创建时就进行依赖注入的,而不是在对象创建后。这样就会导致一个死锁的情况,即A类要等待B类创建完成才能创建,而B类又要等待A类创建完成才能创建。

字段注入和Setter方法注入不会导致循环依赖的问题,因为它们是在对象创建后才进行依赖注入的,而不是在对象创建时。这样就可以避免死锁的情况,即A类和B类都可以先创建出来,然后再互相注入对方。

Spring解决循环依赖的方法是通过提前暴露半成品对象(Early-Stage Object)来解决。半成品对象是指已经实例化但还没有完成初始化的对象。Spring会将半成品对象放入一个缓存中,当其他对象需要依赖它时,就可以从缓存中获取它,并进行后续的属性赋值和初始化操作。

两种方式的流程

字段注入和构造函数注入的流程如下:

字段注入:当IOC容器创建Bean时,它会先通过反射调用无参构造函数来实例化对象,然后再通过反射获取属性上的@Autowired注解,并根据名称或者类型来匹配依赖关系,最后通过反射将依赖关系注入到属性中。

构造函数注入:当IOC容器创建Bean时,它会先通过反射获取构造函数上的@Autowired注解,并根据名称或者类型来匹配依赖关系,然后再通过反射调用带参构造函数来实例化对象,并将依赖关系作为参数传递进去。

为什么Spring不推荐使用字段注入?

Spring不推荐使用字段注入的原因有以下几点:

字段注入违反了单一职责原则,因为它使得添加新的依赖项非常容易,而不会引起警告。这可能导致类有太多的责任和关注点,需要进一步的检查和重构。

字段注入隐藏了依赖关系,因为它没有使用公共接口(方法或构造函数)来清楚地与依赖项通信。这样就不利于类的可测试性和可重用性,也不利于依赖项的可选性和强制性的区分。

字段注入导致了依赖注入容器的耦合,因为它使得类无法脱离容器独立运行。这意味着类不能通过new关键字来创建,也不能切换到其他的依赖注入框架。

字段注入不支持不变性,因为它无法将依赖项声明为final类型,也无法注入静态变量。这可能会导致线程安全性,状态安全性和可读性的问题。

总结

字段注入和构造函数注入都是Spring中常用的依赖注入方式,它们各有优缺点,开发人员应根据具体情况选择合适的注入方式。一般来说,以下几点可以作为参考:

  1. 如果依赖关系是必须的,且不需要重新配置或者重新注入,则推荐使用构造函数注入,因为它可以支持不变性和状态安全性。
  2. 如果依赖关系是可选的,或者需要重新配置或者重新注入,则推荐使用字段注入或者Setter方法注入,因为它们可以提高代码的简洁性和灵活性。
  3. 如果有循环依赖的问题,则不能使用构造函数注入,只能使用字段注入或者Setter方法注入,因为它们可以避免死锁的情况。
Spring是如何整合MyBatis将Mapper接口注册为Bean的原理?

1.首先MyBatis的Mapper接口核心是JDK动态代理

2.Spring会排除接口,无法注册到IOC容器中

3.MyBatis实现了BeanDefinitionRegistryPostProcessor可以动态注册BeanDefinition

4.需要自定义扫描器(继承Spring内部扫描器ClassPathBeanDefinitionScanner)重写排除接口的方法(isCandidateComponent)

5.但是接口虽然注册成了BeanDefinition但是无法实例化Bean,因为接口无法实例化

6.需要将BeanDefinition的BeanClass,替换成JDK动态代理的实例(偷天换日

7.MyBatis通过FactoryBean的工厂方法设计模式可以自由控制Bean的实例化过程,可以在getObject方法中创建JDK动态代理

SpringCloud有哪些常用组件,作用是什么

1.Eureka: 注册中心
2.Nacos: 注册中心、配置中心
3.Consul: 注册中心、配置中心
4.Spring Cloud Config: 配置中心
5.Feign/OpenFeign: RPC调用
6.Kong: 服务网关
7.Zuul: 服务网关
8.Spring Cloud Gateway: 服务网关
9.Ribbon: 负载均衡
10.Spring CLoud sleuth: 链路追踪
11.Zipkin: 链路追踪
12.Seata: 分布式事务
13.Dubbo: RPC调用
14.Sentinel: 服务熔断
15.Hystrix: 服务熔断

SpringCloud和Dubbo有哪些区别

SpringCloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo一开始是一个RPC调用框架,核心是解决服务调用间的问题,SpingCloud是一个大而全的框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有SpingChoud全面,但是Dubbo的服务调用性能比Spring Cloud高,不过SpringCioud和Dubbo并不是对立的,是可以结合起来一起使用的。

Dubbo是如何完成服务导出的

1.首先Dubbo会将程序员所使用的@Dubboserrce注解或@Senice注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
2.然后调用ServiceBean的export方法进行服务导出
3.然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
4.将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
5.还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat、Netty等

Dubbo是如何完成服务引入的

1.当程序员使用@Reference注解来引入一个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服务名、服务接口是什么
2.然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中
3.并绑定一些监听器用来监听动态配置中心的变更
4,然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean

Dubbo支持哪些负载均衡策略

1.随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率
2.轮询:依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法
3.最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
4.一致性哈希:相同参数的请求总是发到同一个服务提供者

epoll和poll的区别

1.select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件
2.poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了IO事件
3.epoll模型,epoll和poll是完全不同的,epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询

HashMap的扩容机制原理

1.7版本
1.先生成新数组
遍历老数组中的每个位置上的链表上的每个元素
2.)
3.取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
将元素添加到新数组中去
4.
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本
1.先生成新数组
2.遍历老数组中的每个位置上的链表或红黑树
3.如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4.如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
a.统计每个下标位置的元素个数
b.如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
c.如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
5.所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

接口优化

后端优化

1.缓存机制

2.并发调用

3.同步接口异步化

4.避免大事务

5.优化日志记录

数据库

1.数据库查询优化

2.表设计冗余数据

3.使用连接池管理数据库连接

4.使用数据压缩技术

5.加机器,或者换成合适的数据库

RocketMQ的事务消息是如何实现的

1.生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的
2.再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
3.并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
4.一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
5.如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理

Redis

Redis是单线程吗?

Redis单线程指的是**{接受客户端请求->解析请求->进行数据读写扽操作->发送数据给客户端}这个过程是由一个线程(主线程)**来完成的,这是我们常说Redis是单线程的原因

但是,Redis程序并不是单线程的,Redis在启动的时候,是会启动后台线程 (BIO)

2.6版本,会启动2个后台线程,分别处理关闭文件,AOF刷盘这两个任务

4.0版本之后,新增了一个新的后台线程,用来异步释放Redis内存,也就是lazyfree线程

6.0版本之后,采用了多个I/O线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络I/O的处理上。但是对于命令的执行,Redis仍然使用单线程来处理。

Redis持久化

方式:

AOF日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里

RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘

混合持久化方式:Redis 4.0 新增的方式,集成了AOF和RBD的优点

Redis集群

如何是实现高可用

主从复制

主从复制时Redis高可用服务的最基础的保证,实现方案就是将从前的一台Redis服务器,同步数据到多台从Redis服务器上,即一主多从的模式,且从服务器之间采用的时(读写分离)的方式

注意,主从服务器之间的命令复制是异步进行的

哨兵模式

在使用Redis主从服务的时候,会有一个问题,就是当Redis的主从服务器出现故障宕机时,需要手动进行恢复

为了解决 这个问题,Redis增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能

切片集群模式

当Redis缓存数据量大到一台服务器无法缓存时,就需要使用Redis切片集群(Redis Cluster)方案,它将数据分布在不同的服务器上吗,以此来降低系统对单主节点的依赖,从而提高Redis服务的读写性能

Redis过期删除与内存淘汰

Redis使用的过期策略策略是(惰性删除+定时删除)这两种策略配合使用

1.惰性删除

不主动删除过期键,每次从数据库访问key时,都检测key是否过期,如果过期则删除该key

优点

只会使用很少的系统资源,对CUP时间友好

缺点

造成一定的内存空间浪费,对内存不友好

2.定期删除

每一段时间(随机)从数据库中取出一定数量的key进行检查,并删除其中的过期key

优点

通过限制删除操作执行的时长和频率,来减少删除操作对CPU的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用

缺点

难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对CPU不友好,如果执行的太少,那又和惰性删除一样了,过期key占用的内存不会及时得到释放

Redis缓存设计

如何避免缓存雪崩?

大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户的用户请求,都无法在Redis中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩

解决方案

将缓存失效时间随机打散:我们可以在原有的失效时间基础上增加一个随机值(比如1到10分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率

设置缓存不过期:我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题

如何避免缓存击穿?

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题

可以认为缓存击穿时缓存雪崩的一个子集,应对缓存击穿可以采取前面两种方案

1.互斥锁方案(Redis中使用setNX方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值

2.不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期时,提前通知后台线程更新缓存以及重新设置过期时间

如何避免缓存穿透?

当用户访问的数据,既不在缓存中,又不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求,那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题

缓存穿透的发生 一般有这两种情况:

1.业务误操作:缓存中的数据和数据库中的数据都被误删除了,所有导致缓存和数据库中都没有数据

2.黑客恶意攻击:故意大量访问某些读取不存在数据的业务

解决方案

非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

缓存更新策略

常见的缓存更新策略共有3种:

1.Cache Aside(旁路缓存)策略

2.Read/Write Through(读写/写穿)策略

3.Write Back(写回)策略

实际开发中,Redis和MySQL的更新策略用的是Cache Aside,另外两种策略应用不了

Cache Aside(旁路缓存)策略

可以细分为读策略写策略

写策略步骤:

先更新数据库中的数据,再删除缓存中的数据

读策略步骤

如何读取的数据命中了缓存,则直接返回数据

如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户

Redis实战
Redis 如何实现延迟队列?

延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:

1.在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消

2.打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单

3.点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单

在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。

Redis管道有什么用?

管道技术(Pipeline)是客户端提供的一种批处理技术,用于一次处理多个Redis命令,从而提高整个交互性能

使用管道技术可以解决多个命令执行时的网络等待,它是吧多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。

但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞

要注意的是,管道技术本质上是客户端提供的功能,而非Redis服务器的功能

如何用Redis实现分布式锁的?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源同一时刻只能被一个应用所使用

Redis的SET命令有个NX参数可以实现(key不存在才插入),所以可以用它来实现分布式锁:

1.如歌key不存在,则显示插入成功,可以用来表示加锁成功

2.如果key存在,则会显示插入失败,可以用来表示加锁失败

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件

1.加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁

2.锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间

3.锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端

满足这三个条件的分布式命令如下:

1
set lock_key unique_value NX PX 10000

lock_key 就是 key 键
unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作
NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作
PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁

而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

基于 Redis 实现分布式锁的优点:
1.性能高效(这是选择缓存实现分布式锁最核心的出发点)。
2.实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。
3.避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。

基于 Redis 实现分布式锁的缺点:
1.超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,注意 A 线程没执行完,后续线程 B 又意外的持有了锁,意味着可以操作共享资源,那么两个线程之间的共享资源就没办法进行保护了。

  • 那么如何合理设置超时时间呢?我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。

2.Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

Redis 如何解决集群情况下分布式锁的可靠性?

为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)

它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。

Redlock 算法加锁三个过程:

第一步是,客户端获取当前时间(t1)。

第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:

  1. 加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。

  2. 如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时 时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。

第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。

加锁成功要同时满足两个条件(简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功):

条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;

条件二:客户端从大多数节点获取锁的总耗时(t2-t1)小于锁设置的过期时间。

加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。

加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。

Redis数据结构的应用场景

Redis的数据结构:
1.**字符串:**可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个ison格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
2.**哈希表:**可以用来存储一些key-value对,更适合用来存储对象
3.**列表:**Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
4.**集合:**和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
5.**有序集合(ZSet):**集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能

MySQL

MySQL执行流程

MySQL的架构共分为两层:Server层和存储引擎层

Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等)都在 Server 层实现。

存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+树 ,且是默认使用,也就是说在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引。

第一步:连接器

1.连接的过程需要先经过TCP三次握手,因为MySQL是基于TCP协议进行传输的。

2.校验客户端的用户和密码,如果用户名或密码不对,则会报错

3.如果用户名和密码都对,会读取该用户的权限,然后后面的权限逻辑判断都基于此时读取的权限

第二步:查询缓存

连接器得工作完成后,客户端就可以向 MySQL 服务发送 SQL 语句了,MySQL 服务收到 SQL 语句后,就会解析出 SQL 语句的第一个字段,看看是什么类型的语句。

如果 SQL 是查询语句(select 语句),MySQL 就会先去查询缓存( Query Cache )里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。

如果查询的语句命中查询缓存,那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中,那么就要往下继续执行,等执行完后,查询的结果就会被存入查询缓存中。

这么看,查询缓存还挺有用,但是其实查询缓存挺鸡肋的。

对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。如果刚缓存了一个查询结果很大的数据,还没被使用的时候,刚好这个表有更新操作,查询缓冲就被清空了,相当于缓存了个寂寞。

所以,MySQL 8.0 版本直接将查询缓存删掉了,也就是说 MySQL 8.0 开始,执行一条 SQL 查询语句,不会再走到查询缓存这个阶段了。

对于 MySQL 8.0 之前的版本,如果想关闭查询缓存,我们可以通过将参数 query_cache_type 设置成 DEMAND

第三步:解析SQL

在正式执行SQL查询语句之前,MySQL会先对SQL语句做解析,这个工作交由(解析器)来完成

解析器

第一件事情,词法分析。MySQL 会根据你输入的字符串识别出关键字出来,例如,SQL语句 select username from userinfo,在分析之后,会得到4个Token,其中有2个Keyword,分别为select和from。

关键字 非关键字 关键字 非关键字
select username from userinfo

第二件事情,语法分析。根据词法分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法,如果没问题就会构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、 where 条件等等。

如果我们输入的SQL语句语法不对,就会在解析器这个阶段报错。

第四步:执行SQL

经过解析器后,接着就要进入执行 SQL 查询语句的流程了,每条SELECT 查询语句流程主要可以分为下面这三个阶段:

1.prepare 阶段,也就是预处理阶段;
2.optimize 阶段,也就是优化阶段;
3.execute 阶段,也就是执行阶段;

预处理器

1.检查 SQL 查询语句中的表或者字段是否存在

2.将 select * 中的 * 符号,扩展为表上的所有列

优化器

经过预处理阶段后,还需要为 SQL 查询语句先制定一个执行计划,这个工作交由「优化器」来完成的。

优化器主要负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。

例如查询语句(select * from product where id = 1),就是选择使用主键索引。

要想知道优化器选择了哪个索引,我们可以在查询语句最前面加个 explain 命令,这样就会输出这条 SQL 语句的执行计划,然后执行计划中的 key 就表示执行过程中使用了哪个索引。

explain的type字段(访问类型)的10个状态(从左到右,越靠左的越优秀)

NULL system const eq_ref ref ref_or_null index_merge range index ALL

执行器

经历完优化器后,就确定了执行方案,接下来 MySQL 就真正开始执行语句了,这个工作是由「执行器」完成的。在执行的过程中,执行器就会和存储引擎交互了,交互是以记录为单位的。

接下来,用三种方式执行过程,说一下执行器和存储引擎的交互过程

1.主键索引查询

以下面这个查询语句为例,看看执行器是怎么工作的。

1
select * from product where id = 1;

这条查询语句的查询条件用到了主键索引,而且是等值查询,同时主键 id 是唯一,不会有 id 相同的记录,所以优化器决定选用访问类型为 const 进行查询,也就是使用主键索引查询一条记录,那么执行器与存储引擎的执行流程是这样的:

  1. 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为 InnoDB 引擎索引查询的接口,把条件 id = 1 交给存储引擎,让存储引擎定位符合条件的第一条记录。
  2. 存储引擎通过主键索引的 B+ 树结构定位到 id = 1的第一条记录,如果记录是不存在的,就会向执行器上报记录找不到的错误,然后查询结束。如果记录是存在的,就会将记录返回给执行器;
  3. 执行器从存储引擎读到记录后,接着判断记录是否符合查询条件,如果符合则发送给客户端,如果不符合则跳过该记录。
  4. 执行器查询的过程是一个 while 循环,所以还会再查一次,但是这次因为不是第一次查询了,所以会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为一个永远返回 - 1 的函数,所以当调用该函数的时候,执行器就退出循环,也就是结束查询了。

2.全表扫描

举个全表扫描的例子:

1
select * from product where name = 'iphone';

这条查询语句的查询条件没有用到索引,所以优化器决定选用访问类型为 ALL 进行查询,也就是全表扫描的方式查询,那么这时执行器与存储引擎的执行流程是这样的:

  1. 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 all,这个函数指针被指向为 InnoDB 引擎全扫描的接口,让存储引擎读取表中的第一条记录;
  2. 执行器会判断读到的这条记录的 name 是不是 iphone,如果不是则跳过;如果是则将记录发给客户的(是的没错,Server 层每从存储引擎读到一条记录就会发送给客户端,之所以客户端显示的时候是直接显示所有记录的,是因为客户端是等查询语句查询完成后,才会显示出所有的记录)。
  3. 执行器查询的过程是一个 while 循环,所以还会再查一次,会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 all,read_record 函数指针指向的还是 InnoDB 引擎全扫描的接口,所以接着向存储引擎层要求继续读刚才那条记录的下一条记录,存储引擎把下一条记录取出后就将其返回给执行器(Server层),执行器继续判断条件,不符合查询条件即跳过该记录,否则发送到客户端;
  4. 一直重复上述过程,直到存储引擎把表中的所有记录读完,然后向执行器(Server层) 返回了读取完毕的信息;
  5. 执行器收到存储引擎报告的查询完毕的信息,退出循环,停止查询。

3.索引下推

索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将Server层部分负责的事情,交给存储引擎层去处理了

举一个具体的例子,age 和 height字段建立了联合索引(age,height):

1
select * from Students where age > 20 and height = 180;

联合索引当遇到范围查询 (>、<) 就会停止匹配,也就是 age 字段能用到联合索引,但是 height字段则无法利用到索引。

不使用索引下推(MySQL 5.6 之前的版本)时,执行器与存储引擎的执行流程是这样的:

  1. Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录
  2. 存储引擎根据二级索引的 B+ 树快速定位到这条记录后,获取主键值,然后进行回表操作,将完整的记录返回给 Server 层
  3. Server 层在判断该记录的 height 是否等于 180,如果成立则将其发送给客户端;否则跳过该记录
  4. 接着,继续向存储引擎索要下一条记录,存储引擎在二级索引定位到记录后,获取主键值,然后回表操作,将完整的记录返回给 Server 层
  5. 如此往复,直到存储引擎把表中的所有记录读完。

可以看到,没有索引下推的时候,每查询到一条二级索引记录,都要进行回表操作,然后将记录返回给 Server,接着 Server 再判断该记录的 height是否等于 180。

而使用索引下推后,判断记录的 height是否等于 180 的工作交给了存储引擎层,过程如下 :

  1. Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录
  2. 存储引擎定位到二级索引后,先不执行回表操作,而是先判断一下该索引中包含的列(height列)的条件(height是否等于 180)是否成立。如果条件不成立,则直接跳过该二级索引。如果成立,则执行回表操作,将完成记录返回给 Server 层。
  3. Server 层在判断其他的查询条件(本次查询没有其他条件)是否成立,如果成立则将其发送给客户端;否则跳过该记录,然后向存储引擎索要下一条记录。
  4. 如此往复,直到存储引擎把表中的所有记录读完。

可以看到,使用了索引下推后,虽然 height 列无法使用到联合索引,但是因为它包含在联合索引(age,height)里,所以直接在存储引擎过滤出满足 height= 180的记录后,才去执行回表操作获取整个记录。相比于没有使用索引下推,节省了很多回表操作。

当你发现执行计划里的 Extr 部分显示了 “Using index condition”,说明使用了索引下推。

总结
  • 连接器:建立连接,管理连接、校验用户身份

  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块

  • 解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型

  • 执行 SQL:执行 SQL 共有三个阶段:

    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。

    • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划

    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

常见面试题
索引的分类

按照四个角度来分类索引

按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。

B树和B+树的区别,为什么MySQL使用B+树

B树的特点:

1.节点排序

2.一个节点可以存多个元素,多个元素也排序了

B+树的特点:

1.拥有B树的特点

2.叶子节点之间有指针

3.非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所以的元素,并且排好顺序

Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql!中一个nnodb页就是一个B+树节点,一个mnodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。

Innodb是如何实现事务的

Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,
以一个update语句为例:
1.Innodb在收到一个update语句后,会先根据条件找到数据所在的页,
并将该页缓存在Buffer Pool中
2.执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
3.针对update语句生成-个RedoLog对象,并存入LogBuffer中
4.针对update语句生成undolog日志,用于事务回滚
5.如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Bufer Pool中所修改的数据页持久化到磁盘中
6.如果事务回滚,则利用undolog日志进行回滚

MySQL的rewriteBatchedStatements的使用场景
批量保存方式 数据量(条) 耗时(ms)
单条循环插入 1000 121011
mybatis-plus saveBatch 1000 59927
mybatis-plus saveBatch(添加rewtire参数) 1000 2589
手动拼接sql 1000 2275
jdbc executeBatch 1000 55663
jdbc executeBatch(添加rewtire参数) 1000 324

所以如果有使用 JDBC的 Batch 性能方面的需求,要将 rewriteBatchedStatements 设置为 true,这样能提高很多性能。

然后如果喜欢手动拼接 sql 要注意一次拼接的数量,分批处理。

MySQL慢查询该如何优化?

1.检查是否走了索引,如果没有则优化SQL利用索引
2.检查所利用的索引,是否是最优索引
3.检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
4.检查表中数据是否过多,是否应该进行分库分表了
5.检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

MySQL锁有哪些,如何理解

按锁粒度分类:
1.行锁:锁某行数据,锁粒度最小,并发度高
2.表锁:锁整张表,锁粒度最大,并发度低
3.间隙锁:锁的是一个区间

还可以分为:
1.共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
2.排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读(不能加读锁),也不能写

还可以分为:
1.乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
2.悲观锁:上面所的行锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利用锁来解决幻读

算法笔记

链表

判断回文

1.和出栈比较

2.快慢指针(右边进栈和左边比较)

3.空间O(1)快慢指针把中赋为null右边指针反转,头尾比较等于null退出,把链表复原

栈方法简单(笔试用)

改原链表的方法就需要注意边界了(面试用)

判断是否有环

快慢指针相遇有环(在环上相遇)

快指针为null无环

快指针从头再来慢指针还在原地快慢指针每次走一步,最终会在第一个入环节点处相遇

布隆过滤器

样本量

失误率

判断一个32位的数是否为2的幂(用位运算)

x&(x-1)==0

判断一个32位的数是否为4的幂(用位运算)

1.是2的幂

2.x&(0x55555555)!=0 //0x55555555==(010101…01)

计划搜索缓存

跳过重复条件

二叉树序列化和反序列化

层次遍历序列化
层次遍历反序列化
二叉树的宽度优先遍历(用队列)
最大宽度
最大宽度不用Map
给二叉树中的某个节点,返回该节点的后继节点

二叉树的结构

二叉树的递归套路

请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。给定一个输入参数N,代表纸条都从下边向上方连续对折N次。请从上到下打印所有折痕的方向。例如:N=1时,打印: down N=2时,打印: down down up

思路:用递归模拟了树(树没有建出来)

左右高度相差大于1

给定一颗二叉树的头节点head,任何两个节点之间都存在距离,返回整颗二叉树的最大距离
给定一颗二叉树的头节点head,返回这颗二叉树中最大的二叉树搜索子树的头节点
派对的最大快乐值

打表找规律(暴力找规则)

牛羊吃草

规律解法

连续正数和的数

规律解法

矩阵处理技巧

1.zigzag打印矩阵

1 2 3

4 5 6 –> 打印输出 124753689

7 8 9

2.转圈打印矩阵
原地旋转正方形矩阵

贪心算法求解的标准过程

解题套路
宣讲次数最多

结构

暴力解

第二种

排序(根据谁的时间短排序)

最少灯

暴力法

贪心

分金条

贪心

最大盈利
并查集

简写if

分类用户(代码与上面的通用)

基本结构
宽度优先遍历
深度优先遍历
拓扑排序
最小生成树(Kruskal)利用并查集
Prim
Dijkstra(迪特拉)

改进

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
public class Dijkstra {

public static HashMap<Node, Integer> dijkstra1(Node from) {
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(from, 0);
// 打过对号的点
HashSet<Node> selectedNodes = new HashSet<>();
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
while (minNode != null) {
// 原始点 -> minNode(跳转点) 最小距离distance
int distance = distanceMap.get(minNode);
for (Edge edge : minNode.edges) {
Node toNode = edge.to;
if (!distanceMap.containsKey(toNode)) {
distanceMap.put(toNode, distance + edge.weight);
} else { // toNode
distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
}
}
selectedNodes.add(minNode);
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
}
return distanceMap;
}

public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> touchedNodes) {
Node minNode = null;
int minDistance = Integer.MAX_VALUE;
for (Entry<Node, Integer> entry : distanceMap.entrySet()) {
Node node = entry.getKey();
int distance = entry.getValue();
if (!touchedNodes.contains(node) && distance < minDistance) {
minNode = node;
minDistance = distance;
}
}
return minNode;
}

public static class NodeRecord {
public Node node;
public int distance;

public NodeRecord(Node node, int distance) {
this.node = node;
this.distance = distance;
}
}

public static class NodeHeap {
private Node[] nodes; // 实际的堆结构
// key 某一个node, value 上面堆中的位置
private HashMap<Node, Integer> heapIndexMap;
// key 某一个节点, value 从源节点出发到该节点的目前最小距离
private HashMap<Node, Integer> distanceMap;
private int size; // 堆上有多少个点

public NodeHeap(int size) {
nodes = new Node[size];
heapIndexMap = new HashMap<>();
distanceMap = new HashMap<>();
size = 0;
}

public boolean isEmpty() {
return size == 0;
}

// 有一个点叫node,现在发现了一个从源节点出发到达node的距离为distance
// 判断要不要更新,如果需要的话,就更新
public void addOrUpdateOrIgnore(Node node, int distance) {
if (inHeap(node)) {
distanceMap.put(node, Math.min(distanceMap.get(node), distance));
insertHeapify(heapIndexMap.get(node));
}
if (!isEntered(node)) {
nodes[size] = node;
heapIndexMap.put(node, size);
distanceMap.put(node, distance);
insertHeapify(size++);
}
}

public NodeRecord pop() {
NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
swap(0, size - 1);
heapIndexMap.put(nodes[size - 1], -1);
distanceMap.remove(nodes[size - 1]);
// free C++同学还要把原本堆顶节点析构,对java同学不必
nodes[size - 1] = null;
heapify(0, --size);
return nodeRecord;
}

private void insertHeapify(int index) {
while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}

private void heapify(int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])
? left + 1
: left;
smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;
if (smallest == index) {
break;
}
swap(smallest, index);
index = smallest;
left = index * 2 + 1;
}
}

private boolean isEntered(Node node) {
return heapIndexMap.containsKey(node);
}

private boolean inHeap(Node node) {
return isEntered(node) && heapIndexMap.get(node) != -1;
}

private void swap(int index1, int index2) {
heapIndexMap.put(nodes[index1], index2);
heapIndexMap.put(nodes[index2], index1);
Node tmp = nodes[index1];
nodes[index1] = nodes[index2];
nodes[index2] = tmp;
}
}

// 改进后的dijkstra算法
// 从head出发,所有head能到达的节点,生成到达每个节点的最小路径记录并返回
public static HashMap<Node, Integer> dijkstra2(Node head, int size) {
NodeHeap nodeHeap = new NodeHeap(size);
nodeHeap.addOrUpdateOrIgnore(head, 0);
HashMap<Node, Integer> result = new HashMap<>();
while (!nodeHeap.isEmpty()) {
NodeRecord record = nodeHeap.pop();
Node cur = record.node;
int distance = record.distance;
for (Edge edge : cur.edges) {
nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
}
result.put(cur, distance);
}
return result;
}

}

暴力递归

什么暴力递归可以继续优化
汉诺塔

递归

非递归

逆序栈
字符串的子序列
无重复子序列
字符串全排列
无重复全排列(分支限界)
数字字符转化
背包问题

另一种

选牌
n皇后

利用位移

货币凑整
1
2
3
4
5
6
7
8
9
10
11
// arr[index....] 所有的面值,每一个面值都可以任意选择张数,组成正好rest这么多钱,方法数多少?
public static int process(int[] arr, int index, int rest) {
if (index == arr.length) { // 没钱了
return rest == 0 ? 1 : 0;
}
int ways = 0;
for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
ways += process(arr, index + 1, rest - (zhang * arr[index]));
}
return ways;
}

递归改动态规划(题目同上面相同)

步骤:暴力递归(重复解)->可变参数(不讲究组织)->记忆化搜索(精细化组织)->经典动态规划
回到原始位置

2

背包问题

2

数字字符转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 从右往左的动态规划
// 就是上面方法的动态规划版本
// dp[i]表示:str[i...]有多少种转化方式
public static int dp1(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] str = s.toCharArray();
int N = str.length;
int[] dp = new int[N + 1];
dp[N] = 1;
for (int i = N - 1; i >= 0; i--) {
if (str[i] != '0') {
int ways = dp[i + 1];
if (i + 1 < str.length && (str[i] - '0') * 10 + str[i + 1] - '0' < 27) {
ways += dp[i + 2];
}
dp[i] = ways;
}
}
return dp[0];
}

// 从左往右的动态规划
// dp[i]表示:str[0...i]有多少种转化方式
public static int dp2(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] str = s.toCharArray();
int N = str.length;
if (str[0] == '0') {
return 0;
}
int[] dp = new int[N];
dp[0] = 1;
for (int i = 1; i < N; i++) {
if (str[i] == '0') {
// 如果此时str[i]=='0',那么他是一定要拉前一个字符(i-1的字符)一起拼的,
// 那么就要求前一个字符,不能也是‘0’,否则拼不了。
// 前一个字符不是‘0’就够了嘛?不够,还得要求拼完了要么是10,要么是20,如果更大的话,拼不了。
// 这就够了嘛?还不够,你们拼完了,还得要求str[0...i-2]真的可以被分解!
// 如果str[0...i-2]都不存在分解方案,那i和i-1拼成了也不行,因为之前的搞定不了。
if (str[i - 1] == '0' || str[i - 1] > '2' || (i - 2 >= 0 && dp[i - 2] == 0)) {
return 0;
} else {
dp[i] = i - 2 >= 0 ? dp[i - 2] : 1;
}
} else {
dp[i] = dp[i - 1];
if (str[i - 1] != '0' && (str[i - 1] - '0') * 10 + str[i] - '0' <= 26) {
dp[i] += i - 2 >= 0 ? dp[i - 2] : 1;
}
}
}
return dp[N - 1];
}
选牌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static int win(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int N = arr.length;
int[][] fmap = new int[N][N];
int[][] gmap = new int[N][N];
for (int i = 0; i < N; i++) {
fmap[i][i] = arr[i];
}
for (int startCol = 1; startCol < N; startCol++) {
int L = 0;
int R = startCol;
while (R < N) {
fmap[L][R] = Math.max(arr[L] + gmap[L + 1][R], arr[R] + gmap[L][R - 1]);
gmap[L][R] = Math.min(fmap[L + 1][R], fmap[L][R - 1]);
L++;
R++;
}
}
return Math.max(fmap[0][N - 1], gmap[0][N - 1]);
}
货币凑整
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// arr[index....] 所有的面值,每一个面值都可以任意选择张数,组成正好rest这么多钱,方法数多少?
public static int dp1(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int ways = 0;
for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
ways += dp[index + 1][rest - (zhang * arr[index])];
}
dp[index][rest] = ways;
}
}
return dp[0][aim];
}

public static int dp2(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];
if (rest - arr[index] >= 0) {
dp[index][rest] += dp[index][rest - arr[index]];
}
}
}
return dp[0][aim];
}
贴纸字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public static int minStickers2(String[] stickers, String target) {
int N = stickers.length;
// 关键优化(用词频表替代贴纸数组)
int[][] counts = new int[N][26];
for (int i = 0; i < N; i++) {
char[] str = stickers[i].toCharArray();
for (char cha : str) {
counts[i][cha - 'a']++;
}
}
int ans = process2(counts, target);
return ans == Integer.MAX_VALUE ? -1 : ans;
}

// stickers[i] 数组,当初i号贴纸的字符统计 int[][] stickers -> 所有的贴纸
// 每一种贴纸都有无穷张
// 返回搞定target的最少张数
// 最少张数
public static int process2(int[][] stickers, String t) {
if (t.length() == 0) {
return 0;
}
// target做出词频统计
// target aabbc 2 2 1..
// 0 1 2..
char[] target = t.toCharArray();
int[] tcounts = new int[26];
for (char cha : target) {
tcounts[cha - 'a']++;
}
int N = stickers.length;
int min = Integer.MAX_VALUE;
for (int i = 0; i < N; i++) {
// 尝试第一张贴纸是谁
int[] sticker = stickers[i];
// 最关键的优化(重要的剪枝!这一步也是贪心!)
if (sticker[target[0] - 'a'] > 0) {
StringBuilder builder = new StringBuilder();
for (int j = 0; j < 26; j++) {
if (tcounts[j] > 0) {
int nums = tcounts[j] - sticker[j];
for (int k = 0; k < nums; k++) {
builder.append((char) (j + 'a'));
}
}
}
String rest = builder.toString();
min = Math.min(min, process2(stickers, rest));
}
}
return min + (min == Integer.MAX_VALUE ? 0 : 1);
}

public static int minStickers3(String[] stickers, String target) {
int N = stickers.length;
int[][] counts = new int[N][26];
for (int i = 0; i < N; i++) {
char[] str = stickers[i].toCharArray();
for (char cha : str) {
counts[i][cha - 'a']++;
}
}
HashMap<String, Integer> dp = new HashMap<>();
dp.put("", 0);
int ans = process3(counts, target, dp);
return ans == Integer.MAX_VALUE ? -1 : ans;
}

public static int process3(int[][] stickers, String t, HashMap<String, Integer> dp) {
if (dp.containsKey(t)) {
return dp.get(t);
}
char[] target = t.toCharArray();
int[] tcounts = new int[26];
for (char cha : target) {
tcounts[cha - 'a']++;
}
int N = stickers.length;
int min = Integer.MAX_VALUE;
for (int i = 0; i < N; i++) {
int[] sticker = stickers[i];
if (sticker[target[0] - 'a'] > 0) {
StringBuilder builder = new StringBuilder();
for (int j = 0; j < 26; j++) {
if (tcounts[j] > 0) {
int nums = tcounts[j] - sticker[j];
for (int k = 0; k < nums; k++) {
builder.append((char) (j + 'a'));
}
}
}
String rest = builder.toString();
min = Math.min(min, process3(stickers, rest, dp));
}
}
int ans = min + (min == Integer.MAX_VALUE ? 0 : 1);
dp.put(t, ans);
return ans;
}
两个字符串的最长公共子序列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static int longestCommonSubsequence(char[] str1, char[] str2) {
int N = str1.length;
int M = str2.length;
int[][] dp = new int[N][M];
dp[0][0] = str1[0] == str2[0] ? 1 : 0;
for (int i = 1; i < N; i++) {
dp[i][0] = str1[i] == str2[0] ? 1 : dp[i - 1][0];
}
for (int j = 1; j < M; j++) {
dp[0][j] = str1[0] == str2[j] ? 1 : dp[0][j - 1];
}
for (int i = 1; i < N; i++) {
for (int j = 1; j < M; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
if (str1[i] == str2[j]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
}
return dp[N - 1][M - 1];
}
洗咖啡杯机
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 贪心+优良尝试改成动态规划
public static int minTime2(int[] arr, int n, int a, int b) {
PriorityQueue<Machine> heap = new PriorityQueue<Machine>(new MachineComparator());
for (int i = 0; i < arr.length; i++) {
heap.add(new Machine(0, arr[i]));
}
int[] drinks = new int[n];
for (int i = 0; i < n; i++) {
Machine cur = heap.poll();
cur.timePoint += cur.workTime;
drinks[i] = cur.timePoint;
heap.add(cur);
}
return bestTimeDp(drinks, a, b);
}
public static int bestTimeDp(int[] drinks, int wash, int air) {
int N = drinks.length;
int maxFree = 0;
for (int i = 0; i < drinks.length; i++) {
maxFree = Math.max(maxFree, drinks[i]) + wash;
}
int[][] dp = new int[N + 1][maxFree + 1];
for (int index = N - 1; index >= 0; index--) {
for (int free = 0; free <= maxFree; free++) {
int selfClean1 = Math.max(drinks[index], free) + wash;
if (selfClean1 > maxFree) {
break; // 因为后面的也都不用填了
}
// index号杯子 决定洗
int restClean1 = dp[index + 1][selfClean1];
int p1 = Math.max(selfClean1, restClean1);
// index号杯子 决定挥发
int selfClean2 = drinks[index] + air;
int restClean2 = dp[index + 1][free];
int p2 = Math.max(selfClean2, restClean2);
dp[index][free] = Math.min(p1, p2);
}
}
return dp[0][0];
}

设计暴力递归过程的原则

重构-改善既有代码的设计

第一章,重构,第一个案例

1.如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构哪个程序,使特性的添加比较容易进行,然后再添加特性
2.重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力
3.重构技术就是以微小的步伐修改程序,如果你犯下错误,很容易便可发现它。
4.任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码, 才是优秀的程序员

第二章,重构原则

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构
1.事不过三,三则重构
2.不要过早发布接口,请修改你的代码所有权政策,使重构更顺畅

第三章,代码的坏味道

1.当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余

第四章,构筑测试体系

1.确保所有测试都完全自动化,让它们检查自己的测试结果
2.一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的空间
3.频繁地运行测试。每次编译请把测试也考虑进去–每天至少执行每个测试一次
4.每当你收到bug报告,请先写一个单元测试来暴露bug
5.编写未臻完善的测试并实际运行,好过对完美测试的无尽等待
6.考虑可能出错的边界条件,把测试火力集中在哪儿
7.当事情被认为应该会出错时,别忘记了检查是否抛出了预期的异常
8.不要因为测试无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug

第八章,重新组织数据

检查重构目标是否为不可变对象,或是否可修改为不可变对象

如果该对象目前还不是不可变的,就使用Remove Setting Method(300),直到它成为不可变的为止

如果无法将该对象修改为不可变的,就放弃使用本项重构

建立equals()和hashCode()
编译,测试
考虑是否可以删除工厂函数,并将构造函数声明为pubilc
一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有以下几点

1,你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过于复杂,2,与GUI隔离之后,领域对象的维护和演化都会更容易,你甚至可以让不同的开发者负责不同部分的开发

第九章,简化条件表达式

1.从if,then,else三个段落中分别提炼出独立函数
2.将测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数
3.将重复代码搬到条件表达式之外
4.以break语句或return语句取代控制标记
5.使用卫语句表现所有特殊情况
6.将条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数
7.将null值替换为null对象
8.以断言明确表现这种假设

第十章,简化函数调用

1.修改函数名称
2.为函数添加一个对象参数,让该对象带进函数所需信息
3.移除不必要的参数
4.将查询函数和修改函数分离
5.建立单一函数,以参数表示那些不同的值
6.以明确函数取代参数
7.保持对象完整
8.让参数接受者去除该项参数,并直接调用前一个函数
9.以一个对象取代此参数
10.去掉该字段的所有设值函数
11.将函数修改为private
12.将构造函数替换为工厂函数
13.将向下转型动作移到函数中
14.以异常取代错误码
15.以测试取代异常

第十一章,处理概括关系

1.将子类相同的字段移至父类
2.将子类相同功能的函数移至父类
3.如果各子类中拥有一些几乎一致的构造函数,请在父类中新建一个构造函数,并在子类构造函数中调用它
4.父类中的某个函数只与部分子类相关,将这个函数移到相关的子类去
5.父类中的某些字段只被部分子类用到,将这个字段移到需要它的子类去
6.如果类中的某些特性只被某些实例用到,新建一个子类,将那部分的特性移到子类中
7.如果两个类(或者多个类)有相似特性,为者两个类建立一个超类,将相同特性移至超类
8.将相同的子集提炼到一个独立接口中
9.父类和子类之间无太大区别,将它们合为一体
10.你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同,将这些操作分别放进独立函数中,并保持原函数上移超类
11.某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据,在子类中新建一个字段用以保存超类,调整子类函数,令它改而委托超类,然后去掉两者之间的继承关系
12.在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数,让委托类继承受托类

第十二章,大型重构

1.如果某个继承体系同时承担两项责任,建立两个继承体系,并通过委托关系让其中一个可以调用另一个
2.将过程化设计转化为对象设计
3.将领域逻辑分离出来,为它们建立独立的领域类
4.建立继承体系,以一个子类表示一种特殊情况

第十三章,重构,复用与实现

1.重构以求短期利益
2.降低重构带来的开销
3.安全地进行重构

并发编程艺术

1.如何减少上下文切换

减少上下文切换的方法有无锁并发编程,CAS算法,使用最少线程和使用协程

2.如何解决资源限制的问题

对于硬件资源限制,可以考虑使用集群并行执行程序,既然单机的资源有限制,那么就让程序在多机上运行,比如使用ODPS,Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。

对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接

3.volatile的两条实现原则

1.Lock前缀指令会引起处理器缓存回写到内存

2.一个处理器的缓存回写到内存会导致其他处理器的缓存无效

4.处理器如何实现原子操作

1.第一个机制时通过总线锁保证原子性

2.第二个机制是通过缓存锁定来保证原子性

但是有两种情况下处理器不会使用缓存锁定

第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定

第二种情况是:有些处理器不支持缓存锁定。

5.CAS实现原子操作的三大问题

1.ABA问题

2.循环时间长开销大

3.只能保证一个共享变量的原子操作

6.在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信时指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递
7.Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序原完全透明。
8.happens-before规则

1.程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作

2.监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁

3.volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读

4.传递性:如果A happens-before B,且B happens-before C,那么A happens-before C

9.未同步程序的执行特性

差异

1.顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。

2.顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。

3.JMM不保证64位long型和double型的变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

10.由于Java的CAS同时具有volatile读和volatile写的内存语义因此Java线程之间的通信现在有了下面4种方式

1.A线程写volatile变量,随后B线程读这个volatile变量

2.A线程写volatile变量,随后B线程用CAS更新这个volatile变量

3.A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量

4.A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量

11.对于final,编译器和处理器要遵守两个重排序规则

1.在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

12.线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定
13.锁降级指的是写锁降级成为读锁,如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
14.合理地使用线程池能够带来3个好处

第一,降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗

第二,提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行

第三,提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一分配,调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

15.在并发编程中使用生产者和消费者模式能够解决大多数并发问题。该模式通过平衡生产者和消费者的工作能力来提高程序整体处理数据的速度

设计模式

设计原则

1.针对接口编程,而不是针对实现编程

2.”针对接口编程“真正的意思是“针对超类型(supertype)编程”

3.多用组合少用继承

4.为了交互对象之间的松耦合设计而努力

5.类应该对扩展开放,对修改关闭

6.要依赖抽象,不要依赖具体类(依赖倒置原则)

7.最少知识原则:只和你的密友谈话

8.别调用我们,我们会调用你

9.一个类应该只有一个引起变化的原因

松耦合的威力

当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节,观察者模式提供了一种对象设计,让主题和观察者之间松耦合

松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降低了最低

策略模式

定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户

要点

1.策略模式通常会用行为或算法配置Context类

观察者模式

定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者会收到通知并自动更新

要点

1.观察者模式定义了对象之间一对多的关系

2.主题(也就是可观察者)用一个共同的接口来更新观察者

3.观察者和可观察者之间用松耦合方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口

4.使用此模式时,你可从被观察者处推(push)或拉(pull)数据(然而,推的方式被认为更“正确”)。

5.有多个观察者时,不可以依赖特定的通知次序

6.Java有多种观察者模式的实现,包括了通用的java.util.Observable实现上所带来的一些问题

7.如果有必要的话,可以实现自己的Observable,这并不难,不要害怕

8.Swing大量使用观察者模式,许多GUI框架也是如此

9.此模式也被应用在许多地方,例如:JavaBeans,RMI

装饰者模式

装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案

装饰者和被装饰对象有相同的超类型

你可以用一个或多个装饰者包装一个对象

既然装饰者和被装饰者有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象替代它

装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的

对象可以在任何时候被装饰,所以可以在运行时动态地,不限量地用你喜欢的装饰者来装饰对象

要点

1.继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式

2.在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码

3.组合和委托可用与在运行时动态地加上新的行为

4.除了继承装饰者模式也可以让我们扩展行为

5.装饰者模式意味着一群装饰者类,这些类用来包装具体组件

6.装饰者类放映处被装饰的组件类型(事实上,它们具有相同的类型,都经过接口或继承实现)

7.装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的

8.你可以用无数个装饰者包装一个组件

9.装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型

10.装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂

简单工厂方法模式

定义了一个创建对象的接口,但由于子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类

依赖倒置原则

避免违反

1.变量不可以持有具体类的引用 (如果使用new,就会持有具体类的引用。你可以改用工厂避开这样的做法)

2.不要让类派生自具体类 (如果派生自具体类,你就会依赖具体类,请派生自一个抽象[接口或抽象类])

3.不要覆盖基类中已实现的方法 (如果覆盖基类已实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该由所有的子类共享)

要点

1.简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类解耦

2.工厂方法使用继承,把对象的创建委托给子类,子类实现工厂方法来创建对象

3.工厂方法允许类将实例化延迟到子类进行

4.工厂是很有威力的技巧,帮助外面针对抽象编程,而不要针对具体类编程

抽象工厂模式

提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

要点

1.所有的工厂都是用来封装对象的创建

2.抽象工厂使用对象组合,对象的创建被实现在工厂接口所暴露出来的方法中

3.所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合

4.抽象工厂创建相关的对象家族,而不需要依赖它们的具体类

5.依赖倒置原则,指导外面避免依赖具体类型,而要尽量依赖抽象

单例模式

确保一个类只有一个实例,并提供一个全局访问的

要点

1.单例模式确保程序中一个类最多只有一个实例

2.单例模式也提供访问这个实例的全局点

3.在Java中实现单例模式需要私有的构造器,一个静态方法和一个静态变量

4.确定在性能和资源上的限制,然后小心地选择适当的方案来实现单例,以解决多线程的问题(我们必须认定所有的程序都是多线程的)

5.小心,你如果使用多个类加载器,可能导致单例失效而产生多个实例

命令模式

将“请求”封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象,命令模式也支持可撤销的操作

要点

1.命令模式将发出请求的对象和执行请求的对象解耦

2.在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作

3.通用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用

4.调用者可以接受命令当做参数,甚至在运行时动态地进行

5.命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态

6.宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销

7.实际操作时,很常见使用“聪明”命令对象,也就是之间实现了请求,而不是将工作委托给接收者

8.命令也可以用来实现日志和事务系统

适配器模式

将一个类的接口,转换成客户期望的另一个接口。适配器让原来接口不兼容的类可以合作无间

外观模式

提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用

要点

1.当需要使用一个现有的类而其接口并不符合你的需要时,就使用适配器

2.当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观

3.适配器改变接口以符合客户的期望

4.外观将客户从一个复杂的子系统中解耦

5.实现一个适配器可能需要一番功夫,也可能不费功夫,视目标接口的大小与复杂度而定

6.实现一个外观,需要将子系统组合进外观中,然后将工作委托给子系统执行

7.适配器模式有两种形式:对象适配器和类适配器。类适配器需要用到多重继承

8.你可以为一个子系统实现一个以上的外观

9.适配器将一个对象包装起来以改变其接口,装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象“包装”起来以简化其接口

模板方法模式

在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤

要点

1.”模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类

2.模板方法模式为我们提供了一种代码复用的重要技巧

3.模板方法的抽象类可以定义具体方法,抽象方法和钩子

4.抽象方法由子类实现

5.钩子是一种方法,它在抽象类中不做事,或者只做默认的事,子类可以选择要不要去覆盖它

6.为了防止子类改变模板方法中的算法,可以将模板方法声明为final

7.好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用底层模块

8.你将在真实世界代码中看到模板方法模式的许多变体,不要期待它们全都是一眼就可以被你认出的

9.策略模式和模板方法模式都封装算法,一个用组合,一个用继承

10.工厂方法是模板方法的一种特殊版本

迭代器模式

提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示

迭代器模式让我们能游走于聚合内的每一个元素,而不暴露其内部的表示

把游走的任务放在迭代器上,而不是聚合上,这样简化了聚合的接口和实现,也让责任各得其所

要点

1.迭代器允许访问聚合的元素,而不需要暴露它的内部结构

2.迭代器将遍历聚合的工作封装进一个对象中

3.当使用迭代器的时候,我们依赖聚合提供遍历

4.迭代器提供一个通用的接口,让我们遍历聚合的项,当我们编码使用聚合的项时,就可以使用多态

5.我们应该努力让一个类只分配一个责任

单一原则

类的每个责任都有改变的潜在区域。超过一个责任,意味者超过一个改变的区域

这个原则告诉我们,尽量让每个类保持单一责任

组合模式

允许你将对象组合成树型结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合

组合模式让我们能用树形创建对象的结构,树里面包含了组合以及个别的对象**

使用组合结构,我们能把相同的操作应用在组合和个别对象上,换句话说,在大多数情况下,我们可以忽略组合和个别对象之间的差别

要点

1.组合模式提供一个结构,可同时包容个别对象和组合对象

2.组合模式允许客户对个别对象以及组合对象一视同仁

3.组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点

4.在实现组合模式时,有许多设计上的折衷。你要根据需要平衡透明性和安全性

状态模式

允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类

要点

1.状态模式允许一个对象基于内部状态而拥有不同得行为

2.和程序状态机(PSM)不同,状态模式用类代表状态

3.Context会将行为委托给当前状态对象

4.通过将每个状态封装进一个类,我们把以后需要做得任何改变局部化了

5.状态模式和策略模式有相同的类图,但是它们的意图不同

6..状态模式允许Context随着状态的改变而改变行为

7.状态转换可以由State类或Context类控制

8.使用状态模式通常会导致设计中类的数目大量增加

9.状态类可以被多个Context实例共享

代理模式

为另一个对象提供一个替身或占位符以控制对这个对象的访问

使用代理模式创建代表(representative)对象,让代表对象控制某对象的访问被代理的对象可以是远程的对象,创建开销打的对象或需要安全的控制对象

要点

1.代理模式为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种

2.远程代理管理客户和远程对象之间的交互

3.虚拟代理控制访问实例化开销大的对象

4.保护代理基于调用者控制对象方法的访问

5.代理模式有许多变体,例如:缓存代理,同步代理,防火墙代理和写入时复制代理

6.代理在结构上类似装饰者,但是目的不同

7.装饰者模式为对象加上行为,而代理则是控制访问

8.Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器

9.就和其他的包装者(wrapper)一样,代理会造成你的设计中类的数目增加

复合模式

要点

1.MVC是复合模式,结合了观察者模式,策略模式和组合模式

2.模型使用观察者模式,以便观察者更新,同时保持两者之间解耦

3.控制器是视图的策略,视图可以使用不同的控制器实现,得到不同的行为

4.视图使用组合模式实现用户界面,用户界面通常组合了嵌套的组件,像面板,框架和按钮

5.这些模式携手合作,把MVC模型的三层解耦,这样可以保持设计干净又有弹性

6.适配器模式用来将新的模型适配成已有的视图和控制器

7.Model2是MVC在Web上的应用

8.在Model2中,控制器实现成Serblet,而JSP/HTML实现视图

反模式

告诉你如何采用一个不好的解决方案解决一个问题

桥接模式

使用桥接模式(Bridge Pattern)不只改变你的实现,也改变你的抽象

桥接模式通过将实现和抽象放在两个不同的类层次中而使它们可以独立改变

适合使用在需要跨越多个平台的图形和窗口系统上

当需要用不同的方式改变接口和实现时,你会发现桥接模式很好用

优点

1.将实现予以解耦,让它和界面之间不再永久绑定

2.抽象和实现可以独立扩展,不会影响到对方

3.对于“具体的抽象类”所做的改变,不会影响到客户

缺点

桥接模式的缺点是增加了复杂度

生成器模式

使用生成器模式(Builder Pattern)封装一个产品的构造过程,并允许按步骤构造

经常被用来创建组合结构

优点

1.将一个复杂对象的创建过程封装起来

2.允许对象通过多个步骤来创建,并且可以改变过程(这和只有一个步骤的工厂模式不同)

3.向客户隐藏产品内部的表现

4.产品的实现可以被替换,因为客户只看到一个对象的接口

缺点

与工厂模式相比,采用生成器模式创建对象的客户,需要具备更多的领域知识

责任链模式

当你想要让一个以上的对象有机会能够处理某个请求对象的时候,就使用责任链模式(Chain of Responsibility Pattern)

经常被使用在窗口系统中,处理鼠标和键盘之类的事件

优点

将请求的发送者和接收者解耦

可以简化你的对象,因为它不需要知道链的结构

通过改变链内的成员或调动它们的次序,允许你动态地新增或者删除责任

缺点

并不保证请求一定会被执行,如果没有任何对象处理它的话,它可能会落到链尾端之外(这可以是优点也可以是缺点

可能不容易观察运行时的特征,有碍于出错

蝇量模式

如果让某个类的一个实例能够用来提供许多“虚拟实例”,就使用蝇量模式(Flyweight Pattern)

当一个类有许多的实例,而这些实例能被同一方法控制的时候,我们就可以使用蝇量模式

优点

减少运行时对象实例的个数,节省内存

将许多“虚拟”对象的状态集中管理

缺点

一旦你实现了它,那么单个的逻辑实例将无法拥有独立而不同的行为

解释器模式

使用解释器模式(Interpreter Pattern)为语言创建解释器

当你需要实现一个简单的语言时,使用解释器

当你有一个简单的语法,而且简单比效率更重要时,使用解释器

可以处理脚本语言和编程语言

优点

将每一个语法规则表示成一个类,方便于实现语言

因为语法由许多类表示,所以你可以轻易地改变或扩展此语言

通过在类结构中加入新的方法,可以在解释的同时增加新的行为,例如打印格式的美化或者进行复杂的程序验证

缺点

当语法规则的数目太大时,这个模式可能会变得非常繁杂。在这种情况下,使用解析器或编译器的产生器可能更加合适

中介者模式

使用中介者模式(Mediator Pattern)来集中相关对象之间复杂的沟通和控制方式

中介者常常被用来协调相关的GUI组件

优点

通过将对象彼此解耦,可以增加对象的复用性

通过将控制逻辑集中,可以简化系统维护

可以让对象之间所传递的消息变得简单而且大幅减少

缺点

中介者模式的缺点是,如果设计不当,中介者对象本身会变得过于复杂

备忘录模式

当你需要让对象返回之前的状态时(例如,你的用户请求“撤销”),就使用备忘录模式(MementoPattern)

备忘录用于储存状态

目的

储存系统关键对象的重要状态

维护关键对象的封装

优点

将被储存的状态放在外面,不要和关键对象混在一起,这可以帮助维护内聚

保持关键对象的数据封装

提供了容易实现的恢复能力

缺点

储存和恢复状态的过程可能相当耗时

在Java系统时,其实可以考虑使用序列化(serialization)机制储存系统的状态

原型模式

当创建给定类的实例的过程很复杂时,就使用原型模式(Prototype Pattern)

在一个复杂的类层次中,当系统必须从其中的许多类型创建新对象时,可以考虑原型

优点

向客户隐藏制造新实例的复杂性

提供让客户能够产生未知类型对象的选项

在某些环境下,复制对象比创建新对象更有效

缺点

对象的复制有时相当复杂

访问者模式

当你想要成为一个对象的组合增加新的能力,且封装并不重要时,就使用访问者模式(Visitor Pattern)

当采用访问者模式的时候,就会打破组合类的封装

优点

允许你对组合结构加入新的操作,而无需改变结构本身

想要加入新的操作,相对容易

访问者所进行的操作,其代码是集中在一起的

缺点

因为游走的功能牵涉其中,所以对组合结构的改变就更加困难

定义设计模式

模式是在某情景(context)下,针对某问题的某种解决方案

情境就是应用某个模式的情况。这应该是会不断出现的情况

问题就是你想在某情境下达到的目标,但也可以是某情境下的约束

解决方案就是你所追求的:一个通用的设计,用来解决约束,达到目标

如果你发现自己处于某个情境下,面对这所欲达到的目标被一群约束影响着的问题,然而,你能够应用某个设计,克服这些约束并达到该目标,将你领向某个解决方案

要点

1.让设计模式自然而然地出现在你的设计中,而不是为了使用而使用

2.设计模式并非僵化的教条,你可以依据自己的需要采用或调整

3.总是使用满足需要的最简单解决方案,不管它用不用模式

4.学习设计模式的类目,可以帮你自己熟悉这些模式以及它们之间的关系

5.模式的分类(或类目)是将模式分成不用的族群,如果这么做对你有帮助,就采用吧

6.你必须相当专注才能够成为一个模式的作家,这需要时间也需要耐心,同时还必须乐意做大量的精化工作

7.请牢记:你所遇到大多数的模式都是现有模式的变体,而非新的模式

8.模式能够为你带来的最大好处之一是,让你的团队拥有共享词汇

9.任何社群都有自己的行话,模式社群也是如此。别让这些行话绊着,在读完这本书之后,你已经能够应用大部分的行话了

总结

模式 描述
装饰者 包装一个对象,以提供新的行为
状态 封装了基于状态的行为,并使用委托在行为之间切换
迭代器 在对象的集合之中游走,而不暴露集合的实现
外观 简化一群类的接口
策略 封装可以互换的行为,并使用委托来决定要使用哪一个
代理 包装对象,以控制对此对象的访问
工厂方法 由子类决定要创建的具体类是哪一个
适配器 封装对象,并提供不同的接口
观察者 让对象能够在状态改变时被通知
模板方法 客户用一致的方式处理对象集合和单个对象
组合 客户用一致的方式处理对象集合和单个对象
单件(单例) 确保有且只有一个对象被创建
抽象工厂 允许客户创建对象的家族,而无需指定他们的具体类
命令 封装请求成为对象

并发编程

第一部分

线程安全

1.通过从框架线程中调用应用程序的组件,框架把并发引入了应用程序。组件总是需要访问程序的状态。因此要求在所有代码路径访问状态时,必须时线程安全的
2.设计线程安全的类时,优秀的面向对象技术–封装,不可变性以及明确的不变约束–会给你提供诸多的帮助
3.当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全
4.对于线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态
5.线程安全的类封装了任何必要的同步,因此客户不需要自己提供
6.无状态对象永远是线程安全的
7.为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量
8.对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的
9.每个共享的可变变量都需要由唯一一个确定的锁保护。而维护者应该清楚这个锁
10.对于每个涉及多个变量的不变约束,需要同一个锁保护其所有的变量
11.通常简单性与性能之间是相互牵制的。实现一个同步策略时,不要过早地维克性能而牺牲简单性(这是对安全性潜在的妥协)
12.有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成,执行这些操作期间不要占有锁

共享对象

1.在没有同步的情况下,编译器,处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,你总是会判断错误
2.锁不仅仅是关于同步与互斥的,也是关于内存可见的,为了保证所有线程都能够看到共享的,可变变量的最新值,读取和写入线程必须使用公共的锁进行同步
3.只由当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量的方式包括:用于确保它们所引用的对象状态的可见性,或者用于标识重要的生命周期事件(比如初始化或关闭)的发生
4.枷锁可以保证可见性与原子性,volatile变量只能保证可见性
5.使用volatile变量的基本要求
1.写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2.变量不需要与其他的状态变量共同参与不变约束
3.而且,访问变量时,没有其他的原因需要加锁
6.不要让this引用在构造期间逸出
7.不可变对象永远是线程安全的
8.只有满足如下状态,一个对象才是不可变的:
1.它的状态不能在创建后再被修改
2.所有域都是final类型
3.被正确创建(创建期间没有发生this引用的逸出)
9.正如“将所有的域声明为私有的,除非它们需要更高的可见性”一样,“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践
10.不可变对象可以在没有额外同步的情况下,安全地用于任意线程,甚至发布它们时亦不需要同步
11.为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确创建的对象可以通过下列条件安全地发布:
1.通过静态初始化器初始化对象的引用
2.将它的引用存储到volatile域或AtomicReference
3.将它的引用存储到正确创建的对象的final域中
4.或者将它的引用存储到由锁正确保护的域中
12.任何线程都可以在没有额外的同步下安全地使用一个安全发布的高效不可变对象
13.发布对象的必要条件依赖于对象的可变性
1.不可变对象可以通过任意机制发布
2.高效不可变对象必须要安全发布
3.可变对象必须要安全发布,同时必须要线程安全或者时被锁保护

组合对象

1.设计线程安全类的过程应该包括下面3面基本要素:
1.确定对象状态是由那些变量构成的
2.确定限制状态变量的不变约束
3.制定一个管理并发访问对象状态的策略
2.不理解对象的不变约束和后验条件,你就不能保证线程安全性。要约束状态变量的有效值或者状态转换,就需要原子性与封装性0
3.将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁
4.限制性使构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序
5.如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量
6.如果一个状态变量是线程安全的,没有任何不变约束限制它的值,并且没有任何状态转换限制它的操作,那么它可以被安全发布
7.为类的用户编写类线程安全性担保的文档,为类的维护者编写类的同步策略文档

构建块

1.正如封装一个对象的状态,能够使它更加容易地保持不变约束一样,封装它的同步则可以迫使它符合同步策略
2.用并发容器替换同步容器,这种作法以有很小风险带来了可扩展性显著的提高
3.相比于Hashtable和synchronizedMap,ConcurrentHashMap有众多的优势,而且几乎不存在什么劣势,因此在大多数情况下用ConcurrentHashMap取代同步Map实现只会带来更好的可伸缩性。只有当你的程序需要在独占访问中加锁时,ConurrentHashMap才无法胜任(ConcurrentHashMap是线程安全的)
4.有界队列是强大的资源管理工具,用来建立可靠的应用程序,它们遏制那些可以产生过多工作量,具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮

第一部分总结

1.所有并发问题都归结为如何协调访问并发状态,可变状态越少,保证线程安全就越发容易
2.尽量将域声明为final类型,除非它们的需要是可变的
3.不可变对象天生是线程安全的
4.不可变对象极大地减轻了并发编程的压力。它们简单而且安全,可以在没有锁或者防御性复制的情况下自由地共享
5.封装使管理复杂度变得更可行
6.用锁来守护每一个可变变量
7.对同一不变约束中的所有变量都使用相同的锁
8.在运行复合操作期间持有锁
9.在非同步的多线程情况下,访问可变变量的程序是存在隐患的
10.不要依赖于可以需要同步的小聪明
11.在设计过程中就考虑线程安全。或者在文档中明确地说明它不是线程安全的
12.文档化你的同步策略

第二部分

构建并发应用程序

任务执行

1.如果要在你的程序中实现一个生产者-消费者的设计,使用Executor通常是最简单的方式
2.无论何时当你看到这种形式的代码:
new Thread(runnable).start()
并且你可能最终希望获得一个更加灵活的执行策略时,请认真考虑使用Executor代替Thread
3.Executors中的静态工厂方法

image-20230921090327806

4.大量相互独立且同类的任务进行并发处理,会将程序的任务量分配到不同的任务中,这样才能真正获得性能的提升

取消和关闭

1.在API和语言规范中,并没有把中断与任何取消的语意绑定起来,但是,实际上,使用中断来处理取消之外的任何事情都是不明智的,并且很难支撑起更大的应用
2.调用interrupt并不意味着必然停止目标线程正在进行的工作,它仅仅传递了请求中断的消息
3.中断通常是实现取消最明智的选择
4.因为每一个线程都有其自己的中断策略,所以你不应该中断线程,除非你知道中断对这个线程意味着什么
5.只有实现了线程中断策略的代码才可以接收中断请求。通用目的的任务和库的代码绝不应该接收中断请求
6.对于线程持有的服务,只要服务的存在时间大于创建线程的方法存在的时间,那么就应该提供生命周期方法
7.在一个长时间运行的应用程序中,所有的线程都要给未捕获异常设置一个处理器,这个处理器至少要将异常信息记入日志中

精灵线程

8.线程被分为两种:普通线程和精灵线程(daemon thread)。JVM启动时创建所有的线程,除了主线程以外,其他的都是精灵线程(比如垃圾回收器和其他类似线程)。当一个新的线程创建时,新线程继承了创建它的线程的后台状态,所以默认情况下,任何主线程创建的线程都是普通线程。
9.普通线程和精灵线程之间的差别仅仅在于退出时会发生什么
10.应用程序中,精灵线程不能替代对服务的生命周期恰当,。良好的管理
11.避免使用finalizer

应用线程池

1.一些任务具有这样的特征:需要或者排斥某种特定的执行策略。对其他任务具有依赖性的任务,就会要求线程池足够大,来保证它所依赖任务不必排队或者不被拒绝,采用线程限制的任务需要顺序地执行。把这些需求都写入文档,这样将来的维护者就不会使用一个与原先相悖的执行策略,而破坏安全性或活跃度
2.无论何时,你提交了一个非独立的Executor任务,要明确出现线程饥饿死锁的可能性,并且,在代码或者配置文件以及其他可以配置Executor的地方,任何有关池的大小和配置约束都要写入文档
3.newCachedThreadPool工厂提供了比定长的线程池更好的队列等候性能,它是Executor的一个很好的默认选择。出于资源管理的目的,当你需要限制当前任务的数量,一个定长的线程池就是很好的选择。就像一个接受网络客户端请求的服务器应用程序,如果不进行限制,就会很容易因为过载而遭受攻击。
4.当每个迭代彼此独立,并且完成循环体中每个迭代的工作,意义都足够重大,足以弥补管理一个新任务的开销时,这个顺序循环是适合并行化的

总结

对于并发执行的任务,Executor框架是强大且灵活的。它提供了大量可调节的选项,比如创建和关闭线程的策略,处理队列任务的策略,处理过剩任务的策略,并且提供了几个钩子函数用于扩展它的行为,然而,和大多数强大的框架一样,草率地将一些设定组合在一起,并不能很好地工作;一些类型的任务需要特定的执行策略,而一些调节参数组合在一起后可能产生意外的结果

GUI应用程序

1.Swing的单线程规则:Swing的组件和模型只能在事件分派线程中被创建,修改和请求
2.如果一个数据模型必须被多个线程共享,而且实现一个线程安全模型的尝试却由于阻塞、一致性或者复杂度等原因而失败,这时可以考虑运用分拆模型设计
3.GUI框架几乎都是作为单线程化子系统实现的,所有与表现相关的代码都作为任务在一个事件线程中运行。因为只要唯一一个线程,耗时任务会损害响应性,所以它们应该在后台线程中运行。像SwingWorker以及构建BackgroundTask这些助手类,提供了对取消、进度指示、完成指示的支持/无论是GUI组件还是非GUI组件,都能借助它们简化耗时任务的开发

活跃度,性能和测试

避免活跃度危险

1.安全性和活跃度通常相互牵制。我们使用锁开保证线程安全,但是滥用锁可能引起锁顺序死锁(lock-ordering deadlock)。类似的,我们使用线程池和信号量来约束资源的使用,但是却不能知晓那些管辖范围内的活动可能形成的资源死锁(resource deadlock)。Java应用程序不能从死锁中恢复,所以确保你的设计能够避免死锁出现的先决条件是非常有价值的
2.如果所有线程以通用的固定秩序获得锁,程序就不会出现锁顺序死锁问题了
3.在持有锁的时候调用外部方法是在挑战活跃度问题。外部方法可能会获得其他锁(产生死锁的风险),或者遭遇严重超时的阻塞。当你持有锁的时候会延迟其他试图获得该锁的线程
4.当调用的方法不需要持有锁时,这被称为开放调用
5.在程序中尽量使用开发调用。依赖于开发调用程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度(deadlock-freedom)的分析
6.抵制使用线程优先级的诱惑,因为这会增加平台依赖性,并且可能引起活跃度问题。大多数并发应用程序可以对所有线程使用的优先级
7.可伸缩性指的是:当增加计算资源的时候(比如增加额外CPU数量、内存、存储器、I/O带宽),吞吐量和生产量能够相应地得以改进
8.避免不成熟的优化,首先使程序正确,然后再加快–如果它运行得还不够快
9.测评。不要臆测
10.所有得并发程序都要一些串行源;如果你认为你没有,那么去仔细检查吧
11.不要过分担心非竞争得同步带来的开销。基础的机制已经足够快了,在这个基础上,JVM能够进行额外的优化,大大减少或消除了开销。关注那些真正发生了锁竞争的区域中性能的优化
12.串行化会损害可伸缩性,上下文切换回损害性能。竞争性的锁会同时导致这两种损失,所以减少锁的竞争能够改进性能和可伸缩性
13.并发程序中,对可伸缩性首要的威胁是独占的资源锁
14.三种方式来减少锁的竞争
1.减少持有锁的时间
2.减少请求锁的频率
3.或者用协调机制取代独占锁,从而允许更强的并发性
15.Amdahl定律告诉我们,程序的可伸缩性是由必须连续执行的代码比例决定的

总结

16.可伸缩性通常可以通过以下这些方式提升:减少用于获取锁的时间,减小锁的粒度,减少锁的占用时间,或者用非独占或非阻塞锁来取代独占锁

测试并发程序

1.为并发类创建有效的安全测试,其挑战在于:如何在程序出现问题并导致某些属性极度可能失败时,简单地识别出这些检查的属性来,同时不要人为的让查找错误的代码限制住程序的并发性。最好能做到在检查测试的属性时,不需要任何的同步。
2.测试应该在多处理器系统上运行,以提高潜在交替运行的多样性。但是,多个CPU未必会使测试更加高效,为了能够最大程度地检测到时序敏感的数据竞争的发生机会,应该让测试中的线程数多于CPU数,这样在任何给定的时间里,都要一些线程在运行,一些被交换出执行队列,这样可以增加线程间交替行为的随机性
3.编写有效的性能测试,就需要哄骗优化器不要把你的基准测试当作死代码而优化掉。这需要每一个计算的结果都要应用在你的程序中–以一种不需要的同步或真实计算的方式

高级主题

显示锁

1.性能是一个不断变化的目标,昨天的基准显示X比Y更快,这可能已经过时了
2.正如默认的ReentrantLock一样,内部锁没有提供确定的公平性保证,但是大多数锁实现统计上的公平性保证,在大多数条件下已经足够好了,Java语言规范并没有要求JVM公平地实现内部锁,JVM也的确没有这样做。ReentrantLock并没有减少锁的公平性–它只不过使一些存在的部分更显性化了
3.在内部锁不能够满足使用时,ReentrantLock才被作为更高级的工具。当你需要以下高级特性时,才应该使用;可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized
4.读-写锁(ReadWriteLock)允许多个读者并发访问被守护的对象,当访问多为读取数据结构的时候,它具有改进可伸缩性的潜力

构建自定义的同步工具

1.条件谓词是先验条件的第一站,它在一个操作与状态之间建立起依赖关系
2.将条件谓词和与之关联的条件队列,以及在条件队列中等待的操作,都写入文档
3.每次调用wait都会隐式地与特定的条件谓词相关联。当调用特定条件谓词的wait时,调用者必须已经持有了与条件队列相关的锁,这个锁必须同时还保护着组成条件谓词的状态变量
4.一个单独的内部条件队列可以与多个条件谓词共同使用
5.当使用条件等待(Object.wait或者Condition。await)
1.永远设置一个条件谓词–一些对象状态的测试,线程执行前必须满足它;
2.永远在调用wait前测试条件谓词,并且从wait中返回后再次测试
3.永远在循环中调用wait
4.确保构成条件谓词的状态变量被锁保护,而这个锁正是与条件队列相关联的;
5.当调用wait,notify或者notifyAll时,要持有与条件队列相关联的锁,并且
6.在检查条件谓词之后,开始执行被保护的逻辑之前,不要释放锁
7.无论何时,当你在等待一个条件,一定要确保有人会在条件谓词变为真时通知你
8.只有同时满足下述条件后,才能用单一的notify取代notifyAll(一般使用notifyAll):相同的等待者。只有一个条件谓词与条件队列相关,每个线程从wait返回执行行相同的逻辑;并且,一进一出。一个对条件变量的通知,至多只激活一个线程执行
9.尽管使用notifyAll而非notify可能有些低效,但是这样做更容易确保你的类的行为时正确的
10.一个依赖于状态的类,要么完全将它的等待和通知协议暴露(并文档化)给子类,要么完全阻止子类参与其中
11.危险警告:wait,notify和notifyAll在Condition对象中的对象对等体是await,signal和signalAll。但是,Condition继承Object,这意味着它也有wait和notify方法。一定要确保使用了正确的版本–await和signal!

原子变量与非阻塞同步机制

1.如果能够避免的话,不共享状态的开销会更小。能够通过更有效地竞争改进可伸缩性,但是真正的可伸缩完全是通过减少竞争实现的
2.非阻塞算法通过使用底层级并发原语,比如比较并交换,取代了锁。原子变量类向用户提供了这些底层级原语,也能够当作“更佳的volatile变量”使用,同时提供了整数类和对象引用的原子化更新操作
3.非阻塞算法在设计和实现中很困难,但是在典型条件下能够提供更好的可伸缩性,并能更好地预防活跃度失败。从JVM的一个版本到下一个版本间并发性能的提升很大程度上来源于非阻塞算法的使用,包括在JVM内部以及平台类库。

Java存储模型

1.happens-before的法则包括:
程序次序法制:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都出现在动作A之后
监视器锁法制:对一个监视器锁的解锁happens-before于每一个后续对同一监视器锁的加锁。
volatile变量法制:对volatile域的写入操作happens-before于每一个后续对同一域的读操作。
线程启动法制:在一个线程里,对Thread.start的调用会happens-before于每一个启动程序中的动作
线程启动法制:在一个线程里,对Thread.start的调用会happens-before于每一个启动线程中的动作
线程终结法制:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或者Thread.isAlive返回false。
中断法制:一个线程调用另一个线程的interrupt happens-before 于被中断的线程发现中断(通过抛出InterruptedException,获知调用isInterrupted和interrupted)
终结法制:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
传递性:如果A happens-before于B,且B happens-before 于C,则A happens-before于C
2.除了不可变对象以外,使用被另一个线程初始化的对象,是不安全的,除非对象的发布时happens-before于对象的消费线程使用它
3.初始化安全可以保证,对于正确创建的对象,无论它如何发布的,所有线程都将看到构造函数设置的final域的值,更进一步,一个正确创建的对象中,任何可以通过其final域触及到的变量(比如一个final数组中的元素,或者一个final域引用的HashMap里面的内容),也可以保证对其他线程都是可见的。
4.初始化安全性保证只有以通过final域触及的值,在构造函数完成时才是可见的。对于通过非final域触及的值,或者创建完成后可能改变的值,必须使用同步来确保可见性。

同步Annotation

3个类级Annotation来描述类的可预期的线程安全性保证:@Immutable ,@ThreadSafe 和 @NotThreadSafe

@Immutable自然是意味着类是不可变的,并包含了@ThreadSafe的意义。@NotThreadSafe是可选的–如果类没有被标明是线程安全的,就无法肯定它是不是线程安全的,但是如果你想明确地表示出它不是线程安全的,就标注为@NotThreadSafe

这些Annotation相对是非侵入的,这对用户和维护者都是有益的。用户可以立即看出一个类是否线程安全的,维护者也可以直接检查是否遵循了线程安全性保证。Annotation对于第三个利益既得者也是有用的:工具。静态的代码分析工具可以有能力对代码进行验证,看它是否遵循了由Annotation指定的契约,比如标明为@Immutable的类是否真是不可变的

JAVA建议

1.考虑用静态工厂方法代替构造函数

静态工厂方法的好处

1.与构造函数不同,静态工厂方法具有名字

2.与构造函数不同,它们每次被调用的时候,不要求非得创建一个新的对象

3.与构造函数不同,它可以返回一个原返回类型的子类型的对象

缺点

主要:类如果不含公有的或者受保护的构造函数,就不能被子类化

2.它们与其他的静态方法没有任何区别

2.使用私有构造函数强化singleton属性

3.通过私有构造函数强化不可实例化的能力

4.避免创建重复的对象

5.消除过期的对象引用

6.避免使用终结函数

7.在改写equals的时候请遵守通用约定

8.在改写equals时总是要改写hashCods

9.总是要改写toString

10.谨慎改写clone

11.考虑实现Comparable接口

12.使类和成员的可访问能力最小化

尽可能使每一个类或成员不被外界访问

13.支持非可变性

1.不要提供任何会修改对象的方法(也称为mutator)
2.保证没有可被子类改写的方法
3.使所有的域都是final的
4.使所有域都是成为私有的
5.保证对于任何可变组件的互斥访问

14.复合优先于继承

15.要么专门为继承而设计,并给出文档说明,要么禁止继承

一个类必须通过某种形式提供适合的钩子,以便能够进入到它的内部工作流程中,这样的形式可以是精心选择的受保护(protected)方法

构造函数一定不能调用可被改写的方法

无论是clone还是readObject,都不能调用一个可改写的方法,不管是直接的方式,还是间接的方式

为了继承设计一个类,要求对这个类有一些实质性的限制

对于那些并非为了安全地进行子类化而设计和编写文档类,禁止子类化

禁止子类化的两种方法

1.直接把这个类声明为final的

2.把所有的构造函数变成私有的,或者包级私有的,并且增加一些公有的静态工厂来替代构造函数的位置

16接口优于抽象类

接口和抽象类最大的区别是:抽象类允许包含某些方法的实现,但是接口是不允许的

已有的类可以很容易被更新,已实现新的接口
接口是定义mixin(混合类型)的理想选择
接口使得我们可以构造出非层次结构的类型框架
接口使得安全地增强一个类的功能成为可能

你可以把接口和抽象类的优点结合起来,对于你期望导出的每一个重要接口,都提供一个抽象的骨架实现(skeletal implementation)类

抽象类的演化比接口的演化要容易得多

17.接口只是被用于定义类型

常量接口模式是对接口的不良使用

18.优先考虑静态成员类

如果你声明的成员类不要求访问外围实例,那么请记住把static修饰符放到成员类的声明中

19.用类代替结构

20.用类层次来代替联合

21.用类来代替enum结构

22.用类和接口来代替函数指针

23.检查参数的有效性

24.需要时使用保护性拷贝

假设类的客户会尽一切手段来破坏这个类的约束条件,在这样的前提下,你必须保护性地设计程序
对于构造函数的每个可变参数进行保护性拷贝(defensive copy)是必要的
保护性拷贝动作是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是原始的对象
对于“参数类型可以被不可信方子类化”的情形,请不要使用clone方法进行参数的保护性拷贝

25.谨慎设计方法的原型

谨慎选择方法的名字
不要过于追求提供便利的方法
避免长长的参数列表
对于参数类型,优先使用接口而不是类
谨慎地使用函数对象

26.谨慎地使用重载

对于重载该方法(overloaded method)的选择是静态的,而对于被改写的方法(overridden method)的选择是动态的
避免方法重载机制的混淆用法
一个安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法

27.返回零长度的数组而不是null

没有理由从一个取数组值(array-valued)的方法中返回null,而不是返回一个零长度数组

28.为所有导出的API元素编写文档注释

为了正确地编写API文档,你必须在每一个被导出的类,接口,构造函数,方法和域声明之前增加一个文档注释
每一个方法的文档注释应该简洁地描述出它和客户之间的约定

29.将局部变量的作用域最小化

使一个局部变量的作用域最小化,最有力的技术是在第一次使用它的地方声明
几乎每一个局部变量的声明都应该包含一个初始化表达式

30.了解和使用库

通过使用标准库,你可以充分利用这些编写标准库的专家的知识,以及在你之前其他人的使用经验
在每一个主要的发行版本中,都会有许多新的特性被加入到库中,所以与这些库保持同步是值得的

31.如果要求精确的答案,请避免使用float和double

32.如果其他类型更合适,则尽量避免使用字符串

字符串不适合代替其他的值类型
字符串不适合代替枚举类型
字符串不适合代替聚集类型
字符串也不适合代替能力表

33.了解字符串连接的性能

为连接n个字符串而重复地使用字符串连接操作符,要求n的平方级的时间
为了获得可接受的性能,请使用StringBuffer替代String

34.通过接口引用对象

如果你养成了使用接口作为类型的习惯,那么你的程序将会更加灵活
如果没有合适的接口存在的话,那么,用类而不是接口来引用一个对象,是完全合适的

35.接口优先于映像机制

映像机制的代价
损失了编译时类型检查的好处
要求执行映像访问的代码非常笨拙和冗长
性能损失
通常,普通应用在运行时刻不应该以映像方式访问对象
如果只是在很有限的情况下使用映像机制,那么虽然也会付出少许代价,但你可以获得许多好处

36.谨慎地使用本地方法

37.谨慎地进行优化

努力避免那些限制性能的设计决定
考虑你的API设计决定的性能后果
为了获得好的性能而对API进行曲改,这是一个非常不好的想法
在每次试图做优化之前和之后,请对性能进行测量

39.只针对不正常的条件才使用异常

38.遵守普遍接受的命名惯例

异常只应该被同于不正常的条件,它们永远不应该被用于正常的控制流
一个设计良好的API不应该强迫它的客户为了正常的控制流而使用异常

40.对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常

如果期望调用者能够恢复,那么,对于这样的条件应该使用被检查的异常
用运行时异常来指明程序错误
你所实现的所有的未被检查的抛出结构都应该时RuntimeException的子类(直接的或者间接的)

41.避免不必要地使用被检查的异常

42.尽量使用标准的异常

43.抛出的异常要适合于相应的抽象

高层的实现应该捕获底层的异常,同时抛出一个可以按照高层抽象进行解释的异常
尽管异常转译比不加选择地传递低层异常的做法有所改进,但是它也不能被滥用

44.每个方法抛出的异常都要有文档

总是要单独地声明被检查的异常,并且利用Javadoc的@throws标记,标准地记录下每个异常被抛出的条件
使用Javadoc的@throws标签记录下一个方法可能会抛出的每个未被检查的异常,但是不要使用throws关键字将未被检查的异常包含在方法的声明中
如果一个类中的许多方法出于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的

45.在细节消息中包含失败-捕获消息

为了捕获失败,一个异常的字符串表示应该包含所有“对该异常有贡献”的参数和域的值

46.努力使失败保持原子性

一般而言,一个失败的方法调用应该使对象保持“它在被调用之前的状态”

47.不要忽略异常

空的catch块会使异常达不到应有的目的
至少catch块也应该包含一条说明,用来解释为什么忽略掉这个异常是适合适的

48对共享可变数据的同步访问

为了提高性能,在读或写原子数据的时候,你应该避免使用同步。这个建议是非常危险而错误的
为了在线程之间可靠地通信,以及为了互斥访问,同步是需要的
一般情况下,双重检查模式并不能正确地工作
简而言之,无论何时当多个线程共享可变数据的时候,每个读或者写数据的线程必须获得一把锁

49.避免过多的同步

为了避免死锁的危险,在一个被同步的方法或者代码块中,永远不要放弃对客户的控制

50.永远不要在循环的外面调用wait

总是使用wait循环模式来调用wait方法

51.不要依赖于线程调度器

任何依赖于线程调度器而达到正确性或性能要求的程序,很有可能是不可移植的
线程优先级是Java平台上最不可移植的特征了
对于大多数程序员来说,Thread.yield的惟一用途是在测试期间人为地增加一个程序的并发性

52.线程安全性的文档化

在一个方法的声明中出现synchronization修饰符,这是一个实现细节,并不是实现细节,并不是导出的API的一部分
一个类为了可破多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别
安全级别

非可变的

线程安全的

有条件的线程安全

线程兼容的

线程对立的

53.避免使用线程组

线程组基本上已经过时了

54.谨慎地实现Serialization

因为实现Serialization而付出的最大代价是,一旦一个类被发布,则“改变这个类的实现”的灵活性将大大降低
实现Serialization的第二个代价是,它增加了错误(bug)和安全漏洞的可能性
实现Serialization的第三个代价是,随着一个类的新版本的发行,相关的测试负担增加了
实现Serialization接口不是一个很轻松就可以做出的决定
为了继承而设计的类应该很少实现Serialization,接口也应该很少会扩展它
对于为继承而设计的不可序列化的类,你应该考虑提供一个无参数的构造函数

55.考虑使用自定义的序列化形式

若没有认真考虑默认序列化形式是否合适,则不要接受这种形式
如果一个对象的物理表示等同于它的逻辑内容,则默认的序列化形式可能是合适的
即使你确定了默认序列化形式是合适的,通常你仍然要提供一个readObject方法以保证约束关系和安全性
当一个对象的物理表示与它的逻辑数据内容有实质性的区别时,使用默认序列化形式有4个缺点:

1.它使这个类的导出API永远地束缚在该类的内部表示上

2.它要消耗过多的空间

3.它要消耗过多的时间

4.它会引起栈溢出

transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉
如果所有的实例域都是transient的,那么,从技术角度而言,省去调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这么做
在决定将一个域做成非transient之前,请一定要确信它的值将是该对象逻辑状态的一部分
不管你选择了那种序列化形式,你都要为自己编写的每个可序列化的类声明一个显式的序列版本UID(serial version UID)

56.保护性地编写readObject方法

当一个对象被反序列化的时候,对于客户不应该拥有的对象引用,如果哪个域包含了这样的对象引用,则必须要做保护性拷贝,这是非常重要的

57.必要时提供一个readResolve方法

readResolve方法不仅仅对于singleton对象是必要的,而且对于所有其他的实例受控的(instance-controlled)类也是必需的
readResolve方法的第二个用法是,就像在第56条中建议的那样,作为保护性的readObject方法的一种保守的替代选择
尽管保护性readResolve模式并没有被广泛使用,但是它值得认真考虑
readResolve方法的可访问性(accessibility)是非常重要的
  • Copyrights © 2015-2025 Immanuel
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

微信