设计模式 之 创建型模式
模式 & 描述 | 包括 |
---|---|
创建型模式 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。 | 工厂模式(Factory Pattern) 抽象工厂模式(Abstract Factory Pattern) 单例模式(Singleton Pattern) 建造者模式(Builder Pattern) 原型模式(Prototype Pattern) |
1. 工厂模式(Factory Pattern)
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
1.1 概述
- **意图:**定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
- 主要解决:主要解决接口选择的问题。
- 何时使用:我们明确地计划不同条件下创建不同实例时。
- 如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。
- 关键代码:创建过程在其子类执行。
- 应用实例:
- 您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
- Hibernate 换数据库只需换方言和驱动就可以。
- 优点 :
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
- 缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
- 使用场景:
- 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
- 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
- 设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。
- 注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
1.2 例子
我们将创建一个 Shape
接口和实现 Shape
接口的实体类。下一步是定义工厂类 ShapeFactory
。
FactoryPatternDemo
类使用 ShapeFactory
来获取 Shape
对象。它将向 ShapeFactory
传递信息(CIRCLE
/ RECTANGLE
/ SQUARE
),以便获取它所需对象的类型。
1.2.1 步骤 1 . 创建一个接口
Shape.java
public interface Shape {
void draw();
}
1.2.2 步骤 2 . 创建实现接口的实体类
Rectangle.java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
Square.java
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
Circle.java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
1.2.3 步骤 3 . 创建一个工厂,生成基于给定信息的实体类的对象
ShapeFactory.java
public class ShapeFactory {
//使用 getShape 方法获取形状类型的对象
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}
1.2.4 步骤 4 . 使用该工厂,通过传递类型信息来获取实体类的对象
FactoryPatternDemo.java
public class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
//获取 Circle 的对象,并调用它的 draw 方法
Shape shape1 = shapeFactory.getShape("CIRCLE");
//调用 Circle 的 draw 方法
shape1.draw();
//获取 Rectangle 的对象,并调用它的 draw 方法
Shape shape2 = shapeFactory.getShape("RECTANGLE");
//调用 Rectangle 的 draw 方法
shape2.draw();
//获取 Square 的对象,并调用它的 draw 方法
Shape shape3 = shapeFactory.getShape("SQUARE");
//调用 Square 的 draw 方法
shape3.draw();
}
}
1.2.5 步骤 5 . 执行程序,输出结果
Inside Circle::draw() method.
Inside Rectangle::draw() method.
Inside Square::draw() method.
2. 抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。
2.1 概述
-
意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
-
主要解决:主要解决接口选择的问题。
-
何时使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
-
如何解决:在一个产品族里面,定义多个产品。
-
关键代码:在一个工厂里聚合多个同类产品。
-
应用实例:
- 背景:工作了,为了参加一些聚会,肯定有两套或多套衣服吧,比如说有商务装(成套,一系列具体产品)、时尚装(成套,一系列具体产品),甚至对于一个家庭来说,可能有商务女装、商务男装、时尚女装、时尚男装,这些也都是成套的,即一系列具体产品。
- 假设一种情况,在您的家中,某一个衣柜(具体工厂)只能存放某一种这样的衣服(成套,一系列具体产品),每次拿这种成套的衣服时也自然要从这个衣柜中取出了。
- 解释:用 OOP 的思想去理解,所有的衣柜(具体工厂)都是衣柜类的(抽象工厂)某一个,而每一件成套的衣服又包括具体的上衣(某一具体产品),裤子(某一具体产品),这些具体的上衣其实也都是上衣(抽象产品),具体的裤子也都是裤子(另一个抽象产品)。
-
优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
-
缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。
-
使用场景:
- QQ 换皮肤,一整套一起换。
- 生成不同操作系统的程序。
-
注意事项:产品族难扩展,产品等级易扩展。
2.2 例子
我们将创建 Shape
和 Color
接口和实现这些接口的实体类。下一步是创建抽象工厂类 AbstractFactory
。接着定义工厂类 ShapeFactory
和 ColorFactory
,这两个工厂类都是扩展了 AbstractFactory
。然后创建一个工厂创造器/生成器类 FactoryProducer
。
AbstractFactoryPatternDemo
类使用 FactoryProducer
来获取 AbstractFactory
对象。它将向 AbstractFactory
传递形状信息 Shape
(CIRCLE
/ RECTANGLE
/ SQUARE
),以便获取它所需对象的类型。同时它还向 AbstractFactory
传递颜色信息 Color
(RED
/ GREEN
/ BLUE
),以便获取它所需对象的类型。
2.2.1 步骤 1 . 为形状创建一个接口
Shape.java
public interface Shape {
void draw();
}
2.2.2 步骤 2 . 创建实现接口的实体类
Rectangle.java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
Square.java
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
Circle.java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
2.2.3 步骤 3 . 为颜色创建一个接口
Color.java
public interface Color {
void fill();
}
2.2.4 步骤4 . 创建实现接口的实体类
Red.java
public class Red implements Color {
@Override
public void fill() {
System.out.println("Inside Red::fill() method.");
}
}
Green.java
public class Green implements Color {
@Override
public void fill() {
System.out.println("Inside Green::fill() method.");
}
}
Blue.java
public class Blue implements Color {
@Override
public void fill() {
System.out.println("Inside Blue::fill() method.");
}
}
2.2.5 步骤 5 . 为 Color 和 Shape 对象创建抽象类来获取工厂
AbstractFactory.java
public abstract class AbstractFactory {
public abstract Color getColor(String color);
public abstract Shape getShape(String shape);
}
2.2.6 步骤 6 . 创建扩展了 AbstractFactory 的工厂类,基于给定的信息生成实体类的对象
ShapeFactory.java
public class ShapeFactory extends AbstractFactory {
@Override
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
@Override
public Color getColor(String color) {
return null;
}
}
ColorFactory.java
public class ColorFactory extends AbstractFactory {
@Override
public Shape getShape(String shapeType){
return null;
}
@Override
public Color getColor(String color) {
if(color == null){
return null;
}
if(color.equalsIgnoreCase("RED")){
return new Red();
} else if(color.equalsIgnoreCase("GREEN")){
return new Green();
} else if(color.equalsIgnoreCase("BLUE")){
return new Blue();
}
return null;
}
}
2.2.7 步骤 7 . 创建一个工厂创造器/生成器类,通过传递形状或颜色信息来获取工厂
FactoryProducer.java
public class FactoryProducer {
public static AbstractFactory getFactory(String choice){
if(choice.equalsIgnoreCase("SHAPE")){
return new ShapeFactory();
} else if(choice.equalsIgnoreCase("COLOR")){
return new ColorFactory();
}
return null;
}
}
2.2.8 步骤 8 . 使用 FactoryProducer 来获取 AbstractFactory,通过传递类型信息来获取实体类的对象
public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
//获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
//获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("CIRCLE");
//调用 Circle 的 draw 方法
shape1.draw();
//获取形状为 Rectangle 的对象
Shape shape2 = shapeFactory.getShape("RECTANGLE");
//调用 Rectangle 的 draw 方法
shape2.draw();
//获取形状为 Square 的对象
Shape shape3 = shapeFactory.getShape("SQUARE");
//调用 Square 的 draw 方法
shape3.draw();
//获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
//获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("RED");
//调用 Red 的 fill 方法
color1.fill();
//获取颜色为 Green 的对象
Color color2 = colorFactory.getColor("GREEN");
//调用 Green 的 fill 方法
color2.fill();
//获取颜色为 Blue 的对象
Color color3 = colorFactory.getColor("BLUE");
//调用 Blue 的 fill 方法
color3.fill();
}
}
2.2.9 步骤 9 . 执行程序,输出结果
Inside Circle::draw() method.
Inside Rectangle::draw() method.
Inside Square::draw() method.
Inside Red::fill() method.
Inside Green::fill() method.
Inside Blue::fill() method.
3. 单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
3.1 概述
- 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 主要解决:一个全局使用的类频繁地创建与销毁。
- 何时使用:当您想控制实例数目,节省系统资源的时候。
- 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 关键代码:构造函数是私有的。
- 应用实例:
- 一个班级只有一个班主任
- Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行
- 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件
- 优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)
- 避免对资源的多重占用(比如写文件操作)
- 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
- 使用场景:
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
- 注意事项:
getInstance()
方法中需要使用同步锁synchronized (Singleton.class)
防止多线程同时进入造成instance
被多次实例化。
3.2 例子
我们将创建一个 SingleObject
类。SingleObject
类有它的私有构造函数和本身的一个静态实例。
SingleObject
类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo
类使用 SingleObject
类来获取 SingleObject
对象。
3.2.1 步骤 1 . 创建一个 Singleton 类
SingleObject.java
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
3.2.2 步骤 2 . 从 singleton 类获取唯一的对象
SingletonPatternDemo.java
public class SingletonPatternDemo {
public static void main(String[] args) {
//不合法的构造函数
//编译时错误:构造函数 SingleObject() 是不可见的
//SingleObject object = new SingleObject();
//获取唯一可用的对象
SingleObject object = SingleObject.getInstance();
//显示消息
object.showMessage();
}
}
3.2.3 步骤 3 . 执行程序,输出结果
Hello World!
3.3 单例模式的几种实现方式
单例模式的实现有多种方式,如下所示:
3.3.1 懒汉式,线程不安全
- 是否 Lazy 初始化:是
- 是否多线程安全:否
- 实现难度:易
- 描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
接下来介绍的几种实现方式都支持多线程,但是在性能上有所差异。
3.3.2 懒汉式,线程安全
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:易
- 描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance()
的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.3.3 饿汉式
- 是否 Lazy 初始化:否
- 是否多线程安全:是
- 实现难度:易
- 描述:这种方式比较常用,但容易产生垃圾对象。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
3.3.4 双检锁/双重校验锁(DCL,即 double-checked locking)
- JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:较复杂
- 描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance()
的性能对应用程序很关键。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
3.3.4.1 JAVA 中 volatile 关键字详解
3.3.4.1.1 volatile 作用
volatile关键字的主要作用是使变量在多个线程间可见,方式是强制性从公共堆栈中进行取值。
先看个例子:
RunThread.java
public class RunThread extends Thread{
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
System.out.println("进入 run 了");
while (isRunning == true){
}
System.out.println("线程被停止了");
}
}
TestMain.java
public class TestMain {
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已经赋值为false");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
运行结果如下:
程序会一直运行下去,造成死循环。因为在启动RunThread.java
线程时,变量 private boolean isRunning = true;
存在于公共堆栈及线程的私有堆栈中。线程一直在私有堆栈中取得 isRunning
的值是true。而代码 thread.setRunning(false)
虽然被执行,更新的确实公共堆栈中的 isRunning
变量值 false
,所以一直就是死循环状态。内存结构如下图所示。
线程的私有堆栈
这个问题是私有堆栈中的值和公共堆栈中的值不同不造成的。解决这样的问题就要使用 volatile
关键字了,它主要的作用就是当线程访问 isRunning
这个变量时,强制性从公共堆栈中进行取值。
更改后 RunThread.java
代码如下:
public class RunThread extends Thread{
volatile private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
System.out.println("进入 run 了");
while (isRunning == true){
}
System.out.println("线程被停止了");
}
}
TestMain.java
public class TestMain {
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已经赋值为false");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
运行结果如下:
通过使用 volatile
关键字,强制的从公共内存中读取变量的值,内存结构如下图所示。
- 优点:读取公共内存,使用volatile关键字增加了实例变量在多个线程之间的可见性。
- 缺点:但volatile关键字最致命的缺点是不支持原子性。
- 原子性(Atomicity):指事务的不可分割性,一个事物的所有操作要么不间断地全部被执行,要么一个也没有执行。
3.3.4.1.2 volatile非原子的特性
示例如下:
MyThread.java
public class MyThread extends Thread{
volatile public static int count;
private static void addCount(){
for (int i =0; i < 1000; i++){
count++;
}
System.out.println("count = " + count);
}
@Override
public void run() {
addCount();
}
}
TestRun.java
public class TestRun {
public static void main(String[] args) {
MyThread[] arr = new MyThread[100];
for (int i = 0; i < 100; i++){
arr[i] = new MyThread();
}
for (int j = 0; j < 100; j++){
arr[j].start();
}
}
}
运行结果如下:
更改 MyThread.java
文件代码如下:
public class MyThread extends Thread{
volatile public static int count;
//注意一定要添加static关键字
//这样synchronized与static锁的内容就是MyThread.class 类了,也就达到同步的效果了。
synchronized private static void addCount(){
for (int i =0; i < 1000; i++){
count++;
}
System.out.println("count = " + count);
}
@Override
public void run() {
addCount();
}
}
TestRun.java
public class TestRun {
public static void main(String[] args) {
MyThread[] arr = new MyThread[100];
for (int i = 0; i < 100; i++){
arr[i] = new MyThread();
}
for (int j = 0; j < 100; j++){
arr[j].start();
}
}
}
运行结果如下:
在本示例中,如果在方法 private static void addCount()
前加入 synchronized
同步关键字,也就没有必要再使用 volatile
关键字来声明 count
变量了。
关键字 volatile
主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的 值使用,也就是用多线程读取共享变量时可以获得最新值使用。
关键字 volatile
提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如 i++
,也就是 i= i+1
,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式 i++
的操作步骤分解如下:
- 从内存中取出i的值
- 计算i的值
- 将
i
的值写到内存中
假如在第二步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏数据。解决的办法其实就是使用 synchronized
关键字。所以说 volatile
本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存的。
用图演示关键字 volatile
出现非线程安全的原因,变量在内存中工作的过程如下图所示。
可以得出以下结论:
read
和load
阶段:从主存复制变量到当前线程工作内存;use
和assign
阶段:执行代码,改变共享变量值。store
和write
阶段:用工作内存数据刷新主存对应变量的值。
在多线程环境中,use
和 assign
是多次出现的,但这一操作并不是原子性,也就是在 read
和 load
之后,如果主内存 count
变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化 ,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。
对于用 volatile
修饰的变量,jvm
虚拟机只是保证从主内存加载到线程工作内存的值是最新的。例如 线程1
和 线程2
在进行 read
和 load
的操作中,发现主内存中 count
的值都是 5
,那么都会加载这个最新的值。也就是说,volatile
关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
3.3.4.1.3 原子类也并不完全安全
原子类
在java.util.concurrent.atomic
包下,有一系列“Atomic”开头的类,统称为原子类。
-
肯定你有一个问题
为什么Atomic类可以保证原子性?
- 以
AtomicInteger
为例。在AtomicInteger
中有一个volatile
修饰的value
变量,也就是这个整型的值。在调用getAndIncrement()
时,AtomicInteger
会通过Unsafe
类的getAndAddInt
方法对变量value
进行一次CAS操作
。由于CAS
是具有原子性的,所以AtomicInteger
就保证了操作的线程安全。
- 以
-
CAS
(compare and swap)的缩写,译为比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。执行CAS
操作的时候,将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值;如果不匹配,处理器不做任何操作,多个线程同时执行CAS
操作只有一个会成功。CAS
有 3 个操作数,位置内存值 V ,旧的预期值 A ,要修改的更新值 B 。当且仅当旧的预期值 A 和内存值 V 相同时,将内存值 V 修改为 B ,否则什么都不做或重来。
-
硬件级别保证
CAS
是JDK
提供的非阻塞原子性操作,它通过硬件保证了比较——更新的原子性。它是非阻塞的且自身原子性,也就是说它效率更高且通过硬件保证,说明其更可靠。CAS
是一条CPU
的原子指令(cmpxchg
指令),不会造成所谓的数据不一致问题,Unsafe
提供的CAS
方法(如compareAndSwapXXX
)底层实现即为CPU指令cmpxchg
。执行cmpxchg
指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas
操作,也就是说CAS
的原子性实际上是CPU
实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized
, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好。
-
代码示例
-
public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5); System.out.println(atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(5,2020)+"\t"+atomicInteger.get());//执行后,expect为2020 System.out.println(atomicInteger.compareAndSet(5,1024)+"\t"+atomicInteger.get()); } }
-
-
CAS底层原理分析(以AtomicInteger为例)
-
-
private static final Unsafe U = Unsafe.getUnsafe();
-
Unsafe
,CAS
的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe
相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe
类存在于sun.misc
包中,其内部方法操作可以像C
的指针一样直接操作内存,因为 Java 中CAS
操作的执行依赖于Unsafe
类的方法。- native 详见 https://www.jb51.net/article/235138.htm
-
注意:
Unsafe
类中的所有方法都是native
修饰的,也就是说Unsafe
类中的方法都直接调用操作系统底层资源执行相应任务
-
-
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
- 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
-
private volatile int value;
- 变量value用volatile修饰,保证了多线程之间的内存可见性。
-
@IntrinsicCandidate public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }
- CAS 并发原语体现在JAVA语言中就是 sun.misc.Unsafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS 是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致问题。
- 核心思想:比较要更新变量的值V和预期值 E (compare) ,相等才会将V的值设为新值N (swap) 如果不相等自旋再来。
-
-
3.3.4.2 具体实现
- JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:较复杂
- 描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance()
的性能对应用程序很关键。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
3.3.5 登记式/静态内部类
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:一般
- 描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
- 这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
- 这种方式同样利用了
classloader
机制来保证初始化instance
时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么instance
就会被实例化(没有达到lazy loading
效果),而这种方式是Singleton
类被装载了,instance
不一定被初始化。因为SingletonHolder
类没有被主动使用,只有通过显式调用getInstance
方法时,才会显式装载SingletonHolder
类,从而实例化instance
。 - 想象一下,如果实例化
instance
很消耗资源,所以想让它延迟加载,另外一方面,又不希望在Singleton
类加载时就实例化,因为不能确保Singleton
类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance
显然是不合适的。这个时候,这种方式相比 第 3 种方式 就显得很合理。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.3.6 枚举
- JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:否
- 是否多线程安全:是
- 实现难度:易
- 描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。
- 它更简洁,自动支持序列化机制,绝对防止多次实例化。
- 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
- 不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
**经验之谈:**一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
4. 建造者模式
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。
4.1 概述
-
意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
-
主要解决:
- 主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;
- 由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
-
何时使用:一些基本部件不会变,而其组合经常变化的时候。
-
如何解决:将变与不变分离开。
-
关键代码:
- 建造者:创建和提供实例,
- 导演:管理建造出来的实例的依赖关系。
-
应用实例:
- 去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。
- JAVA 中的 StringBuilder。
-
优点:
- 建造者独立,易扩展。
- 便于控制细节风险。
-
缺点:
- 产品必须有共同点,范围有限制。
- 如内部变化复杂,会有很多的建造类。
-
使用场景:
- 需要生成的对象具有复杂的内部结构。
- 需要生成的对象内部属性本身相互依赖。
-
注意事项:与工厂模式的区别是,建造者模式更加关注与零件装配的顺序。
4.2 例子
我们假设一个快餐店的商业案例,其中,一个典型的套餐可以是一个汉堡(Burger)和一杯冷饮(Cold drink)。汉堡(Burger)可以是素食汉堡(Veg Burger)或鸡肉汉堡(Chicken Burger),它们是包在纸盒中。冷饮(Cold drink)可以是可口可乐(coke)或百事可乐(pepsi),它们是装在瓶子中。
我们将创建一个表示食物条目(比如汉堡和冷饮)的 Item
接口和实现 Item
接口的实体类,以及一个表示食物包装的 Packing
接口和实现 Packing
接口的实体类,汉堡是包在纸盒中,冷饮是装在瓶子中。
然后我们创建一个 Meal
类,带有 Item
的 ArrayList
和一个通过结合 Item 来创建不同类型的 Meal
对象的 MealBuilder
。BuilderPatternDemo
类使用 MealBuilder
来创建一个 Meal
。
4.2.1 步骤 1 . 创建一个表示食物条目和食物包装的接口
Item.java
public interface Item {
public String name();
public Packing packing();
public float price();
}
Packing.java
public interface Packing {
public String pack();
}
4.2.2 步骤 2 . 创建实现 Packing 接口的实体类
Wrapper.java
public class Wrapper implements Packing {
@Override
public String pack() {
return "Wrapper";
}
}
Bottle.java
public class Bottle implements Packing {
@Override
public String pack() {
return "Bottle";
}
}
4.2.3 步骤 3 . 创建实现 Item 接口的抽象类,该类提供了默认的功能
Burger.java
public abstract class Burger implements Item {
@Override
public Packing packing() {
return new Wrapper();
}
@Override
public abstract float price();
}
ColdDrink.java
public abstract class ColdDrink implements Item {
@Override
public Packing packing() {
return new Bottle();
}
@Override
public abstract float price();
}
4.2.4 步骤 4 . 创建扩展了 Burger 和 ColdDrink 的实体类
VegBurger.java
public class VegBurger extends Burger {
@Override
public float price() {
return 25.0f;
}
@Override
public String name() {
return "Veg Burger";
}
}
ChickenBurger.java
public class ChickenBurger extends Burger {
@Override
public float price() {
return 50.5f;
}
@Override
public String name() {
return "Chicken Burger";
}
}
Coke.java
public class Coke extends ColdDrink {
@Override
public float price() {
return 30.0f;
}
@Override
public String name() {
return "Coke";
}
}
Pepsi.java
public class Pepsi extends ColdDrink {
@Override
public float price() {
return 35.0f;
}
@Override
public String name() {
return "Pepsi";
}
}
4.2.5 步骤 5 . 创建一个 Meal 类,带有上面定义的 Item 对象
Meal.java
import java.util.ArrayList;
import java.util.List;
public class Meal {
private List<Item> items = new ArrayList<Item>();
public void addItem(Item item){
items.add(item);
}
public float getCost(){
float cost = 0.0f;
for (Item item : items) {
cost += item.price();
}
return cost;
}
public void showItems(){
for (Item item : items) {
System.out.print("Item : "+item.name());
System.out.print(", Packing : "+item.packing().pack());
System.out.println(", Price : "+item.price());
}
}
}
4.2.6 步骤 6 . 创建一个 MealBuilder 类,实际的 builder 类负责创建 Meal 对象
MealBuilder.java
public class MealBuilder {
public Meal prepareVegMeal (){
Meal meal = new Meal();
meal.addItem(new VegBurger());
meal.addItem(new Coke());
return meal;
}
public Meal prepareNonVegMeal (){
Meal meal = new Meal();
meal.addItem(new ChickenBurger());
meal.addItem(new Pepsi());
return meal;
}
}
4.2.7 步骤 7 . BuiderPatternDemo 使用 MealBuilder 来演示建造者模式(Builder Pattern)
BuilderPatternDemo.java
public class BuilderPatternDemo {
public static void main(String[] args) {
MealBuilder mealBuilder = new MealBuilder();
Meal vegMeal = mealBuilder.prepareVegMeal();
System.out.println("Veg Meal");
vegMeal.showItems();
System.out.println("Total Cost: " +vegMeal.getCost());
Meal nonVegMeal = mealBuilder.prepareNonVegMeal();
System.out.println("\n\nNon-Veg Meal");
nonVegMeal.showItems();
System.out.println("Total Cost: " +nonVegMeal.getCost());
}
}
4.2.8 步骤 8 . 执行程序,输出结果
Veg Meal
Item : Veg Burger, Packing : Wrapper, Price : 25.0
Item : Coke, Packing : Bottle, Price : 30.0
Total Cost: 55.0
Non-Veg Meal
Item : Chicken Burger, Packing : Wrapper, Price : 50.5
Item : Pepsi, Packing : Bottle, Price : 35.0
Total Cost: 85.5
5. 原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
5.1 概述
- 意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
- 主要解决:在运行期建立和删除原型。
- 何时使用:
- 当一个系统应该独立于它的产品创建,构成和表示时。
- 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
- 为了避免创建一个与产品类层次平行的工厂类层次时。
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
- 如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。
- 关键代码:
- 实现克隆操作,在 JAVA 实现 Cloneable 接口,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。
- 原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。
- 应用实例:
- 细胞分裂。
- JAVA 中的 Object clone() 方法。
- 优点:
- 性能提高。
- 逃避构造函数的约束。
- 缺点:
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
- 必须实现 Cloneable 接口。
- 使用场景:
- 资源优化场景。
- 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
- 性能和安全要求的场景。
- 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 一个对象多个修改者的场景。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
- 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
- 注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
5.2 例子
我们将创建一个抽象类 Shape
和扩展了 Shape
类的实体类。下一步是定义类 ShapeCache
,该类把 shape 对象存储在一个 Hashtable
中,并在请求的时候返回它们的克隆。
PrototypePatternDemo
类使用 ShapeCache
类来获取 Shape
对象。
5.2.1 步骤 1 . 创建一个实现了 Cloneable 接口的抽象类
Shape.java
public abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void draw();
public String getType(){
return type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
5.2.2 步骤 2 . 创建扩展了上面抽象类的实体类
Rectangle.java
public class Rectangle extends Shape {
public Rectangle(){
type = "Rectangle";
}
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
Square.java
public class Square extends Shape {
public Square(){
type = "Square";
}
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
Circle.java
public class Circle extends Shape {
public Circle(){
type = "Circle";
}
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
5.2.3 步骤 3 . 创建一个类,从数据库获取实体类,并把它们存储在一个 Hashtable 中
ShapeCache.java
import java.util.Hashtable;
public class ShapeCache {
private static Hashtable<String, Shape> shapeMap
= new Hashtable<String, Shape>();
public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone();
}
// 对每种形状都运行数据库查询,并创建该形状
// shapeMap.put(shapeKey, shape);
// 例如,我们要添加三种形状
public static void loadCache() {
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(),circle);
Square square = new Square();
square.setId("2");
shapeMap.put(square.getId(),square);
Rectangle rectangle = new Rectangle();
rectangle.setId("3");
shapeMap.put(rectangle.getId(),rectangle);
}
}
5.2.4 步骤 4 . PrototypePatternDemo 使用 ShapeCache 类来获取存储在 Hashtable 中的形状的克隆
PrototypePatternDemo.java
public class PrototypePatternDemo {
public static void main(String[] args) {
ShapeCache.loadCache();
Shape clonedShape = (Shape) ShapeCache.getShape("1");
System.out.println("Shape : " + clonedShape.getType());
Shape clonedShape2 = (Shape) ShapeCache.getShape("2");
System.out.println("Shape : " + clonedShape2.getType());
Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
System.out.println("Shape : " + clonedShape3.getType());
}
}
5.2.5 步骤 5 . 执行程序,输出结果
Shape : Circle
Shape : Square
Shape : Rectangle