关于设计模式:Java设计模式单例设计模式

单例设计模式

个别某些的类只须要一个就够了,反复创立他们将破费大量资源时,能够应用单例模式。比方某些工厂类、工具类。

饿汉式

动态常量
 /**
  * @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;
  }
 }

步骤:

  1. 私有化结构器阻止内部代码通过结构器构建类实例;
  2. 创立公有类实例的成员变量申明为动态常量
  3. 通过一个私有的静态方法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次要蕴含两个性能。

  1. 保障可见性。应用 volatile定义的变量,将会保障对所有线程的可见性。
  2. 禁止指令重排序优化。

因为 volatile禁止对象创立时指令之间重排序,所以其余线程不会拜访到一个未初始化的对象,从而保障安全性。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理