单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 避免对资源的多重占用(比如写文件操作)。
**缺点:**没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
1.1、饿汉式- 优点:实现简单,不存在多线程问题,直接声明一个私有对象,然后对外提供一个获取对象的方法。
- 缺点:class 类在被加载的时候创建Singleton实例,如果对象创建后一直没有使用,则会浪费很大的内存空间,此方法不适合创建大对象。
public class Singleton
{
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
1.2、懒汉式(线程不安全)
- 优点:节省内存空间,在使用的时候才会创建;
- 缺点:在多线程下,可能会创建多个实例
public class Singleton
{
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.3.1、指令重排
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
数据依赖关系的例子:
//写一个变量之后,再读这个位置。 //代码1 与代码2 这两行代码不会发生指令重排,因为他们具有依赖性。 int a,b; a = 1;//1 b = a;//2
//写一个变量之后,再写这个变量。 //代码1 与代码2 这两行代码不会发生指令重排,因为他们具有依赖性。 int a,b; a = 1;//1 a = 2;//2
//读一个变量之后,再写这个变量。 //代码1 与代码2 这两行代码不会发生指令重排,因为他们具有依赖性。 a = b;//1 b = 1;//2
//代码1、代码2可能发生指令重排,他们不具备依赖性。 //但是代码3 一定会排在 1、2的后面,因为3和 1、2具备依赖性。 int a,b,c; a = 1;//1 b = 2;//2 c = a + b;//3
多线程环境下指令重排可能引发的问题:
class OrderExample {
int a = 0;
//默认false,无数据,不能读取
boolean flag = false;
//写数据的方法,假如有一个线程A不停的写数据
public void writer() {
// 以下两句执行顺序可能会在指令重排等场景下发生变化
a = 1;//写入数据
flag = true;//true,标识着已经有数据可以读取了
}
//读取数据的方法,假如有一个线程B不停的读取数据
public void reader() {
//先判断是否有数据可以读取
//如果线程A写数据时,writer方法中的代码发生了指令重排,先执行 flag=true
//那么,假如此时线程B刚好 if(flag)判断,就会进入if内部,而此时线程A还没有对变量a进行设值,此时线程B拿到的是错误的值进行运算
if (flag) {
//进行相关运算等
int i = a + 1;
//TODO ...更多操作
}
}
}
如何解决多线程环境下指令重排引发的问题?解决方案就是大名鼎鼎的volatile关键字
1.3.2、volatile关键字1.3.2.1、volatile的禁止指令重排特性
- volatile能禁止指令重排
- volatile关键能保证变量的”可见性“
1.3.2.2、volatile的“可见性”重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
说可见性之前,我们先来了解一下java虚拟机中线程的内存模型
- 每一个线程运行时都有一个线程栈
- 线程栈保存了线程运行时候变量值信息。
- 当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值
- 在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
下面一幅图描述这写交互!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ciim7DB-1655812904816)(.imgs线程内存模型.jpg)]
- 在多线程环境下,若多个线程同时在操作主内存的count变量,就可能会导致每个线程中操作的count的值不是最新的
- 如果给count变量修饰了 volatile关键字,那么就能保证此变量的“可见性”
- 当写一个volatile变量时,JMM(java内存模型)会把该线程本地内存中的变量强制刷新到主内存中去;
- 这个写会操作会导致其他线程中的volatile变量缓存无效。
class OrderExample {
int a = 0;
//使用volatile修饰,既能保证此变量在多线程环境下的可见性,还能够进制指令重排
volatile boolean flag = false;
//写数据的方法,假如有一个线程A不停的写数据
public void writer() {
a = 1;
//禁止指令重排,此变量之前的操作必须是完成后才会轮到它
flag = true;
}
//读取数据的方法,假如有一个线程B不停的读取数据
public void reader() {
//保证了变量的可见性,其他线程修改了flag变量,当前线程也能即时读取到
if (flag) {
int i = a + 1;
//TODO ...更多操作
}
}
}
1.3、懒汉式(线程安全)
- 优点:支持多线程,且以懒汉式的方式加载,不浪费内存空间。
- 缺点:将 synchronized 块加在方法上,会影响并发量,每次调用getInstance()方法都会线程同步,效率十分低下。最重要的是,当创建好实例对象之后,就不必继续进行同步了。
public class Singleton
{
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.4、懒汉式-双重校验锁(线程安全,推荐)
- 优点:支持多线程,并发量高,且以懒汉式加载,不浪费内存空间。
- 缺点:非要说缺点的话,就是实现比较麻烦。
public class Singleton
{
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?
java中 Object obj = new Object() 实际上并不是原子性的操作,它可以被分解为如下3行伪代码:
//定义引用类型 Object obj = null; //1:分配对象的内存空间 memory = allocate(); //2:初始化对象 ctorInstance(memory); //3:设置obj指向刚分配的内存地址 obj = memory;
上面三行伪代码中的2和3之间,可能会被重排序,即编译器或处理器为提高性能改变代码执行顺序
在单线程程序下,重排序不会对最终结果产生影响,但是并发的情况下,可能会导致某些线程访问到未初始化的变量。
解决方案就是大名鼎鼎的volatile关键字
public class Singleton
{
//volatile修饰此变量,禁止指令重排
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
1.5、Holder单例模式
特点:既实现懒加载,性能好,线程安全
public class HolderSingleton {
// 外部类的私有构造方法
private HolderSingleton() {
}
// 外部类静态方法,通过静态内部类的静态成员变量获取
public static HolderSingleton getInstance() {
//java类加载机制,使用此类静态变量时会加载此类,并对其进行初始化
return Holder.instance;
}
// 静态内部类内部加载的时候创建外部类,只加载一次,所以只创建一个
private static class Holder {
private static HolderSingleton instance = new HolderSingleton();
}
}
2、代理模式注:之后我们的service、dao层等实现类全部使用单例模式。
网上概念太正规了,我这里就简单点说吧
有一件事情原本是你自己应该要做的,但是由于某些原因,你自己做起来很麻烦,这时候就可以叫别人来帮你(代理你)做。
如:结婚,你可以花点钱,叫婚庆公司代理你置办酒席,司仪,车队等琐事,你只要专心等待婚庆公司叫你洞房就成。
代理模式的组成:
- 抽象角色:通过接口或抽象类声明真实角色实现的业务方法。(如:结婚,这本身是件很抽象的事情,很多人都可以结婚的)
- 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加额外的操作。(如:婚庆公司,他可以帮你做一些额外的琐事)
- 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用(如:结婚的真正的人)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUziPFSN-1655812904818)(.imgs代理模式-1.png)]
2.1、静态代理优点:可以做到在不修改目标对象的功能前提下,对目标功能扩展.
缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.
- 抽象角色—约会接口
public interface AppointmentService {
public void play1();
public void play2();
}
- 真实角色 — 约会的人(我)
public class MyServiceImpl implements AppointmentService {
public void play1() {
System.out.println("==== 今天和女朋友看了一场电影");
}
public void play2() {
System.out.println("==== 今天和女朋友去做了过山车");
}
}
- 代理角色 — 可以在约会之前或结束额外处理一些我并不关心的琐事
public class StaticProxy implements AppointmentService {
//真实的被代理的对象(我)
private AppointmentService target = null;
public StaticProxy(AppointmentService target){
this.target = target;
}
public void play1() {
System.out.println("*** 女朋友起床了");
System.out.println("*** 女朋友洗漱、化妆");
//调用真实对象(我) 周一约会的方法
this.target.play1();
System.out.println("*** 女朋友回家卸妆");
System.out.println("*** 女朋友有睡觉了");
}
public void play2() {
System.out.println("*** 女朋友起床了");
System.out.println("*** 女朋友洗漱、化妆");
//调用真实对象(我) 周二约会的方法
this.target.play2();
System.out.println("*** 女朋友回家卸妆");
System.out.println("*** 女朋友有睡觉了");
}
}
- 最后测试使用
public class Test {
public static void main(String[] args) {
//真实的约会对象---我
MyServiceImpl my = new MyServiceImpl();
//约会的代理对象 --- 代理的是我
StaticProxy proxy = new StaticProxy(my);
System.out.println("------周一");
//开始约会 -- 周一玩啥呢?
proxy.play1();
System.out.println("------周二");
//开始约会 -- 周二玩啥呢?
proxy.play2();
}
}
打印结果如下
------周一 *** 女朋友起床了 *** 女朋友洗漱、化妆 ==== 今天和女朋友看了一场电影 *** 女朋友回家卸妆 *** 女朋友有睡觉了 ------周二 *** 女朋友起床了 *** 女朋友洗漱、化妆 ==== 今天和女朋友去做了过山车 *** 女朋友回家卸妆 *** 女朋友有睡觉了
2.2、动态代理从上面的例子中我们可以很明显的看到静态代理的缺点,一旦我们增加新的约会方法,那么代理类中也要增加其实现。万一我们的业务系统中需要代理的类很多,需要代理的方法也很多,我们将要去维护大量的代理类。
如何解决静态代理中的缺点呢?答案是可以使用动态代理方式
- 代理对象,不需要实现接口
- 代理对象的生成,是利用JDK的API(java.lang.reflect.Proxy),动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
- 动态代理也叫做:JDK代理,接口代理
jdk反射实现动态代理核心接口:
InvocationHandler(方法调用处理器)。实现此接口后,在调用指定java类方法时会进入此处理器。然后进行额外处理
jdk创建代理对象核心方法:
Proxy.newProxyInstance(类加载器对象,被拦截的接口class,被拦截时处理器实现对象)
- 我们来看看动态代理如何做
- 沿用上面的例子,将静态代理类改为动态代理类
- 注意实现方法处理器的接口
public class DynamicProxy implements InvocationHandler {
//目标对象--真实的代理对象
private AppointmentService target = null;
public DynamicProxy(AppointmentService target){
this.target = target;
}
//获取代理对象
public AppointmentService getProxyInstance(){
//获取class信息
Class extends AppointmentService> clz = this.target.getClass();
AppointmentService proxy =
(AppointmentService)Proxy.newProxyInstance(
clz.getClassLoader(),
clz.getInterfaces(),
this
);
//返回代理对象
return proxy;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("*** 女朋友起床了");
System.out.println("*** 女朋友洗漱、化妆");
//调用真实对象(我) 约会的方法
Object ret = method.invoke(this.target,args);
System.out.println("*** 女朋友回家卸妆");
System.out.println("*** 女朋友有睡觉了");
//返回
return ret;
}
}
- 使用
public class Test {
public static void main(String[] args) {
//真实的约会对象---我
MyServiceImpl my = new MyServiceImpl();
//约会的代理帮助类/处理器实现类
DynamicProxy proxyHandler = new DynamicProxy(my);
//利用jdk反射,获取代理对象--- 代理的是我
AppointmentService proxy = proxyHandler.getProxyInstance();
System.out.println("------周一");
//开始约会 -- 周一玩啥呢?
proxy.play1();
System.out.println("------周二");
//开始约会 -- 周二玩啥呢?
proxy.play2();
}
}
2.3、CGLib代理
- 上面的静态代理和动态代理模式都是要求目标对象是实现了接口
- 如果我的目标对象没有实现任何接口,如何来做代理模式?
正常情况下是做不了的,但是有大佬开发了一款CGLib(一个强大的、高性能的代码生成库 )
CGLib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展(它可以在运行期扩展java类与实现java接口).它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)
- 继续沿用上面的例子,注意,MyServiceImpl 类中没有实现任何接口了
- 注意项目中引入cglib-2.2.2.jar、asm-3.3.1.jar 这两个jar
public class MyServiceImpl {
public void play1() {
System.out.println("==== 今天和女朋友看了一场电影");
}
public void play2() {
System.out.println("==== 今天和女朋友去做了过山车");
}
}
- CGLib代理
import javafx.beans.property.adapter.ReadOnlyJavaBeanBooleanProperty;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CGLibProxy {
//CGLib代理只需要提供代理目标的 class即可
private Class> targetClz = null;
public CGLibProxy(Class> targetClz){
this.targetClz = targetClz;
}
public Object getCGlibProxy(){
//对java类的扩展器/增强器
Enhancer enhancer = new Enhancer();
//针对哪个class进行增强
enhancer.setSuperclass(targetClz);
//设置增强/扩展 的处理
enhancer.setCallback(
//匿名类,此类实现了 MethodInterceptor 接口
new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("*** 女朋友起床了");
System.out.println("*** 女朋友洗漱、化妆");
//调用真实对象(我) 约会的方法
Object ret = methodProxy.invokeSuper(o,args);
System.out.println("*** 女朋友回家卸妆");
System.out.println("*** 女朋友有睡觉了");
//返回
return ret;
}
}
);
//利用增强器 创建targetClz的代理对象,并返回
return enhancer.create();
}
}
- 使用
public class Test {
public static void main(String[] args) {
//利用cglib创建一个 MyServiceImpl 的代理对象
MyServiceImpl proxy =
(MyServiceImpl)new CGLibProxy(MyServiceImpl.class).getCGlibProxy();
System.out.println("------周一");
//开始约会 -- 周一玩啥呢?
proxy.play1();
System.out.println("------周二");
//开始约会 -- 周二玩啥呢?
proxy.play2();
}
}



