面向对象vs面向过程(洗衣服)
封装:
就是把内部的东西保护起来,不被外界所看到.
继承:
就是用于类的扩展
多态:
概念:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说:就是用基类的引用指向子类的对象。
类与对象中:
共性的称为类;
个性的称为对象.
优点:消除耦合,减小因需求变化引起代码僵化。
优点:降低了程序各部分之间的耦合性,其适应性、灵活性、稳定性都比较好。当已有软件系统需要增加新的功能时,不需要对作为系统基础的抽象层进行修改,只需要在原有基础上附加新的模块就能实现所需要添加的功能。增加的新模块对原有的模块完全没有影响或影响很小,这样就无须为原有模块进行重新测试。
优点:可以很容易的实现同一父类下各个子类的互换,而客户端可以毫不察觉。
高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。另一种表述为: 要针对接口编程,不要针对实现编程。
优点:人的思维本身实际上就是很抽象的,我们分析问题的时候不是一下子就考虑到细节,而是很抽象的将整个问题都构思出来,所以面向抽象设计是符合人的思维的。另外这个原则会很好的支持(开闭原则)OCP,面向抽象的设计使我们能够不必太多依赖于实现,这样扩展就成为了可能。
优点:会使一个软件系统功能扩展时,修改的压力不会传到别的对象那里。
优点:消除耦合。
分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。
面对高并发的读写访问,当数据库master服务器无法承载写操作压力时,不管如何扩展slave服务器,此时都没有意义了。
因此,我们必须换一种思路,对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库。
与分表策略相似,分库可以采用通过一个关键字取模的方式,来对数据访问进行路由
能不分就不分
并不是所有表都需要切分,主要还是看数据的增长速度。切分后在某种程度上提升了业务的复杂程度。不到万不得已不要轻易使用分库分表这个“大招”,避免“过度设计”和“过早优化”。分库分表之前,先尽力做力所能及的优化:升级硬件、升级网络、读写分离、索引优化等。当数据量达到单表瓶颈后,在考虑分库分表。
数据量过大,正常运维影响业务访问
这里的运维是指:对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO
对一个很大的表做DDL,MYSQL会锁住整个表,这个时间会很长,这段时间业务不能访问此表,影响很大。
大表经常访问和更新,就更有可能出现锁等待。
随着业务发展,需要对某些字段垂直拆分
这里就不举例了。在实际业务中都可能会碰到,有些不经常访问或者更新频率低的字段应该从大表中分离出去。
数据量快速增长
随着业务的快速发展,单表中的数据量会持续增长,当性能接近瓶颈时,就需要考虑水平切分,做分库分表了。
设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结,其中最出名的当属 Gang of Four (GoF) 的分类了,他们将设计模式分类为 23 种经典的模式,根据用途我们又可以分为三大类,分别为创建型模式、结构型模式和行为型模式。
创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。
和名字一样简单,非常简单,直接上代码吧:
public class FoodFactory {
public static Food makeFood(String name) {
if (name.equals("noodle")) {
Food noodle = new LanZhouNoodle();
noodle.addSpicy("more");
return noodle;
} else if (name.equals("chicken")) {
Food chicken = new HuangMenChicken();
chicken.addCondiment("potato");
return chicken;
} else {
return null;
}
}
}
其中,LanZhouNoodle 和 HuangMenChicken 都继承自 Food。
简单地说,简单工厂模式通常就是这样,一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。
我们强调职责单一 原则,一个类只提供一种功能,FoodFactory 的功能就是只要负责生产各种 Food。
简单工厂模式很简单,如果它能满足我们的需要,我觉得就不要折腾了。之所以需要引入工厂模式,是因为我们往往需要使用两个或两个以上的工厂。
public interface FoodFactory {
Food makeFood(String name);
}
public class ChineseFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new ChineseFoodA();
} else if (name.equals("B")) {
return new ChineseFoodB();
} else {
return null;
}
}
}
public class AmericanFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new AmericanFoodA();
} else if (name.equals("B")) {
return new AmericanFoodB();
} else {
return null;
}
}
}
其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。
客户端调用:
public class APP {
public static void main(String[] args) {
// 先选择一个具体的工厂
FoodFactory factory = new ChineseFoodFactory();
// 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
Food food = factory.makeFood("A");
}
}
虽然都是调用 makeFood(“A”) 制作 A 类食物,但是,不同的工厂生产出来的完全不一样。
第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。
核心在于,我们需要在第一步选好我们需要的工厂 。比如,我们有 LogFactory 接口,实现类有 FileLogFactory 和 KafkaLogFactory,分别对应将日志写入文件和写入 Kafka 中,显然,我们客户端第一步就需要决定到底要实例化 FileLogFactory 还是 KafkaLogFactory,这将决定之后的所有的操作。
虽然简单,不过我也把所有的构件都画到一张图上,这样读者看着比较清晰:
当涉及到产品族 的时候,就需要引入抽象工厂模式了。
一个经典的例子是造一台电脑。我们先不引入抽象工厂模式,看看怎么实现。
因为电脑是由许多的构件组成的,我们将 CPU 和主板进行抽象,然后 CPU 由 CPUFactory 生产,主板由 MainBoardFactory 生产,然后,我们再将 CPU 和主板搭配起来组合在一起,如下图:
这个时候的客户端调用是这样的:
// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();
// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();
// 组装 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);
单独看 CPU 工厂和主板工厂,它们分别是前面我们说的工厂模式 。这种方式也容易扩展,因为要给电脑加硬盘的话,只需要加一个 HardDiskFactory 和相应的实现即可,不需要修改现有的工厂。
但是,这种方式有一个问题,那就是如果 Intel 家产的 CPU 和 AMD 产的主板不能兼容使用 ,那么这代码就容易出错,因为客户端并不知道它们不兼容,也就会错误地出现随意组合。
下面就是我们要说的产品族 的概念,它代表了组成某个产品的一系列附件的集合:
当涉及到这种产品族的问题的时候,就需要抽象工厂模式来支持了。我们不再定义 CPU 工厂、主板工厂、硬盘工厂、显示屏工厂等等,我们直接定义电脑工厂,每个电脑工厂负责生产所有的设备,这样能保证肯定不存在兼容问题。
这个时候,对于客户端来说,不再需要单独挑选 CPU厂商、主板厂商、硬盘厂商等,直接选择一家品牌工厂,品牌工厂会负责生产所有的东西,而且能保证肯定是兼容可用的。
public static void main(String[] args) {
// 第一步就要选定一个“大厂”
ComputerFactory cf = new AmdFactory();
// 从这个大厂造 CPU
CPU cpu = cf.makeCPU();
// 从这个大厂造主板
MainBoard board = cf.makeMainBoard();
// 从这个大厂造硬盘
HardDisk hardDisk = cf.makeHardDisk();
// 将同一个厂子出来的 CPU、主板、硬盘组装在一起
Computer result = new Computer(cpu, board, hardDisk);
}
当然,抽象工厂的问题也是显而易见的,比如我们要加个显示器,就需要修改所有的工厂,给所有的工厂都加上制造显示器的方法。这有点违反了对修改关闭,对扩展开放 这个设计原则。
单例模式用得最多,错得最多。
饿汉模式最简单:
public class Singleton {
// 首先,将 new Singleton() 堵死
private Singleton() {};
// 创建私有静态实例,意味着这个类第一次使用的时候就会进行创建
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
// 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
// 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
public static Date getDate(String mode) {return new Date();}
}
很多人都能说出饿汉模式的缺点,可是我觉得生产过程中,很少碰到这种情况:你定义了一个单例的类,不需要其实例,可是你却把一个或几个你会用到的静态方法塞到这个类中。
饱汉模式最容易出错:
public class Singleton {
// 首先,也是先堵死 new Singleton() 这条路
private Singleton() {}
// 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
// 加锁
synchronized (Singleton.class) {
// 这一次判断也是必须的,不然会有并发问题
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查,指的是两次检查 instance 是否为 null。
volatile 在这里是需要的,希望能引起读者的关注。
很多人不知道怎么写,直接就在 getInstance() 方法签名上加上 synchronized,这就不多说了,性能太差。
嵌套类最经典,以后大家就用它吧:
public class Singleton3 {
private Singleton3() {}
// 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
private static class Holder {
private static Singleton3 instance = new Singleton3();
}
public static Singleton3 getInstance() {
return Holder.instance;
}
}
注意,很多人都会把这个嵌套类 说成是静态内部类 ,严格地说,内部类和嵌套类是不一样的,它们能访问的外部类权限也是不一样的。
最后,我们说一下枚举,枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。
虽然我们平时很少看到用枚举来实现单例,但是在 RxJava 的源码中,有很多地方都用了枚举来实现单例。
经常碰见的 XxxBuilder 的类,通常都是建造者模式的产物。建造者模式其实有很多的变种,但是对于客户端来说,我们的使用通常都是一个模式的:
Food food = new FoodBuilder().a().b().c().build();
Food food = Food.builder().a().b().c().build();
套路就是先 new 一个 Builder,然后可以链式地调用一堆方法,最后再调用一次 build() 方法,我们需要的对象就有了。
来一个中规中矩的建造者模式:
class User {
// 下面是“一堆”的属性
private String name;
private String password;
private String nickName;
private int age;
// 构造方法私有化,不然客户端就会直接调用构造方法了
private User(String name, String password, String nickName, int age) {
this.name = name;
this.password = password;
this.nickName = nickName;
this.age = age;
}
// 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,
// 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
// 下面是和 User 一模一样的一堆属性
private String name;
private String password;
private String nickName;
private int age;
private UserBuilder() {
}
// 链式调用设置各个属性值,返回 this,即 UserBuilder
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。
// 当然,可以在 “复制” 之前做点检验
public User build() {
if (name == null || password == null) {
throw new RuntimeException("用户名和密码必填");
}
if (age <= 0 || age >= 150) {
throw new RuntimeException("年龄不合法");
}
// 还可以做赋予”默认值“的功能
if (nickName == null) {
nickName = name;
}
return new User(name, password, nickName, age);
}
}
}
核心是:先把所有的属性都设置给 Builder,然后 build() 方法的时候,将这些属性复制 给实际产生的对象。
看看客户端的调用:
public class APP {
public static void main(String[] args) {
User d = User.builder()
.name("foo")
.password("pAss12345")
.age(25)
.build();
}
}
说实话,建造者模式的链式 写法很吸引人,但是,多写了很多“无用”的 builder 的代码,感觉这个模式没什么用。不过,当属性很多,而且有些必填,有些选填的时候,这个模式会使代码清晰很多。我们可以在 Builder 的构造方法 中强制让调用者提供必填字段,还有,在 build() 方法中校验各个参数比在 User 的构造方法中校验,代码要优雅一些。
题外话,强烈建议读者使用 lombok,用了 lombok 以后,上面的一大堆代码会变成如下这样:
@Builderclass User { private String name; private String password; private String nickName; private int age;}
怎么样,省下来的时间是不是又可以干点别的了。
当然,如果你只是想要链式写法,不想要建造者模式,有个很简单的办法,User 的 getter 方法不变,所有的 setter 方法都让其 return this 就可以了,然后就可以像下面这样调用:
User user = new User().setName("").setPassword("").setAge(20);
很多人是这么用的,但是笔者觉得其实这种写法非常地不优雅,不是很推荐使用。
这是我要说的创建型模式的最后一个设计模式了。
原型模式很简单:有一个原型实例 ,基于这个原型实例产生新的实例,也就是“克隆”了。
Object 类中有一个 clone() 方法,它用于生成一个新的对象,当然,如果我们要调用这个方法,java 要求我们的类必须先实现 Cloneable 接口 ,此接口没有定义任何方法,但是不这么做的话,在 clone() 的时候,会抛出 CloneNotSupportedException 异常。
protected native Object clone() throws CloneNotSupportedException;
java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。
原型模式了解到这里我觉得就够了,各种变着法子说这种代码或那种代码是原型模式,没什么意义。
创建型模式总体上比较简单,它们的作用就是为了产生实例对象,算是各种工作的第一步了,因为我们写的是面向对象 的代码,所以我们第一步当然是需要创建一个对象了。
简单工厂模式最简单;工厂模式在简单工厂模式的基础上增加了选择工厂的维度,需要第一步选择合适的工厂;抽象工厂模式有产品族的概念,如果各个产品是存在兼容性问题的,就要用抽象工厂模式。单例模式就不说了,为了保证全局使用的是同一对象,一方面是安全性考虑,一方面是为了节省资源;建造者模式专门对付属性很多的那种类,为了让代码更优美;原型模式用得最少,了解和 Object 类中的 clone() 方法相关的知识即可。
前面创建型模式介绍了创建对象的一些设计模式,这节介绍的结构型模式旨在通过改变代码结构来达到解耦的目的,使得我们的代码容易维护和扩展。
第一个要介绍的代理模式是最常使用的模式之一了,用一个代理来隐藏具体实现类的实现细节,通常还用于在真实的实现的前后添加一部分逻辑。
既然说是代理 ,那就要对客户端隐藏真实实现,由代理来负责客户端的所有请求。当然,代理只是个代理,它不会完成实际的业务逻辑,而是一层皮而已,但是对于客户端来说,它必须表现得就是客户端需要的真实实现。
理解代理 这个词,这个模式其实就简单了。
public interface FoodService {
Food makeChicken();
Food makeNoodle();
}
public class FoodServiceImpl implements FoodService {
public Food makeChicken() {
Food f = new Chicken()
f.setChicken("1kg");
f.setSpicy("1g");
f.setSalt("3g");
return f;
}
public Food makeNoodle() {
Food f = new Noodle();
f.setNoodle("500g");
f.setSalt("5g");
return f;
}
}
// 代理要表现得“就像是”真实实现类,所以需要实现 FoodService
public class FoodServiceProxy implements FoodService {
// 内部一定要有一个真实的实现类,当然也可以通过构造方法注入
private FoodService foodService = new FoodServiceImpl();
public Food makeChicken() {
System.out.println("我们马上要开始制作鸡肉了");
// 如果我们定义这句为核心代码的话,那么,核心代码是真实实现类做的,
// 代理只是在核心代码前后做些“无足轻重”的事情
Food food = foodService.makeChicken();
System.out.println("鸡肉制作完成啦,加点胡椒粉"); // 增强
food.addCondiment("pepper");
return food;
}
public Food makeNoodle() {
System.out.println("准备制作拉面~");
Food food = foodService.makeNoodle();
System.out.println("制作完成啦")
return food;
}
}
客户端调用,注意,我们要用代理来实例化接口:
// 这里用代理类来实例化
FoodService foodService = new FoodServiceProxy();
foodService.makeChicken();
我们发现没有,代理模式说白了就是做 “方法包装” 或做 “方法增强” 。在面向切面编程中,其实就是动态代理的过程。比如 Spring 中,我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理,然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。
说到动态代理,又可以展开说,Spring 中实现动态代理有两种,一种是如果我们的类定义了接口,如 UserService 接口和 UserServiceImpl 实现,那么采用 JDK 的动态代理,感兴趣的读者可以去看看 java.lang.reflect.Proxy 类的源码;另一种是我们自己没有定义接口的,Spring 会采用 CGLIB 进行动态代理,它是一个 jar 包,性能还不错。
说完代理模式,说适配器模式,是因为它们很相似,这里可以做个比较。
适配器模式做的就是,有一个接口需要实现,但是我们现成的对象都不满足,需要加一层适配器来进行适配。
适配器模式总体来说分三种:默认适配器模式、对象适配器模式、类适配器模式。先不急着分清楚这几个,先看看例子再说。
首先,我们先看看最简单的适配器模式默认适配器模式(Default Adapter) 是怎么样的。
我们用 Appache commons-io 包中的 FileAlterationListener 做例子,此接口定义了很多的方法,用于对文件或文件夹进行监控,一旦发生了对应的操作,就会触发相应的方法。
public interface FileAlterationListener {
void onStart(final FileAlterationObserver observer);
void onDirectoryCreate(final File directory);
void onDirectoryChange(final File directory);
void onDirectoryDelete(final File directory);
void onFileCreate(final File file);
void onFileChange(final File file);
void onFileDelete(final File file);
void onStop(final FileAlterationObserver observer);
}
此接口的一大问题是抽象方法太多了,如果我们要用这个接口,意味着我们要实现每一个抽象方法,如果我们只是想要监控文件夹中的文件创建 和文件删除 事件,可是我们还是不得不实现所有的方法,很明显,这不是我们想要的。
所以,我们需要下面的一个适配器 ,它用于实现上面的接口,但是所有的方法都是空方法 ,这样,我们就可以转而定义自己的类来继承下面这个类即可。
public class FileAlterationListenerAdaptor implements FileAlterationListener {
public void onStart(final FileAlterationObserver observer) {
}
public void onDirectoryCreate(final File directory) {
}
public void onDirectoryChange(final File directory) {
}
public void onDirectoryDelete(final File directory) {
}
public void onFileCreate(final File file) {
}
public void onFileChange(final File file) {
}
public void onFileDelete(final File file) {
}
public void onStop(final FileAlterationObserver observer) {
}
}
比如我们可以定义以下类,我们仅仅需要实现我们想实现的方法就可以了:
public class FileMonitor extends FileAlterationListenerAdaptor {
public void onFileCreate(final File file) {
// 文件创建
doSomething();
}
public void onFileDelete(final File file) {
// 文件删除
doSomething();
}
}
当然,上面说的只是适配器模式的其中一种,也是最简单的一种,无需多言。下面,再介绍**“正统的”** 适配器模式。
来看一个《Head First 设计模式》中的一个例子,我稍微修改了一下,看看怎么将鸡适配成鸭,这样鸡也能当鸭来用。因为,现在鸭这个接口,我们没有合适的实现类可以用,所以需要适配器。
public interface Duck {
public void quack(); // 鸭的呱呱叫
public void fly(); // 飞
}
public interface Cock {
public void gobble(); // 鸡的咕咕叫
public void fly(); // 飞
}
public class WildCock implements Cock {
public void gobble() {
System.out.println("咕咕叫");
}
public void fly() {
System.out.println("鸡也会飞哦");
}
}
鸭接口有 fly() 和 quare() 两个方法,鸡 Cock 如果要冒充鸭,fly() 方法是现成的,但是鸡不会鸭的呱呱叫,没有 quack() 方法。这个时候就需要适配了:
// 毫无疑问,首先,这个适配器肯定需要 implements Duck,这样才能当做鸭来用
public class CockAdapter implements Duck {
Cock cock;
// 构造方法中需要一个鸡的实例,此类就是将这只鸡适配成鸭来用
public CockAdapter(Cock cock) {
this.cock = cock;
}
// 实现鸭的呱呱叫方法
@Override
public void quack() {
// 内部其实是一只鸡的咕咕叫
cock.gobble();
}
@Override
public void fly() {
cock.fly();
}
}
客户端调用很简单了:
public static void main(String[] args) { // 有一只野鸡 Cock wildCock = new WildCock(); // 成功将野鸡适配成鸭 Duck duck = new CockAdapter(wildCock); ...}
到这里,大家也就知道了适配器模式是怎么回事了。无非是我们需要一只鸭,但是我们只有一只鸡,这个时候就需要定义一个适配器,由这个适配器来充当鸭,但是适配器里面的方法还是由鸡来实现的。
我们用一个图来简单说明下:
上图应该还是很容易理解的,我就不做更多的解释了。下面,我们看看类适配模式怎么样的。
废话少说,直接上图:
看到这个图,大家应该很容易理解的吧,通过继承的方法,适配器自动获得了所需要的大部分方法。这个时候,客户端使用更加简单,直接 Target t = new SomeAdapter();
就可以了。
类适配和对象适配的异同
一个采用继承,一个采用组合;
类适配属于静态实现,对象适配属于组合的动态实现,对象适配需要多实例化一个对象。
总体来说,对象适配用得比较多。
适配器模式和代理模式的异同
比较这两种模式,其实是比较对象适配器模式和代理模式,在代码结构上,它们很相似,都需要一个具体的实现类的实例。但是它们的目的不一样,代理模式做的是增强原方法的活;适配器做的是适配的活,为的是提供“把鸡包装成鸭,然后当做鸭来使用”,而鸡和鸭它们之间原本没有继承关系。
理解桥梁模式,其实就是理解代码抽象和解耦。
我们首先需要一个桥梁,它是一个接口,定义提供的接口方法。
public interface DrawAPI { public void draw(int radius, int x, int y);}
然后是一系列实现类:
public class RedPen implements DrawAPI {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class GreenPen implements DrawAPI {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class BluePen implements DrawAPI {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
定义一个抽象类,此类的实现类都需要使用 DrawAPI:
public abstract class Shape {
protected DrawAPI drawAPI;
protected Shape(DrawAPI drawAPI) {
this.drawAPI = drawAPI;
}
public abstract void draw();
}
定义抽象类的子类:
// 圆形
public class Circle extends Shape {
private int radius;
public Circle(int radius, DrawAPI drawAPI) {
super(drawAPI);
this.radius = radius;
}
public void draw() {
drawAPI.draw(radius, 0, 0);
}
}
// 长方形
public class Rectangle extends Shape {
private int x;
private int y;
public Rectangle(int x, int y, DrawAPI drawAPI) {
super(drawAPI);
this.x = x;
this.y = y;
}
public void draw() {
drawAPI.draw(0, x, y);
}
}
最后,我们来看客户端演示:
public static void main(String[] args) {
Shape greenCircle = new Circle(10, new GreenPen());
Shape redRectangle = new Rectangle(4, 8, new RedPen());
greenCircle.draw();
redRectangle.draw();
}
可能大家看上面一步步还不是特别清晰,我把所有的东西整合到一张图上:
这回大家应该就知道抽象在哪里,怎么解耦了吧。桥梁模式的优点也是显而易见的,就是非常容易进行扩展。
本节引用了这里的例子,并对其进行了修改。
要把装饰模式说清楚明白,不是件容易的事情。也许读者知道 Java IO 中的几个类是典型的装饰模式的应用,但是读者不一定清楚其中的关系,也许看完就忘了,希望看完这节后,读者可以对其有更深的感悟。
首先,我们先看一个简单的图,看这个图的时候,了解下层次结构就可以了:
我们来说说装饰模式的出发点,从图中可以看到,接口 Component
其实已经有了 ConcreteComponentA
和 ConcreteComponentB
两个实现类了,但是,如果我们要增强 这两个实现类的话,我们就可以采用装饰模式,用具体的装饰器来装饰 实现类,以达到增强的目的。
从名字来简单解释下装饰器。既然说是装饰,那么往往就是添加小功能 这种,而且,我们要满足可以添加多个小功能。最简单的,代理模式就可以实现功能的增强,但是代理不容易实现多个功能的增强,当然你可以说用代理包装代理的多层包装方式,但是那样的话代码就复杂了。
首先明白一些简单的概念,从图中我们看到,所有的具体装饰者们 ConcreteDecorator * 都可以作为 Component 来使用,因为它们都实现了 Component 中的所有接口。它们和 Component 实现类 ConcreteComponent* 的区别是,它们只是装饰者,起装饰 作用,也就是即使它们看上去牛逼轰轰,但是它们都只是在具体的实现中加了层皮来装饰 而已。
注意这段话中混杂在各个名词中的 Component 和 Decorator,别搞混了。
下面来看看一个例子,先把装饰模式弄清楚,然后再介绍下 java io 中的装饰模式的应用。
最近大街上流行起来了“快乐柠檬”,我们把快乐柠檬的饮料分为三类:红茶、绿茶、咖啡,在这三大类的基础上,又增加了许多的口味,什么金桔柠檬红茶、金桔柠檬珍珠绿茶、芒果红茶、芒果绿茶、芒果珍珠红茶、烤珍珠红茶、烤珍珠芒果绿茶、椰香胚芽咖啡、焦糖可可咖啡等等,每家店都有很长的菜单,但是仔细看下,其实原料也没几样,但是可以搭配出很多组合,如果顾客需要,很多没出现在菜单中的饮料他们也是可以做的。
在这个例子中,红茶、绿茶、咖啡是最基础的饮料,其他的像金桔柠檬、芒果、珍珠、椰果、焦糖等都属于装饰用的。当然,在开发中,我们确实可以像门店一样,开发这些类:LemonBlackTea、LemonGreenTea、MangoBlackTea、MangoLemonGreenTea…但是,很快我们就发现,这样子干肯定是不行的,这会导致我们需要组合出所有的可能,而且如果客人需要在红茶中加双份柠檬怎么办?三份柠檬怎么办?
不说废话了,上代码。
首先,定义饮料抽象基类:
public abstract class Beverage { // 返回描述 public abstract String getDescription(); // 返回价格 public abstract double cost();}
然后是三个基础饮料实现类,红茶、绿茶和咖啡:
public class BlackTea extends Beverage { public String getDescription() { return "红茶"; } public double cost() { return 10; }}public class GreenTea extends Beverage { public String getDescription() { return "绿茶"; } public double cost() { return 11; }}...// 咖啡省略
定义调料,也就是装饰者的基类,此类必须继承自 Beverage:
// 调料public abstract class Condiment extends Beverage {}
然后我们来定义柠檬、芒果等具体的调料,它们属于装饰者,毫无疑问,这些调料肯定都需要继承调料 Condiment 类:
public class Lemon extends Condiment {
private Beverage bevarage;
// 这里很关键,需要传入具体的饮料,如需要传入没有被装饰的红茶或绿茶,
// 当然也可以传入已经装饰好的芒果绿茶,这样可以做芒果柠檬绿茶
public Lemon(Beverage bevarage) {
this.bevarage = bevarage;
}
public String getDescription() {
// 装饰
return bevarage.getDescription() + ", 加柠檬";
}
public double cost() {
// 装饰
return beverage.cost() + 2; // 加柠檬需要 2 元
}
}
public class Mango extends Condiment {
private Beverage bevarage;
public Mango(Beverage bevarage) {
this.bevarage = bevarage;
}
public String getDescription() {
return bevarage.getDescription() + ", 加芒果";
}
public double cost() {
return beverage.cost() + 3; // 加芒果需要 3 元
}
}
...// 给每一种调料都加一个类
看客户端调用:
public static void main(String[] args) {
// 首先,我们需要一个基础饮料,红茶、绿茶或咖啡
Beverage beverage = new GreenTea();
// 开始装饰
beverage = new Lemon(beverage); // 先加一份柠檬
beverage = new Mongo(beverage); // 再加一份芒果
System.out.println(beverage.getDescription() + " 价格:¥" + beverage.cost());
//"绿茶, 加柠檬, 加芒果 价格:¥16"
}
如果我们需要 芒果-珍珠-双份柠檬-红茶 :
Beverage beverage = new Mongo(new Pearl(new Lemon(new Lemon(new BlackTea()))));
是不是很变态?
看看下图可能会清晰一些:
到这里,大家应该已经清楚装饰模式了吧。
下面,我们再来说说 java IO 中的装饰模式。看下图 InputStream 派生出来的部分类:
我们知道 InputStream 代表了输入流,具体的输入来源可以是文件(FileInputStream)、管道(PipedInputStream)、数组(ByteArrayInputStream)等,这些就像前面奶茶的例子中的红茶、绿茶,属于基础输入流。
FilterInputStream 承接了装饰模式的关键节点,它的实现类是一系列装饰器,比如 BufferedInputStream 代表用缓冲来装饰,也就使得输入流具有了缓冲的功能,LineNumberInputStream 代表用行号来装饰,在操作的时候就可以取得行号了,DataInputStream 的装饰,使得我们可以从输入流转换为 java 中的基本类型值。
当然,在 java IO 中,如果我们使用装饰器的话,就不太适合面向接口编程了,如:
InputStream inputStream = new LineNumberInputStream(new BufferedInputStream(new FileInputStream("")));
这样的结果是,InputStream 还是不具有读取行号的功能,因为读取行号的方法定义在 LineNumberInputStream 类中。
我们应该像下面这样使用:
DataInputStream is = new DataInputStream(
new BufferedInputStream(
new FileInputStream("")));
所以说嘛,要找到纯的严格符合设计模式的代码还是比较难的。
门面模式(也叫外观模式,Facade Pattern)在许多源码中有使用,比如 slf4j 就可以理解为是门面模式的应用。这是一个简单的设计模式,我们直接上代码再说吧。
首先,我们定义一个接口:
public interface Shape {
void draw();
}
定义几个实现类:
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Circle::draw()");
}
}
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Rectangle::draw()");
}
}
客户端调用:
public static void main(String[] args) {
// 画一个圆形
Shape circle = new Circle();
circle.draw();
// 画一个长方形
Shape rectangle = new Rectangle();
rectangle.draw();
}
以上是我们常写的代码,我们需要画圆就要先实例化圆,画长方形就需要先实例化一个长方形,然后再调用相应的 draw() 方法。
下面,我们看看怎么用门面模式来让客户端调用更加友好一些。
我们先定义一个门面:
public class ShapeMaker {
private Shape circle;
private Shape rectangle;
private Shape square;
public ShapeMaker() {
circle = new Circle();
rectangle = new Rectangle();
square = new Square();
}
/**
* 下面定义一堆方法,具体应该调用什么方法,由这个门面来决定
*/
public void drawCircle(){
circle.draw();
}
public void drawRectangle(){
rectangle.draw();
}
public void drawSquare(){
square.draw();
}
}
看看现在客户端怎么调用:
public static void main(String[] args) {
ShapeMaker shapeMaker = new ShapeMaker();
// 客户端调用现在更加清晰了
shapeMaker.drawCircle();
shapeMaker.drawRectangle();
shapeMaker.drawSquare();
}
门面模式的优点显而易见,客户端不再需要关注实例化时应该使用哪个实现类,直接调用门面提供的方法就可以了,因为门面类提供的方法的方法名对于客户端来说已经很友好了。
组合模式用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。
直接看一个例子吧,每个员工都有姓名、部门、薪水这些属性,同时还有下属员工集合(虽然可能集合为空),而下属员工和自己的结构是一样的,也有姓名、部门这些属性,同时也有他们的下属员工集合。
public class Employee {
private String name;
private String dept;
private int salary;
private List subordinates; // 下属
public Employee(String name,String dept, int sal) {
this.name = name;
this.dept = dept;
this.salary = sal;
subordinates = new ArrayList();
}
public void add(Employee e) {
subordinates.add(e);
}
public void remove(Employee e) {
subordinates.remove(e);
}
public List getSubordinates(){
return subordinates;
}
public String toString(){
return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]");
}
}
通常,这种类需要定义 add(node)、remove(node)、getChildren() 这些方法。
这说的其实就是组合模式,这种简单的模式我就不做过多介绍了,相信各位读者也不喜欢看我写废话。
英文是 Flyweight Pattern,不知道是谁最先翻译的这个词,感觉这翻译真的不好理解,我们试着强行关联起来吧。Flyweight 是轻量级的意思,享元分开来说就是 共享 元器件,也就是复用已经生成的对象,这种做法当然也就是轻量级的了。
复用对象最简单的方式是,用一个 HashMap 来存放每次新生成的对象。每次需要一个对象的时候,先到 HashMap 中看看有没有,如果没有,再生成新的对象,然后将这个对象放入 HashMap 中。
这种简单的代码我就不演示了。
前面,我们说了代理模式、适配器模式、桥梁模式、装饰模式、门面模式、组合模式和享元模式。读者是否可以分别把这几个模式说清楚了呢?在说到这些模式的时候,心中是否有一个清晰的图或处理流程在脑海里呢?
代理模式是做方法增强的,适配器模式是把鸡包装成鸭这种用来适配接口的,桥梁模式做到了很好的解耦,装饰模式从名字上就看得出来,适合于装饰类或者说是增强类的场景,门面模式的优点是客户端不需要关心实例化过程,只要调用需要的方法即可,组合模式用于描述具有层次结构的数据,享元模式是为了在特定的场景中缓存已经创建的对象,用于提高性能。
行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰。
策略模式太常用了,所以把它放到最前面进行介绍。它比较简单,我就不废话,直接用代码说事吧。
下面设计的场景是,我们需要画一个图形,可选的策略就是用红色笔来画,还是绿色笔来画,或者蓝色笔来画。
首先,先定义一个策略接口:
public interface Strategy {
public void draw(int radius, int x, int y);
}
然后我们定义具体的几个策略:
public class RedPen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class GreenPen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
public class BluePen implements Strategy {
@Override
public void draw(int radius, int x, int y) {
System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);
}
}
使用策略的类:
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeDraw(int radius, int x, int y){
return strategy.draw(radius, x, y);
}
}
客户端演示:
public static void main(String[] args) {
Context context = new Context(new BluePen()); // 使用绿色笔来画
context.executeDraw(10, 0, 0);
}
放到一张图上,让大家看得清晰些:
这个时候,大家有没有联想到结构型模式中的桥梁模式,它们其实非常相似,我把桥梁模式的图拿过来大家对比下:
要我说的话,它们非常相似,桥梁模式在左侧加了一层抽象而已。桥梁模式的耦合更低,结构更复杂一些。
观察者模式对于我们来说,真是再简单不过了。无外乎两个操作,观察者订阅自己关心的主题和主题有数据变化后通知观察者们。
首先,需要定义主题,每个主题需要持有观察者列表的引用,用于在数据变更的时候通知各个观察者:
public class Subject {
private List<Observer> observers = new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
// 数据已变更,通知观察者们
notifyAllObservers();
}
// 注册观察者
public void attach(Observer observer) {
observers.add(observer);
}
// 通知观察者们
public 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 发布出去的操作一定要小心
this.subject.attach(this);
}
// 该方法由主题类在数据变更的时候进行调用
@Override
public void update() {
String result = Integer.toBinaryString(subject.getState());
System.out.println("订阅的数据发生变化,新的数据处理为二进制值为:" + result);
}
}
public class HexaObserver extends Observer {
public HexaObserver(Subject subject) {
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
String result = Integer.toHexString(subject.getState()).toUpperCase();
System.out.println("订阅的数据发生变化,新的数据处理为十六进制值为:" + result);
}
}
客户端使用也非常简单:
public static void main(String[] args) {
// 先定义一个主题
Subject subject1 = new Subject();
// 定义观察者
new BinaryObserver(subject1);
new HexaObserver(subject1);
// 模拟数据变更,这个时候,观察者们的 update 方法将会被调用
subject.setState(11);
}
output:
订阅的数据发生变化,新的数据处理为二进制值为:1011
订阅的数据发生变化,新的数据处理为十六进制值为:B
当然,jdk 也提供了相似的支持,具体的大家可以参考 java.util.Observable 和 java.util.Observer 这两个类。
实际生产过程中,观察者模式往往用消息中间件来实现,如果要实现单机观察者模式,笔者建议读者使用 Guava 中的 EventBus,它有同步实现也有异步实现,本文主要介绍设计模式,就不展开说了。
还有,即使是上面的这个代码,也会有很多变种,大家只要记住核心的部分,那就是一定有一个地方存放了所有的观察者,然后在事件发生的时候,遍历观察者,调用它们的回调函数。
责任链通常需要先建立一个单向链表,然后调用方只需要调用头部节点就可以了,后面会自动流转下去。比如流程审批就是一个很好的例子,只要终端用户提交申请,根据申请的内容信息,自动建立一条责任链,然后就可以开始流转了。
有这么一个场景,用户参加一个活动可以领取奖品,但是活动需要进行很多的规则校验然后才能放行,比如首先需要校验用户是否是新用户、今日参与人数是否有限额、全场参与人数是否有限额等等。设定的规则都通过后,才能让用户领走奖品。
如果产品给你这个需求的话,我想大部分人一开始肯定想的就是,用一个 List 来存放所有的规则,然后 foreach 执行一下每个规则就好了。不过,读者也先别急,看看责任链模式和我们说的这个有什么不一样?
首先,我们要定义流程上节点的基类:
public abstract class RuleHandler {
// 后继节点
protected RuleHandler successor;
public abstract void apply(Context context);
public void setSuccessor(RuleHandler successor) {
this.successor = successor;
}
public RuleHandler getSuccessor() {
return successor;
}
}
接下来,我们需要定义具体的每个节点了。
校验用户是否是新用户:
public class NewUserRuleHandler extends RuleHandler {
public void apply(Context context) {
if (context.isNewUser()) {
// 如果有后继节点的话,传递下去
if (this.getSuccessor() != null) {
this.getSuccessor().apply(context);
}
} else {
throw new RuntimeException("该活动仅限新用户参与");
}
}
}
校验用户所在地区是否可以参与:
public class LocationRuleHandler extends RuleHandler {
public void apply(Context context) {
boolean allowed = activityService.isSupportedLocation(context.getLocation);
if (allowed) {
if (this.getSuccessor() != null) {
this.getSuccessor().apply(context);
}
} else {
throw new RuntimeException("非常抱歉,您所在的地区无法参与本次活动");
}
}
}
校验奖品是否已领完:
public class LimitRuleHandler extends RuleHandler {
public void apply(Context context) {
int remainedTimes = activityService.queryRemainedTimes(context); // 查询剩余奖品
if (remainedTimes > 0) {
if (this.getSuccessor() != null) {
this.getSuccessor().apply(userInfo);
}
} else {
throw new RuntimeException("您来得太晚了,奖品被领完了");
}
}
}
客户端:
public static void main(String[] args) {
RuleHandler newUserHandler = new NewUserRuleHandler();
RuleHandler locationHandler = new LocationRuleHandler();
RuleHandler limitHandler = new LimitRuleHandler();
// 假设本次活动仅校验地区和奖品数量,不校验新老用户
locationHandler.setSuccessor(limitHandler);
locationHandler.apply(context);
}
代码其实很简单,就是先定义好一个链表,然后在通过任意一节点后,如果此节点有后继节点,那么传递下去。
至于它和我们前面说的用一个 List 存放需要执行的规则的做法有什么异同,留给读者自己琢磨吧。
在含有继承结构的代码中,模板方法模式是非常常用的。
通常会有一个抽象类:
public abstract class AbstractTemplate {
// 这就是模板方法
public void templateMethod() {
init();
apply(); // 这个是重点
end(); // 可以作为钩子方法
}
protected void init() {
System.out.println("init 抽象层已经实现,子类也可以选择覆写");
}
// 留给子类实现
protected abstract void apply();
protected void end() {
}
}
模板方法中调用了 3 个方法,其中 apply() 是抽象方法,子类必须实现它,其实模板方法中有几个抽象方法完全是自由的,我们也可以将三个方法都设置为抽象方法,让子类来实现。也就是说,模板方法只负责定义第一步应该要做什么,第二步应该做什么,第三步应该做什么,至于怎么做,由子类来实现。
我们写一个实现类:
public class ConcreteTemplate extends AbstractTemplate {
public void apply() {
System.out.println("子类实现抽象方法 apply");
}
public void end() {
System.out.println("我们可以把 method3 当做钩子方法来使用,需要的时候覆写就可以了");
}
}
客户端调用演示:
public static void main(String[] args) {
AbstractTemplate t = new ConcreteTemplate();
// 调用模板方法
t.templateMethod();
}
代码其实很简单,基本上看到就懂了,关键是要学会用到自己的代码中。
废话我就不说了,我们说一个简单的例子。商品库存中心有个最基本的需求是减库存和补库存,我们看看怎么用状态模式来写。
核心在于,我们的关注点不再是 Context 是该进行哪种操作,而是关注在这个 Context 会有哪些操作。
定义状态接口:
public interface State {
public void doAction(Context context);
}
定义减库存的状态:
public class DeductState implements State {
public void doAction(Context context) {
System.out.println("商品卖出,准备减库存");
context.setState(this);
//... 执行减库存的具体操作
}
public String toString() {
return "Deduct State";
}
}
定义补库存状态:
public class RevertState implements State {
public void doAction(Context context) {
System.out.println("给此商品补库存");
context.setState(this);
//... 执行加库存的具体操作
}
public String toString() {
return "Revert State";
}
}
前面用到了 context.setState(this),我们来看看怎么定义 Context 类:
public class Context {
private State state;
private String name;
public Context(String name) {
this.name = name;
}
public void setState(State state) {
this.state = state;
}
public void getState() {
return this.state;
}
}
我们来看下客户端调用,大家就一清二楚了:
public static void main(String[] args) {
// 我们需要操作的是 iPhone X
Context context = new Context("iPhone X");
// 看看怎么进行补库存操作
State revertState = new RevertState();
revertState.doAction(context);
// 同样的,减库存操作也非常简单
State deductState = new DeductState();
deductState.doAction(context);
// 如果需要我们可以获取当前的状态
// context.getState().toString();
}
读者可能会发现,在上面这个例子中,如果我们不关心当前 context 处于什么状态,那么 Context 就可以不用维护 state 属性了,那样代码会简单很多。
不过,商品库存这个例子毕竟只是个例,我们还有很多实例是需要知道当前 context 处于什么状态的。
行为型模式部分介绍了策略模式、观察者模式、责任链模式、模板方法模式和状态模式,其实,经典的行为型模式还包括备忘录模式、命令模式等,但是它们的使用场景比较有限,而且本文篇幅也挺大了,我就不进行介绍了。
Singleton(单态):只允许一个实例
Runtime.getRuntime();
java.awt.Toolkit.getDefaultToolkit();
java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment();
java.awt.Desktop.getDesktop();
Abstract factory(抽象工厂):创建一组有关联的对象实例
java.util.Calendar.getInstance();
java.util.Arrays.asList();
java.util.ResourceBundle.getBundle(String baseName);
java.sql.DriverManager.getConnection(String url);
java.sql.Connection.createStatement();
java.sql.Statement.executeQuery(String sql);
java.text.NumberFormat.getInstance();
javax.xml.transform.TransformerFactory.newInstance();
Factory(工厂方法):按照需求返回一个类型的实例
java.lang.Proxy.newProxyInstance();
java.lang.Object.toString();
java.lang.Class.newInstance();
java.lang.reflect.Array.newInstance();
java.lang.reflect.Constructor.newInstance();
java.lang.Boolean.valueOf(String s);
java.lang.Class.forName(String className);
Builder(建造者):主要用来简化一个复杂的对象的创建
java.lang.StringBuilder.append(Object obj);
java.lang.StringBuffer.append(Object obj);
java.sql.PreparedStatement
javax.swing.GroupLayout.Group.addComponent();
Prototype(原型):使用自己的实例创建另一个实例
java.lang.Object.clone();
java.lang.Cloneable;
Adapter(适配器):个接口或是类变成另外一种
java.util.Arrays.asList();
javax.swing.JTable(TableModel)
java.io.InputStreamReader(InputStream)
java.io.OutputStreamWriter(OutputStream)
javax.xml.bind.annotation.adapters.XmlAdapter.marshal();
javax.xml.bind.annotation.adapters.XmlAdapter.unmarshal();
Composite(组合):让使用者把单独的对象和组合对象混用
javax.swing.JComponent.add(Component comp);
java.awt.Container.add(Component comp);
java.util.Map.putAll(Map m);
java.util.List.addAll(Collection c);
java.util.Set.addAll(Collection c);
Decorator(装饰器):为一个对象动态的加上一系列的动作,而不需要因为这些动作的不同而产生大量的继承类
java.io.BufferedInputStream(InputStream);
java.io.DataInputStream(InputStream);
java.io.BufferedOutputStream(OutputStream);
java.util.zip.ZipOutputStream(OutputStream);
java.util.Collections.checkedList(List list, Class type) ;
Facade(门面):一个简单的接口包状一组组件,接口,抽象或是子系统。
java.lang.Class
javax.faces.webapp.FacesServlet
Flyweight(共享元):有效率地存储大量的小的对象
java.lang.Integer.valueOf(int)
java.lang.Boolean.valueOf(boolean)
java.lang.Byte.valueOf(byte)
java.lang.Character.valueOf(char)
Proxy(代理):用一个简单的对象来代替一个复杂的对象
java.lang.reflect.Proxy
Chain of Responsibility(责任链):一个对象在一个链接传递直到被处理
java.util.logging.Logger.log()
javax.servlet.Filter.doFilter()
Command(命令):将一系列的命令封装成在一个类中
java.lang.Runnable
javax.swing.Action
Interpreter(解释器):定义语言的文法 ,并且建立一个解释器来解释该语言中的句子
java.util.Pattern
java.text.Normalizer
java.text.Format
Iterator(迭代器):对象遍历
java.util.Iterator
java.util.Enumeration
Mediator(中介):用一个中介对象来封装一系列关于对象交互行为.
java.util.Timer
java.util.concurrent.Executor.execute()
java.util.concurrent.ExecutorService#submit()
java.lang.reflect.Method.invoke()
Memento(快照):保存另外一个对象内部状态拷贝的对象
java.util.Date
java.io.Serializable
Observer(观察者):事件监听器
java.util.EventListener
javax.servlet.http.HttpSessionBindingListener
javax.servlet.http.HttpSessionAttributeListener
javax.faces.event.PhaseListener
State(状态):不同的状态,不同的行为;或者说,每个状态有着相应的行为.
java.util.Iterator
javax.faces.lifecycle.LifeCycle.execute()
Strategy(策略):定义一系列的算法,把这些算法一个个封装成单独的类
java.util.Comparator.compare()
javax.servlet.http.HttpServlet
javax.servlet.Filter.doFilter()
Template(模板):定义一个操作中算法的骨架,将一些步骤的执行延迟到其子类中
java.util.Collections.sort()
java.io.InputStream.skip()
java.io.InputStream.read()
java.util.AbstractList.indexOf()
Visitor(访问者):作用于某个对象群中各个对象的操作. 它可以使你在不改变这些对象本身的情况下,定义作用于这些对象的新操作.
javax.lang.model.element.Element 和javax.lang.model.element.ElementVisitor
javax.lang.model.type.TypeMirror 和javax.lang.model.type.TypeVisitor
spring中常用的设计模式达到九种,我们举例说明:
又叫做静态工厂方法(StaticFactory Method)模式,但不属于23种GOF设计模式之一。
简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。如下配置,就是在 HelloItxxz 类中创建一个 itxxzBean。
<beans>
<bean id="singletonBean" class="com.itxxz.HelloItxxz">
<constructor-arg>
<value>Hello! 这是singletonBean!value>
constructor-arg>
bean>
<bean id="itxxzBean" class="com.itxxz.HelloItxxz"
singleton="false">
<constructor-arg>
<value>Hello! 这是itxxzBean! value>
constructor-arg>
bean>
beans>
通常由应用程序直接使用new创建新的对象,为了将对象的创建和使用相分离,采用工厂模式,即应用程序将对象的创建及初始化职责交给工厂对象。
一般情况下,应用程序有自己的工厂对象来创建bean.如果将应用程序自己的工厂对象交给Spring管理,那么Spring管理的就不是普通的bean,而是工厂Bean。
螃蟹就以工厂方法中的静态方法为例讲解一下:import java.util.Random;
public class StaticFactoryBean {
public static Integer createRandom() {
return new Integer(new Random().nextInt());
}
}
建一个config.xm配置文件,将其纳入Spring容器来管理,需要通过factory-method指定静态方法名称
<bean id="random"
class="example.chapter3.StaticFactoryBean" factory-method="createRandom" //createRandom方法必须是static的,才能找到 scope="prototype"
/>
测试:
public static void main(String[] args) {
//调用getBean()时,返回随机数.如果没有指定factory-method,会返回StaticFactoryBean的实例,即返回工厂Bean的实例 XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("config.xml")); System.out.println("我是IT学习者创建的实例:"+factory.getBean("random").toString());
}
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是是任意的java对象。
核心提示点:Spring下默认的bean均为singleton,可以通过singleton=“true|false” 或者 scope=“?”来指定
在Spring的Aop中,使用的Advice(通知)来增强被代理类的功能。Spring实现这一AOP功能的原理就使用代理模式(1、JDK动态代理。2、CGLib字节码生成技术代理。)对类进行方法级别的切面增强,即,生成被代理类的代理类, 并在代理类的方法前,设置拦截器,通过执行拦截器重的内容增强了代理方法的功能,实现的面向切面编程。
Adapter类接口:Target
public interface AdvisorAdapter {
boolean supportsAdvice(Advice advice);
MethodInterceptor getInterceptor(Advisor advisor);
} MethodBeforeAdviceAdapter类,Adapter
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
public boolean supportsAdvice(Advice advice) {
return (advice instanceof MethodBeforeAdvice);
}
public MethodInterceptor getInterceptor(Advisor advisor) {
MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
return new MethodBeforeAdviceInterceptor(advice);
}
}
在我们的项目中遇到这样一个问题:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。我们以往在spring和hibernate框架中总是配置一个数据源,因而sessionFactory的dataSource属性总是指向这个数据源并且恒定不变,所有DAO在使用sessionFactory的时候都是通过这个数据源访问数据库。但是现在,由于项目的需要,我们的DAO在访问sessionFactory的时候都不得不在多个数据源中不断切换,问题就出现了:如何让sessionFactory在执行数据持久化的时候,根据客户的需求能够动态切换不同的数据源?我们能不能在spring的框架下通过少量修改得到解决?是否有什么设计模式可以利用呢?
首先想到在spring的applicationContext中配置所有的dataSource。这些dataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySQL等,也可能是不同的数据源:比如apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi.JndiObjectFactoryBean等。然后sessionFactory根据客户的每次请求,将dataSource属性设置成不同的数据源,以到达切换数据源的目的。
spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。基本上都是动态地给一个对象添加一些额外的职责。
为其他对象提供一种代理以控制对这个对象的访问。 从结构上来看和Decorator模式类似,但Proxy是控制,更像是一种对功能的限制,而Decorator是增加职责。
spring的Proxy模式在aop中有体现,比如JdkDynamicAopProxy和Cglib2AopProxy。
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
spring中Observer模式常用的地方是listener的实现。如ApplicationListener。
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
spring中在实例化对象的时候用到Strategy模式
在SimpleInstantiationStrategy中有如下代码说明了策略模式的使用情况:
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
Template Method模式一般是需要继承的。这里想要探讨另一种对Template Method的理解。spring中的JdbcTemplate,在用这个类时并不想去继承这个类,因为这个类的方法太多,但是我们还是想用到JdbcTemplate已有的稳定的、公用的数据库连接,那么我们怎么办呢?我们可以把变化的东西抽出来作为一个参数传入JdbcTemplate的方法中。但是变化的东西是一段代码,而且这段代码会用到JdbcTemplate中的变量。怎么办?那我们就用回调对象吧。在这个回调对象中定义一个操纵JdbcTemplate中变量的方法,我们去实现这个方法,就把变化的东西集中到这里了。然后我们再传入这个回调对象到JdbcTemplate,从而完成了调用。这可能是Template Method不需要继承的另一种实现方式吧。
以下是一个具体的例子:
JdbcTemplate中的execute方法
public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDataSource());
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null) {
// Extract native JDBC Connection, castable to OracleConnection or the like.
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
else {
// Create close-suppressing Connection proxy, also preparing returned Statements.
conToUse = createConnectionProxy(con);
}
return action.doInConnection(conToUse);
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
}
finally {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
并行和并发的区别:
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
**并行(parallel):**指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。
**并发(concurrency):**指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
线程和进程的区别
线程(thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
以QQ为例:QQ程序是电脑的一个进程,我用QQ给A发语音、给B发消息、接收C传来的文件等等就是线程。
当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
既然是线程安全问题,那么毫无疑问,所有的隐患都是在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行。
如何确保线程安全?
既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?我们说说常见的几种方式
synchronized
synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。
注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。
lock
先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。
进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。
其实在Lock还有几种获取锁的方式,我们这里再说一种,就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。
(1)什么是IO流?
IO指的是Input/Output,IO流:输入输出流。 我们称为数据流。
在Java程序中,对于数据的输入 / 输出操作都是以流的方式进行的。
JDK中提供了各种各样的流类,用以获取不同种类的数据。
常见的2种IO流分类:
1)按流向分:
输入流:程序可以从中读取数据的流
输出流:程序能向其中写入数据的流
2)按数据传输单位分:
字节流:以字节为单位传输数据的流
字符流:以字符为单位传输数据的流
(2)IO流的基类和分类
以下流都是继承四个抽象基类:
字节输入流:InputStream 字符输入流:Reader
字节输出流:OutputStream 字符输出流:Writer
为了更好的学习Java流体系,下面主要先从四个抽象基类开始分析。
InputStream和OutputSteam
InputStream 字节输入流:继承自InputStream的流都是用于向程序中输入数据的。
OutputStream 字节输出流 :继承自OutputStream的流是程序用于向外输出数据的。
下面举个例子来说,继承自这两个流的文件字节操作流FileInputStream和FileOutputStream的使用。
例子:现在有个需求是要将一份音乐文件从e:\src复制到e:\dest。
public static void main(String[] args) {
String srcPath = "D:\\Stream.txt";
String destDir = "E:\\";
File destDirFile = new File(destDir);
if (!destDirFile.exists()) {
destDirFile.mkdir();
}
FileInputStream fInputStream = null;
FileOutputStream fOutputStream = null;
//从Src读:输入流
//写入dest:输出流
//数据类型:视频,只能用字节流
try {
fInputStream = new FileInputStream(srcPath);
fOutputStream = new FileOutputStream(destDirFile.getAbsolutePath()+File.separator+"Stream.txt");
//循环通过输入流,读取数据,边读边写
byte[] buff = new byte[100];
int len=-1;
while ((len=fInputStream.read(buff,0,buff.length))!=-1) {
//返回值:是真实读取到的字节数,如果到了流的末尾,返回-1
//通过输出流写出
fOutputStream.write(buff,0,len);//推荐使用这个方法,读到多少字节就写入多少字节
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
if (fInputStream != null) {
fInputStream.close();
}
if (fOutputStream != null) {
fOutputStream.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Reader 和Writer
Reader 字符输入流,继承自Reader的流都是用于向程序中输入数据的。
Writer 字符输出流,继承Writer的流都是向程序中输出的。
下面同样举个例子来说,继承自这两个流的文件字符操作流FileWriter和FileReader的使用。
例子:用FileWriter和FileReader完成文件的复制[把D盘中的Stream.txt复制到C盘的根目录下]
public static void main(String[] args) {
File filesrc=new File("D:\\Stream.txt");
File filedest=new File("D:\\"+"copy.txt");
try {
Reader in=new FileReader(filesrc);
Writer out=new FileWriter(filedest);
char[]cs=new char[1024];
int len=-1;//实际的位置
while((len=in.read(cs,0,cs.length))!=-1){
out.write(cs,0,len);
}
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
复制的文件是乱码的原因:这是由于电脑本身的txt文件的编码是ANSI,而IDEA的默认编码是UFT-8。解决方法:将文件另存为编码UFT-8的txt
IO缓冲流
Java缓冲流其实本身是不具有IO功能,它是在原始IO流上增加了缓冲功能,使其在传输数据上提高效率,我们也称为装饰流。当需求需要频繁操作读写文件或者传输数据时,使用缓冲流先将数据缓存起来,然后一起写入或读取出来,这样效率会大大提高。
缓冲流四个类:
BufferedInputStream BufferedOutputStream 字节流
BufferedReader BufferedWriter 字符流
我们一般对缓冲流的操作分为四个步骤:
1.创建原始流对象:创建成功就已经有了数据通路了
2.创建缓冲流:需要将原始流对象传入构造方法
3.加了缓冲,直接通过缓冲流读取数据即可
4.关闭流:只需要关闭装饰流
讲了这么多,作为一个程序猿还是喜欢看具体代码来掌握知识点。
实例:
1、已知电脑E盘新建一个io.txt文件.
2、用输出流BufferedWriter 向Stream.txt文件中写入”我叫XXX,性别:男,出生年月:XXXXX””
3、用输入流BufferdeReader 读取获Stream.txt文件内容并打印到控制台
public class Test1 {
public static void main(String[] args) throws IOException {
File file = new File("E:\\io.txt");
if (!file.exists()) {
file.createNewFile();
System.out.println("创建成功");
}
FileWriter fw = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(fw);
String str = "我叫XXX,性别:男,出生年月:XXXXX\n我叫XXX,性别:男,出生年月:XXXXX";
bw.write(str);
bw.newLine();
bw.close();
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}
}
为了能全面都介绍到IO缓冲流,我们把上面的字符流BufferedReader 、BufferedWriter 换成字节流BufferedInputStream 、BufferedOutputStream 来实现相同的功能,代码如下:
为了能全面都介绍到IO缓冲流,我们把上面的字符流BufferedReader 、BufferedWriter 换成字节流BufferedInputStream 、BufferedOutputStream 来实现相同的功能,代码如下:
public class Test1_1 {
public static void main(String[] args) throws IOException {
File file = new File("E:\\io.txt");
if (!file.exists()) {
file.createNewFile();
System.out.println("创建成功");
}
OutputStream out = new FileOutputStream(file);
BufferedOutputStream bos =new BufferedOutputStream(out);
String msg = "我叫XXX3,性别:男,出生年月:XXXXX\n我叫XXX3,性别:男,出生年月:XXXX";
byte[] bytes = msg.getBytes();
bos.write(bytes);
bos.close();
InputStream isStream=new FileInputStream(file);
BufferedInputStream bis=new BufferedInputStream(isStream);
byte[]buff=new byte[100];
int len=-1;//当前的位置
StringBuffer sBuffer=new StringBuffer();
while((len=bis.read(buff, 0, buff.length))!=-1){
String str=new String(buff,0,len);//将字符数组转成字符串
sBuffer.append(str);
}
System.out.println(sBuffer);
bis.close();
}
}
IO转换流
转换流是指将字节流与字符流之间的转换,包含两个类:InputStreamReader和OutputStreamWriter。
InputStreamReader:字节流转换成字符流,可以按照指定字符集进行解码
OutputStreamWriter:将字符转成字节存到指定的设备中,可以按照指定的字符集进行编码。
在《Java网络编程》中这样描述到,
Reader和Writer最重要的子类是InputStreamReader和OutputStreamWriter类。
InputStreamReader类包含了一个底层输入流,可以从中读取原始字节。它根据指定的编码方式,将这些字节转换为Unicode字符。
OutputStreamWriter从运行的程序中接收Unicode字符,然后使用指定的编码方式将这些字符转换为字节,再将这些字节写入底层输出流中。
例子:
从控制台输入一行文字(BufferedReader来读, BuffereWriter来写,编码集用utf-8),保存到文件中 。
public static void main(String[] args) throws IOException {
//数据的方向:标准输入---》内存
InputStream is = System.in;//System.in已经是描述好的标准输入流对象,直接使用
//转换
InputStreamReader ir = new InputStreamReader(is, "utf-8");
//加缓冲
BufferedReader bfr = new BufferedReader(ir);
System.out.println("请输入一行文字:");
String msg = bfr.readLine();
// 保存到文件中
//创建原始流
FileOutputStream fos = new FileOutputStream("E:\\save.txt");
//转换成字符输出流
OutputStreamWriter ops = new OutputStreamWriter(fos, "utf-8");
//加入缓冲功能
BufferedWriter bfw = new BufferedWriter(ops);
bfw.write(msg);
bfw.close();
}
从电脑中的一个文本文件中的内容打印到屏幕上显示。
String srcFilePath ="E:\\code.java";
//1.用FileInputStream创建数据通路
FileInputStream fis = new FileInputStream(srcFilePath);
//2.用转换流将字节流转换成字符流:参数二是指定的字符编码集的名称
InputStreamReader ir = new InputStreamReader(fis,"unicode");
//3.用缓冲流包装字符流
BufferedReader bfr = new BufferedReader(ir);
String line = bfr.readLine();
System.out.println(line);
bfr.close();
(1) 堆
堆的作用是存放对象实例和数组。堆从结构上来分,可以分为新生代和老生代。而新生代又可以分为Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)。 所有新生成的对象首先都是放在年轻代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
控制参数
-Xms设置堆的最小空间大小。-Xmx设置堆的最大空间大小。-XX:NewSize设置新生代最小空间大小。-XX:MaxNewSize设置新生代最小空间大小。
垃圾回收
此区域是垃圾回收的主要操作区域。
异常情况
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
(2)方法区
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。
很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚
拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。在Java8中永生代彻底消失了。
控制参数
-XX:PermSize 设置最小空间 -XX:MaxPermSize 设置最大空间
垃圾回收
对此区域会涉及但是很少进行垃圾回收。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意。
异常情况
根据Java 虚拟机规范的规定, 当方法区无法满足内存分配需求时, 将抛出
OutOfMemoryError 异常。
(3)PC计数器
它的作用可以看做是当前线程所执行的字节码的行号指示器。
异常情况
此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
(4)方法栈
每个线程会有一个私有的栈。每个线程中方法的调用又会在本栈中创建一个栈帧。在方法栈中会存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
控制参数
-Xss控制每个线程栈的大小。
异常情况
在Java 虚拟机规范中,对这个区域规定了两种异常状况:
将抛出StackOverflowError: 异常线程请求的栈深度大
于虚拟机所允许的深度时抛出;
OutOfMemoryError 异常: 虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出。
(5)本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其
区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则
是为虚拟机使用到的Native 方法服务。
控制参数
在Sun JDK中本地方法栈和方法栈是同一个,因此也可以用-Xss控制每个线程的大小。
异常情况
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError
异常。
虚拟机参数配置
什么是虚拟机参数配置
在虚拟机运行的过程中,如果可以跟踪系统的运行状态,那么对于问题的故障排查会有一定的帮助,为此,在虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行Java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。我们进行虚拟机参数配置,其实就是围绕着堆、栈、方法区、进行配置,而最多的就是关于堆内存中新生代和老年代的参数配置,
堆的参数配置
-XX:+PrintGC 每次触发GC的时候打印相关日志
-XX:+UseSerialGC 串行回收
-XX:+PrintGCDetails 更详细的GC日志
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
含以-XX:SurvivorRatio=eden/from=den/to
总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,
这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
设置最大堆内存
参数: -Xms5m -Xmx20m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags
设置新生代与老年代优化参数
-Xmn 新生代大小,一般设为整个堆的1/3到1/4左右
-XX:SurvivorRatio 设置新生代中eden区和from/to空间的比例关系n/1
设置新生代比例参数
参数: -Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
设置新生与老年代代参数
-Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
-Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
-XX:NewRatio=2
总结:不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略:尽可能将对象预留在新生代,减少老年代的GC次数。除了可以设置新生代的绝对大小(-Xmn),可以使用(-XX:NewRatio)设置新生代和老年代的比例:-XX:NewRatio=老年代/新生代
内存溢出解决办法
设置堆内存大小
错误原因: java.lang.OutOfMemoryError: Java heap space
解决办法:设置堆内存大小 -Xms1m -Xmx70m -XX:+HeapDumpOnOutOfMemoryError
设置栈内存大小
错误原因: java.lang.StackOverflowError
栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。
解决办法:设置线程最大调用深度
-Xss5m 设置最大调用深度
Tomcat内存溢出在catalina.sh 修改JVM堆内存大小
JAVA_OPTS="-server -Xms800m -Xmx800m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxNewSize=512m"
JVM参数调优总结
在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:
GC的时间足够的小
GC的次数足够的少
发生Full GC的周期足够的长
前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。
针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小
年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,否则也就不会有调优。我们观察一下二者大小变化有哪些影响
更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。
但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:
(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理
(B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间
dump命令:
./jmap -dump:live,format=b,file=heap.hprof 38220
工具:MemoryAnalyzer-1.8.1.20180910-win32.win32.x86_64
步骤一:使用jsp查找程序进程
jps -l
步骤二:使用jstack查看线程堆栈信息
jstack -l 85413 >> 123.txt
扩展
通过jdk提供的工具jconsole排查死锁问题:jdk提供的一个可视化的工具,方便排查程序的一些问题,如:程序内存溢出、死锁问题等等。jconsole位于jdk的bin目录中。
通过jdk提供的工具VisualVM排查死锁问题:VisualVM是JDK自带的一个用于Java程序性能分析的工具,在JDK安装目录的bin文件夹下名称为 jvisualvm.exe。
ps -ef|grep java
查看当前服务器的java进程
jps
同上,不包含启动参数
jmap
jmap -heap 查看java内存信息
jmap -histo 查看内存详细信息
jmap -dump:file=file.dump 导出堆栈信息
jstack
jstack 导出堆栈信息
BLOCKED、deadlock
jvisualvm
分析堆栈信息、线程信息工具
jvm出现OOM后自动导出堆栈信息
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/heap/dump
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKhbLQN8-1629367795294)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image001.gif)]当前系统的实时数据面板
dashboard
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GWWmNYdS-1629367795295)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]查看调用堆栈
com.thunisoft.lxfz.util.TAPConfigUtils getConfigureByFydm
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kavIHgVW-1629367795296)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]跟踪方法调用栈,分析耗时信息
trace com.thunisoft.lxfz.util.TAPConfigUtils getConfigureByFydm
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ys3bps7W-1629367795298)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]跟踪方法调用参数和返回结果,可以自定义条件和结果显示深度
watch com.thunisoft.lxfz.util.TAPConfigUtils getConfigureByFydm ‘{params, returnObj}’ “params[1]==‘dzjz.url’” -x 3
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-asiAZuZs-1629367795302)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image003.gif)]反编译class
jad com.thunisoft.lxfz.util.TAPConfigUtils
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E9XN0bCf-1629367795305)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]增强调试模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wrtR78SJ-1629367795306)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image005.gif)]跟踪方法调用
tt -t demo.MathGame primeFactors
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TR5Ut9Hz-1629367795308)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image007.gif)]查看调用历史记录
tt -l
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k33rYvN5-1629367795311)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image009.gif)]查看调用详情
tt -i 1003
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-znOInj7S-1629367795312)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image005.gif)]重新发起调用
tt -i 1004 -p
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LY2gvBtV-1629367795313)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]打印线程信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C2rIAjwm-1629367795315)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image005.gif)]找出最忙的线程
thread -n 3
thread -n -1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nK5tVGSg-1629367795318)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image012.gif)]找出当前阻塞其他线程的线程
thread -b
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmbqpsRb-1629367795323)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image005.gif)]指定采样时间间隔
thread -n -1 -i 1000
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ImBTwq3-1629367795326)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]监控现场接口稳定性
monitor -c 60 com.thunisoft.lxfz.util.TAPConfigUtils getConfigureByFydm
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5XPAaNO-1629367795327)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]以jpda模式启动tomcat
Linux为:./catalina.sh jpda start
windows为:catalina.bat jpda start
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VIWlblae-1629367795328)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image002.gif)]命令行启动方式(用来在ide上调试)
java –jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n 【】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FclaV0L7-1629367795331)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image013.gif)]启动调试
jdb -attach localhost:8000
如果上述报错,用这个:
Jdb -connect com.sun.jdi.SocketAttach:port=8000,hostname=localhost
增加断点:
stop at 类:行号
例如:stop at com.thunisoft.lxfz.controller.WritServerApi:161
stop in 类.方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LySBryqO-1629367795332)(file:///C:\Users\HY\AppData\Local\Temp\msohtmlclip1\01\clip_image015.jpg)]
n next 跳过一行(跨过调用)
n cont 从断点处继续执行
n locals 查看当前环境参数
n print 打印参数
n step执行当前行
n step up 执行到当前方法返回其调用方
n stepi 执行当前指令
方式一:继承Thread类的方式
方式二:实现Runnable接口的方式
以下两种方式是jdk1.5新增的!
方式三:实现Callable接口
说明:
方式四:使用线程池
说明:
好处:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Jy7Sc5E-1629367795333)(https://www.pianshen.com/images/651/89e229ece94f66d103f98d591d7cddbb.png)]
线程池状态
Java中接口和抽象类的定义语法分别为interface与abstract关键字。
抽象类:在Java中被abstract关键字修饰的类称为抽象类,被abstract关键字修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法体。抽象类的特点:
a、抽象类不能被实例化只能被继承;
b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;
c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;
e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。
**接口:**Java中接口使用interface关键字修饰,特点为:
a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);
b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;
c、一个类可以实现多个接口;
d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。
如下例子所示:
public interface Person{
public static final int a=10;
//JDK1.8
default void sayHello(){
System.out.println("Hello World");
}
public void say();
}
public abstract class Person{
public abstract void say();
public void eat(){};
}
如上述代码所示:
接口只能是功能的定义,而抽象类既可以为功能的定义也可以为功能的实现。
相同点
(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点
(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
微服务是一种用于构建应用的架构方案。微服务架构有别于更为传统的单体式方案,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署,这意味着各项服务在工作(和出现故障)时不会相互影响。
同步:RPC ,REST等。
异步:消息队列,要考虑消息的可靠传输、高性能,以及编程模型的变化等。
一.两者的模块组成:
1、Dubbo主要分为服务注册中心,服务提供者,服务消费者,还有管控中心;
2、相比起Dubbo简单的四个模块,SpringCloud则是一个完整的分布式一站式框架,他有着一样的服务注册中心,服务提供者,服务消费者,管控台,断路器,分布式配置服务,消息总线,以及服务追踪等;
二.在性能上来说
由于Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC。而SpringCloud是基于Http协议+rest接口调用远程过程的,相对来说,Http请求会有更大的报文,占的带宽也会更多。
Dubbo对于上表中总结为“无”的组件不代表不能实现,而只是Dubbo框架自身不提供,需要另外整合以实现对应的功能,比如:
分布式配置:可以使用淘宝的diamond、百度的disconf来实现分布式配置管理。但是Spring Cloud中的Config组件除了提供配置管理之外,由于其存储可以使用git,因此它天然的实现了配置内容的版本管理,可以完美的与应用版本管理整合起来。
服务跟踪:可以使用京东开源的Hydra
批量任务:可以使用当当开源的Elastic-Job
……
虽然,Dubbo自身只是实现了服务治理的基础,其他为保证集群安全、可维护、可测试等特性方面都没有很好的实现,但是几乎大部分关键组件都能找到第三方开源来实现,这些组件主要来自于国内各家大型互联网企业的开源产品。
RPC vs REST
另外,由于Dubbo是基础框架,其实现的内容对于我们实施微服务架构是否合理,也需要我们根据自身需求去考虑是否要修改,比如Dubbo的服务调用是通过RPC实现的,但是如果仔细拜读过Martin Fowler的 microservices 一文,其定义的服务间通信是HTTP协议的REST API。那么这两种有何区别呢?
先来说说,使用Dubbo的RPC来实现服务间调用的一些痛点:
服务提供方与调用方接口依赖方式太强:我们为每个微服务定义了各自的service抽象接口,并通过持续集成发布到私有仓库中,调用方应用对微服务提供的抽象接口存在强依赖关系,因此不论开发、测试、集成环境都需要严格的管理版本依赖,才不会出现服务方与调用方的不一致导致应用无法编译成功等一系列问题,以及这也会直接影响本地开发的环境要求,往往一个依赖很多服务的上层应用,每天都要更新很多代码并install之后才能进行后续的开发。若没有严格的版本管理制度或开发一些自动化工具,这样的依赖关系会成为开发团队的一大噩梦。而REST接口相比RPC更为轻量化,服务提供方和调用方的依赖只是依靠一纸契约,不存在代码级别的强依赖,当然REST接口也有痛点,因为接口定义过轻,很容易导致定义文档与实际实现不一致导致服务集成时的问题,但是该问题很好解决,只需要通过每个服务整合swagger,让每个服务的代码与文档一体化,就能解决。所以在分布式环境下,REST方式的服务依赖要比RPC方式的依赖更为灵活。
服务对平台敏感,难以简单复用:通常我们在提供对外服务时,都会以REST的方式提供出去,这样可以实现跨平台的特点,任何一个语言的调用方都可以根据接口定义来实现。那么在Dubbo中我们要提供REST接口时,不得不实现一层代理,用来将RPC接口转换成REST接口进行对外发布。若我们每个服务本身就以REST接口方式存在,当要对外提供服务时,主要在API网关中配置映射关系和权限控制就可实现服务的复用了。
相信这些痛点也是为什么当当网在dubbox(基于Dubbo的开源扩展)中增加了对REST支持的原因之一。
小结:Dubbo实现了服务治理的基础,但是要完成一个完备的微服务架构,还需要在各环节去扩展和完善以保证集群的健康,以减轻开发、测试以及运维各个环节上增加出来的压力,这样才能让各环节人员真正的专注于业务逻辑。而Spring Cloud依然发扬了Spring Source整合一切的作风,以标准化的姿态将一些微服务架构的成熟产品与框架揉为一体,并继承了Spring Boot简单配置、快速开发、轻松部署的特点,让原本复杂的架构工作变得相对容易上手一些(如果您读过我之前关于Spring Cloud的一些核心组件使用的文章,应该能体会这些让人兴奋而激动的特性,传送门)。所以,如果选择Dubbo请务必在各个环节做好整套解决方案的准备,不然很可能随着服务数量的增长,整个团队都将疲于应付各种架构上不足引起的困难。而如果选择Spring Cloud,相对来说每个环节都已经有了对应的组件支持,可能有些也不一定能满足你所有的需求,但是其活跃的社区与高速的迭代进度也会是你可以依靠的强大后盾。
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。
在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。
Eureka和Zookeeper就是CAP定理中的实现,Eureka(保证AP),Zookeeper(保证CP)。
Zookeeper的设计理念就是分布式协调服务,保证数据(配置数据,状态数据)在多个服务系统之间保证一致性,这也不难看出Zookeeper是属于CP特性(Zookeeper的核心算法是Zab,保证分布式系统下,数据如何在多个服务之间保证数据同步)。Eureka是吸取Zookeeper问题的经验,先保证可用性。
先来聊一聊Spring作为Java开发人员,大家都Spring可不陌生,简而言之,Spring框架为开发Java应用程序提供了全面的基础架构支持。它包含一些很好的功能,如依赖注入和开箱即用的模块,如:
Spring JDBC 、Spring MVC 、Spring Security、 Spring AOP 、Spring ORM 、Spring Test
这些模块大家应该都用过吧,这些模块缩短应用程序的开发时间,提高了应用开发的效率
例如,在Java Web开发的早期阶段,我们需要编写大量的代码来将记录插入到数据源中。但是通过使用Spring JDBC模块的JDBCTemplate,我们可以将这操作简化为只需配置几行代码。
Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的XML配置,为更快,更高效的开发生态系统铺平了道路。
以下是Spring Boot中的一些特点:
(1)创建独立的spring应用。
(2)嵌入Tomcat, Jetty Undertow 而且不需要部署他们。
(3)提供的“starters” poms来简化Maven配置
(4)尽可能自动配置spring应用。
(5)提供生产指标,健壮检查和外部化配置
(6)绝对没有代码生成和XML配置要求
(1)方式一:先更新数据库,再更新缓存场景
当有两个线程A、B,同时对一条数据进行操作,一开始数据库和redis的数据都为tony,当线程A去修改数据库,将tony改为allen,然后线程A在修改缓存中的数据,可能因为网络原因出现延迟,这个时候线程B,将数据修改成了Mike、然后将数据库中的tony,也改成了Mike,然后线程A恢复正常,将redis中的缓存改成了allen,此时就出现了缓存数据和数据库数据不一致情况。不推荐
(2)方式二:先更新缓存,再更新数据库场景
当有两个线程A、B,同时对一条数据进行操作,线程A先将redis中 的数据修改为了allen,然后CPU切换到了线程B,将redis中的数据修改为了mike,然后将数据库中的信息也修改了mike,然后线程A获得CPU执行,将数据库中的信息改为了allen,此时出现缓存和数据库数据不一致情况。不推荐
(3)方式三:先删除缓存,再更新数据库的场景
当有两个线程A、B,同时对一条数据进行操作,当线程A进行修改缓存操作时,先删除掉缓存中的数据,然后去修改数据库,因为网络问题出现延迟,这时线程B需要去数据库中查询数据为tony,然后将数据更新到缓存中,线程A网络恢复,又将数据库数据修改为了allen,此时出现数据不一致。不推荐
(4)方式四:先更新数据库,在删除缓存场景
一改一查场景:
当有两个线程A、B,线程A先去将数据库的值修改为allen,然后需要去删除redis中的缓存,当线程B去读取缓存时,线程A已经完成delete操作时,缓存不命中,需要去查询数据库,然后在更新缓存,数据一致性;如果线程A没有完成delete操作,线程B直接命中,返回的数据与数据库中的数据不一致,可能会短暂出现数据不一致情况,但最终都会一致。推荐
存在的问题:当数据过期或者初始化时,会出现数据不一致情况,也就是线程B从数据库中,查询到数据为tony,然后线程A将tony修改为了allen,然后去删除redis中的数据,然后线程B将读到的tony,更新到了数据库中,出现了数据不一致问题。
解决方案:对于不过期的数据我们要在上线的时候做好数据的预热,保证缓存命中。对于存在过期的数据,因为有过期时间,只会在特定的时间段内数据不一致,下次数据过期后,可以恢复,对于实时性要求不高时,可以接受。
两次修改场景:
当有两个线程A、B,线程A去修改数据库中的值改为allen,然后出现网络波动,线程B将数库中的值修改为了Mike,然后两个线程都会删除缓存,保证数据一致性。
(5)方式五:最佳实现,数据异步同步
Canal:基于数据库增量日志解析,提供增量数据订阅和消费https://github.com/alibaba/canal
mysql会将操作记录在Binary log日志中,通过canal去监听数据库日志二进制文件,解析log日志,同步到redis中进行增删改操作。
canal的工作原理:canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议;MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );canal 解析 binary log 对象(原始为 byte 流)。
(1)缓存穿透:服务不断请求没有的缓存和数据库没有的值,一直查询数据库导致数据库压力过大。
**解决办法:**1)数据库中没有结果时,将该key的缓存值设置为null,过期时间短一点,一般30s,最长不超过五分钟。
2)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
(2)缓存击穿:缓存中某个key过期,大量并发请求查询这个key,导致数据库压力瞬间增大。
**解决办法:**1)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
2)使用普通互斥锁,缓存中没有值,去数据库查询的时候加锁,其他查询请求n秒后再来查询,查询完库后把结果写在缓存中,其他请求查询这个key 就不用去查询数据库了。
3)使用互斥锁(mutex key)业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
4)"提前"使用互斥锁(mutex key):
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。
5)设置热点数据永不过期
(3)缓存雪崩:大量缓存key同时过期导致大量请求同时查询数据库导致数据库压力过大。
**解决办法:**1)对每个key设置一个随机的过期时间,防止同一时间大量数据过期现象发生。
2)设置热点数据永不过期
Redis有三种集群模式,分别是:
主从模式
Sentinel模式
Cluster模式
(1)主从模式
主从模式介绍
主从模式是三种模式中最简单的,在主从复制中,数据库分为两类:主数据库(master)和从数据库(slave)。
其中主从复制有如下特点:
主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库
从数据库一般都是只读的,并且接收主数据库同步过来的数据
一个master可以拥有多个slave,但是一个slave只能对应一个master
slave挂了不影响其他slave的读和master的读和写,重新启动后会将数据从master同步过来
master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务
master挂了以后,不会在slave节点中重新选一个master
工作机制:
当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。
复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性。
安全设置:
当master节点设置密码后,客户端访问master需要密码,启动slave需要密码,在配置文件中配置即可,客户端访问slave不需要密码
缺点:
从上面可以看出,master节点在主从模式中唯一,若master挂掉,则redis无法对外提供写服务。
(2)Sentinel模式
Sentinel模式介绍
主从模式的弊端就是不具备高可用性,当master挂掉以后,Redis将不能再对外提供写入操作,因此sentinel应运而生。
sentinel中文含义为哨兵,顾名思义,它的作用就是监控redis集群的运行状况,特点如下:
sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
当master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
当master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
多sentinel配置的时候,sentinel之间也会自动监控
当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
一个sentinel或sentinel集群可以管理多个主从Redis,多个sentinel也可以监控同一个redis
sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
工作机制:
每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令
如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被sentinel标记为主观下线。
如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态
当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态, 则master会被标记为客观下线
在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;
若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除
当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。
(3)Cluster模式介绍
sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。
cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容。
使用集群,只需要将redis配置文件中的cluster-enable配置打开即可。每个集群中至少需要三个主数据库才能正常运行,新增节点非常方便。
cluster集群特点:
多个redis节点网络互联,数据共享
所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上,
并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为
支持在线增加、删除节点
客户端可以连接任何一个主节点进行读写
分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。
面对高并发的读写访问,当数据库master服务器无法承载写操作压力时,不管如何扩展slave服务器,此时都没有意义了。
因此,我们必须换一种思路,对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库。
与分表策略相似,分库可以采用通过一个关键字取模的方式,来对数据访问进行路由
能不分就不分
并不是所有表都需要切分,主要还是看数据的增长速度。切分后在某种程度上提升了业务的复杂程度。不到万不得已不要轻易使用分库分表这个“大招”,避免“过度设计”和“过早优化”。分库分表之前,先尽力做力所能及的优化:升级硬件、升级网络、读写分离、索引优化等。当数据量达到单表瓶颈后,在考虑分库分表。
数据量过大,正常运维影响业务访问
这里的运维是指:对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO
对一个很大的表做DDL,MYSQL会锁住整个表,这个时间会很长,这段时间业务不能访问此表,影响很大。
大表经常访问和更新,就更有可能出现锁等待。
随着业务发展,需要对某些字段垂直拆分
有些不经常访问或者更新频率低的字段应该从大表中分离出去。
数据量快速增长
随着业务的快速发展,单表中的数据量会持续增长,当性能接近瓶颈时,就需要考虑水平切分,做分库分表了。
or的前后两个字段没有同时创建索引
where 子句中用is null和is not null、in 和 not in (可以用between)
模糊查询like的%在左边
对where后面的条件进行表达式操作、函数操作
查询结果大于表中总数据量的30%
在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
索引失效举例
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
select id from t where name like '%abc%'
select id from t where num/2=100
应改为:
select id from t where num=100*2
select id from t where substring(name,1,3)='abc'--name以abc开头的id
应改为:
select id from t where name like 'abc%'
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(...)**
临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log , 以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。
*在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。*
1、能熟练地进行多线程编程,有效解决同步问题
2、线程池的使用
3、java的4种并发模型
4、能实现无锁队列
1、平行工人:并行工作器模式,传入的工作分配给不同的工人,说明并行工作程序。
特点:各干各的、为了增加应用程序的并行化,只需添加更多工作程序
2、流水线:组装线并发模型,有时也称为无共享并发模型。本质上是单线程实现,不必考虑并发问题
3、功能并行, 函数并行化模型、跨CPU协调函数调用会带来开销。如果函数调用很小,则尝试并行化它们实际上可能比单线程,单CPU执行慢。
每个函数调用都可以独立于任何其他函数调用执行。当每个函数调用可以独立执行时,每个函数调用可以在单独的CPU上执行。这就是说,功能上实现的算法可以在多个CPU上并行执行。
1、Java线程池的四种创建方式
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
2、Java线程池七个参数详解
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
当前的线程数量大于corePoolSize,且有线程空闲,keepAliveTime后被销毁
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
①CallerRunsPolicy
该策略下,在调用者线程中(主线程)直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
volatile关键字:当多个线程操作共享数据时,可以保证内存中的数据可见。用这个关键字修饰共享数据,就会及时的把线程缓存中的数据刷新到主存中去,也可以理解为,就是直接操作主存中的数据。所以在不使用锁的情况下,可以使用volatile。如下:
public volatile boolean flag = false;
volatile不具备互斥性(当一个线程持有锁时,其他线程进不来,这就是互斥性)。
volatile不具备原子性。
原子性就是操作不可再细分,而i++操作分为读改写三步,如下:
int temp = i;
i = i+1;
i = temp;
所以i++明显不是原子操作。上面10个线程进行i++时,内存图解如下:
看到这里,好像和上面的内存可见性问题一样。是不是加个volatile关键字就可以了呢?其实不是的,因为加了volatile,只是相当于所有线程都是在主存中操作数据而已,但是不具备互斥性。比如两个线程同时读取主存中的0,然后又同时自增,同时写入主存,结果还是会出现重复数据。
**使用原子变量改进i++问题:**原子变量用法和包装类差不多,如下:
AtomicInteger i = new AtomicInteger();
public int getI(){
return i.getAndIncrement();
}
NEW(新建)
RUNNABLE(可运行的)
BLOCKED(阻塞)
WAITING(无限等待)
TIMED_WAITING(限期等待)
TERMINATED(终止)
CAS算法是计算机硬件对并发操作共享数据的支持,CAS包含3个操作数:
当且仅当V==A时,才会把B的值赋给V,即V = B,否则不做任何操作。就上面的i++问题,CAS算法是这样处理的:首先V是主存中的值0,然后预估值A也是0,因为此时还没有任何操作,这时V=B,所以进行自增,同时把主存中的值变为1。如果第二个线程读取到主内存中的还是0也没关系,因为此时预估值已经变成1,V不等于A,所以不进行任何操作。
在完成某些运算时,只有其他所有线程的运算全部完成,当前运算才继续执行,这就叫闭锁。
java.util.concurrent包中提供了多种并发容器类来改进同步容器的性能。ContDownLatch是一个同步辅助类
**我们的需求是输出0-5000内的所有偶数,然后输出“耗费时间为x秒”。**看下面代码:
public class TestCountDownLatch {
public static void main(String[] args){
LatchDemo ld = new LatchDemo();
long start = System.currentTimeMillis();
for (int i = 0;i<10;i++){
new Thread(ld).start();
}
long end = System.currentTimeMillis();
System.out.println("耗费时间为:"+(end - start)+"秒");
}
}
class LatchDemo implements Runnable{
private CountDownLatch latch;
public LatchDemo(){
}
@Override
public void run() {
for (int i = 0;i<5000;i++){
if (i % 2 == 0){//50000以内的偶数
System.out.println(i);
}
}
}
}
这段代码就是10个线程同时去输出5000以内的偶数,然后在主线程那里计算执行时间。其实这是计算不了那10个线程的执行时间的,因为主线程与这10个线程也是同时执行的,可能那10个线程才执行到一半,主线程就已经输出“耗费时间为x秒”这句话了。
所以要想计算这10个线程执行的时间,就得让主线程先等待,等10个分线程都执行完了才能执行主线程。这就要用到闭锁。
看如何使用:
public class TestCountDownLatch {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(10);//有多少个线程这个参数就是几
LatchDemo ld = new LatchDemo(latch);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(ld).start();
}
try {
latch.await();//这10个线程执行完之前先等待
} catch (InterruptedException e) {
}
long end = System.currentTimeMillis();
System.out.println("耗费时间为:" + (end - start));
}
}
class LatchDemo implements Runnable {
private CountDownLatch latch;
public LatchDemo(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
synchronized (this) {
try {
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0) {//50000以内的偶数
System.out.println(i);
}
}
} finally {
latch.countDown();//每执行完一个就递减一个
}
}
}
}
public class TestLock {
public static void main(String[] args) {
Ticket td = new Ticket();
new Thread(td, "窗口1").start();
new Thread(td, "窗口2").start();
new Thread(td, "窗口3").start();
}
}
class Ticket implements Runnable {
private Lock lock = new ReentrantLock();//创建lock锁
private int ticket = 100;
@Override
public void run() {
while (true) {
lock.lock();//上锁
try {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "完成售票,余票为:" + (--ticket));
}
}finally {
lock.unlock();//释放锁
}
}
}
}
**1、虚假唤醒问题:**解决办法就是把if判断改成while。
根据任务类型是CPU 密集型任务还是 IO 密集型任务进行划分:
理论上线程的数量 = CPU 核数就是最合适的,不过通常把线程的数量设置为CPU 核数 +1,会实现最优的利用率。
计算方式根据《Java虚拟机并发编程》中提出的:线程数 = CPU 核心数 / (1 - 阻塞系数)
其中计算密集型阻塞系数为 0,IO 密集型阻塞系数接近 1,一般认为在 0.8 ~ 0.9 之间。比如 8 核 CPU,按照公式就是 2 / ( 1 - 0.9 ) = 20 个线程数(简单粗暴的计算就是核心线程数的两倍)
**总结:**核心线程数的设置和CPU核心数、任务的执行情况(不确定,需要测试)
线程的平均工作时间所占比例越高,就需要越少的线程;
线程的平均等待时间所占比例越高,就需要越多的线程;
针对不同的程序,进行对应的实际测试就可以得到最合适 的选择。
+ import java.util.Iterator;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Description
*
* @Author blindore
* @Date Created in 2020-12-18 15:13
* @Version 2.10.6
*/
public class ThteadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8, 0, TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>(100),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 1; i <= 100; i++) {
System.out.println("添加第" + i + "个任务");
int finalI = i;
executor.execute(new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行:任务" + finalI + " run");
},"任务"+i ));
Iterator iterator = executor.getQueue().iterator();
while (iterator.hasNext()){
System.out.println("列表:"+iterator.next());
}
}
}
}