共计 6204 个字符,预计需要花费 16 分钟才能阅读完成。
单例设计模式
个别某些的类只须要一个就够了,反复创立他们将破费大量资源时,能够应用单例模式。比方某些工厂类、工具类。
饿汉式
动态常量
/**
* @author objcfeng
* @description 饿汉式——动态常量
* @Step:
* 1. 私有化结构器
* 2. 创立公有类实例
* 3. 私有办法返回类实例
* @date 2020/10/13
*/
public class HungryMan01 {private static final HungryMan01 instance=new HungryMan01();
private HungryMan01() {}
public static HungryMan01 getInstance(){return instance;}
}
步骤:
- 私有化结构器 阻止内部代码通过结构器构建类实例;
- 创立公有类实例 的成员变量申明为 动态常量;
- 通过一个 私有的静态方法getInstance 提供类实例。
留神:提供类实例的办法肯定是动态的,不然无法访问到这个办法。
动态代码块
/**
* @author objcfeng
* @description 饿汉式——动态代码块
* @date 2020/10/13
*/
public class HungryMan02 {
private static final HungryMan02 instance;
private HungryMan02() {}
static {instance=new HungryMan02();
}
public static HungryMan02 getInstance(){return instance;}
}
步骤差不多,只是把创立公有类实例的过程放在动态代码块中执行罢了。
饿汉式都是线程平安的。
懒汉式
非线程平安的懒汉式
/**
* @author objcfeng
* @description 非线程平安的懒汉式
* @date 2020/10/13
*/
@NotThreadSafe
public class LazyMan {
private static LazyMan instance=null;
private LazyMan() {}
public LazyMan static getInstance(){if (instance==null){instance=new LazyMan();
}
return instance;
}
}
懒汉式解决了饿汉式类加载时就创立了实例而不论实例是否被用到的问题。
然而这里的懒汉式是存在线程平安问题的,在并发环境下,当线程 A 执行到 if (instance==null)
后果为 true, 并进入 if 的执行代码块中,但在执行实例化操作前,CPU 调配给了了线程 B ,线程 B 开始执行,判断 if (instance==null)
为 true,并执行实例化操作生成类实例 B。而后 CPU 重新分配给线程 A, A 曾经通过判断了,所以又执行了一次实例化操作生成类实例 A。这时便有两个类实例了。
代码验证:
/**
* @author objcfeng
* @description 非线程平安的懒汉式
* @date 2020/10/13
*/
@NotThreadSafe
public class LazyMan {
private static LazyMan instance=null;
private LazyMan() {}
public static LazyMan getInstance() throws InterruptedException {if (instance==null){System.out.println(Thread.currentThread().getName()+"进入办法 getInstance()...");
if(Thread.currentThread().getName().equals("A")){TimeUnit.SECONDS.sleep(1);
}
instance=new LazyMan();}
return instance;
}
public static void main(String[] args) {new Thread(()->{
try {LazyMan lazyMan1=LazyMan.getInstance();
System.out.println(lazyMan1);
} catch (InterruptedException e) {e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {LazyMan lazyMan2=LazyMan.getInstance();
System.out.println(lazyMan2);
} catch (InterruptedException e) {e.printStackTrace();
}
},"B").start();}
}
输入
A 进入办法 getInstance()…
B 进入办法 getInstance()…
创立型. 懒汉式.LazyMan@43d83830
创立型. 懒汉式.LazyMan@6218fb2e
由之前的剖析能够看出,懒汉式不是线程平安的要害有两点,一是在线程 A 执行完判断未执行实例化时就有线程 B 进入办法;二是当线程 A 从新取得 CPU 资源后没有再次判断 if 内的条件还是否为 true;解决二者之一,就能解决懒汉式非线程平安的问题了。
首先来尝试解决二号问题,首先再次判断在 if 内再应用一次 if 必定是不行的,起因还是会在通过判断的时候可能产生 CPU 资源的切换。我记得在解决虚伪唤醒的时候曾将 if 改为 while 来再次执行判断,能够一试:
@NotThreadSafe
public class LazyMan02 {
private static LazyMan02 instance=null;
private LazyMan02() {}
public static LazyMan02 getInstance() throws InterruptedException {while (instance==null){System.out.println(Thread.currentThread().getName()+"进入办法 getInstance()...");
if(Thread.currentThread().getName().equals("A")){Thread.yield();// 让线程变为就绪态
}
System.out.println(Thread.currentThread().getName()+"执行实例化");
instance=new LazyMan02();}
return instance;
}
public static void main(String[] args) {new Thread(()->{
try {LazyMan02 lazyMan1= LazyMan02.getInstance();
System.out.println(lazyMan1);
} catch (InterruptedException e) {e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {LazyMan02 lazyMan2= LazyMan02.getInstance();
System.out.println(lazyMan2);
} catch (InterruptedException e) {e.printStackTrace();
}
},"B").start();}
}
输入
A 进入办法 getInstance()…
B 进入办法 getInstance()…
B 执行实例化
A 执行实例化
创立型. 懒汉式.LazyMan02@4cc3632
创立型. 懒汉式.LazyMan02@7245297c
发现不行,看来应用 while 再次进行判断只有在应用期待、唤醒(wait、notify)时能力起成果。
第一个问题解决起来就简略了,增加同步,在线程 A 没进去前限度其余线程进入就完事了。见下章。
线程平安的懒汉式
@ThreadSafe
public class LazyMan03 {
private static LazyMan03 instance=null;
private LazyMan03() {}
public static synchronized LazyMan03 getInstance() throws InterruptedException {System.out.println(Thread.currentThread().getName()+"进入办法 getInstance()...");
if (instance==null){if(Thread.currentThread().getName().equals("A")){TimeUnit.SECONDS.sleep(1);
}
instance=new LazyMan03();}
return instance;
}
}
如上加同步(synchronized)就完了。
输入:
A 进入办法 getInstance()…
B 进入办法 getInstance()…
创立型. 懒汉式. 非线程平安.LazyMan03@4cc3632
创立型. 懒汉式. 非线程平安.LazyMan03@4cc3632
可见两个类实例是一样的。
尽管解决起来简略然而毛病也很大,在并发大的时候,每个线程都要等进入 getInstance()内的办法执行完后才有可能进入办法开始获取实例。性能很低。
双重查看
/**
* @author objcfeng
* @description 非线程平安的懒汉式
* @date 2020/10/13
*/
@ThreadSafe
public class LazyMan04 {
private static volatile LazyMan04 instance=null;
private LazyMan04() {}
public static LazyMan04 getInstance() throws InterruptedException {System.out.println(Thread.currentThread().getName()+"进入办法 getInstance()...");
if (instance==null){synchronized (LazyMan04.class){if (instance==null){if(Thread.currentThread().getName().equals("A")){TimeUnit.SECONDS.sleep(1);
}
instance=new LazyMan04();}
}
}
return instance;
}
public static void main(String[] args) {new Thread(()->{
try {DoubleCheck doubleCheck= DoubleCheck.getInstance();
System.out.println(doubleCheck);
} catch (InterruptedException e) {e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {DoubleCheck doubleCheck= DoubleCheck.getInstance();
System.out.println(doubleCheck);
} catch (InterruptedException e) {e.printStackTrace();
}
},"B").start();}
}
输入:
A 进入办法 getInstance()…
B 进入办法 getInstance()…
创立型. 懒汉式. 线程平安.LazyMan04@6218fb2e
创立型. 懒汉式. 线程平安.LazyMan04@6218fb2e
双重查看,顾名思义就是应用了两次 if 判断,当类实例还未创立进去时,线程通过第一个判断进入同步代码块,进行第二次判断和创立类实例,保障了线程在判断后和执行创立前不会有其余线程进入代码块,保障了线程安全性。
另外,在类实例创立进去后,所有线程都不会再进入同步代码块,保障了效率。
为什么要应用 volatile?
因为 new 不是原子操作。
public class Test {public static void main(String[] args) {new Object();
}
}
应用 javac 命令编译.java 文件为.class 文件
javac -encoding UTF-8 Test.java
再应用 javap 命令反编译.class 文件
javap -c Test
D:WorkSpaceJavaJava 设计模式 MDsrc 创立型双重查看 >javac -encoding UTF-8 Test.java
D:WorkSpaceJavaJava 设计模式 MDsrc 创立型双重查看 >javap -c Test.class
Compiled from "Test.java"
public class 创立型. 双重查看.Test {public 创立型. 双重查看.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
// 创建对象实例分配内存
0: new #2 // class java/lang/Object
// 复制栈顶地址,并将其压入栈顶
3: dup
// 调用结构器办法,初始化对象
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: pop
8: return
}
线程 1 | 线程 2 | |
---|---|---|
t1 | 分配内存 | |
t2 | 变量赋值 | |
t3 | 判断对象是否为 null | |
t4 | 因为对象不为 null,拜访该对象 | |
t5 | 初始化对象 |
如果线程 1 获取到锁进入创建对象实例,这个时候产生了指令重排序。当线程 1 执行到 t3 时刻,线程 2 刚好进入,因为此时对象曾经不为 Null,所以线程 2 能够自在拜访该对象。而后 该对象还未初始化,所以线程 2 拜访时将会产生异样。(参考自 https://www.cnblogs.com/zhuifeng523/p/11360012.html)
volatile 作用
正确的双重查看锁定模式须要须要应用 volatile。volatile 次要蕴含两个性能。
- 保障可见性。应用 volatile 定义的变量,将会保障对所有线程的可见性。
- 禁止指令重排序优化。
因为 volatile 禁止对象创立时指令之间重排序,所以其余线程不会拜访到一个未初始化的对象,从而保障安全性。