java多线程编程(二)----单例模式

一.java中的设计模式

单例模式是一种设计模式,就比如下棋的时候对于高手来说,每个人都会很多种棋谱,在比赛中按照棋谱的套路灵活应用,见招拆招。java中的设计模式就和棋谱一样,程序员按照棋谱来写代码能够保证下限。设计模式有很多种,之前有个大佬写了一本书有23种设计模式,不同的语言有不同的设计模式。对于新手来说最主要的是理解2种设计模式,单例模式和工产模式。

二.单例模式

单例模式是一种设计模式,用于确保一个类只能有一个实例,也就是只能new一次,并提供全局访问这个实例的方式。单例模式的基本思想是将类的实例化过程封装起来,使得整个程序中只有一个对象能够被创建。这个唯一的对象被称为单例对象,他可以被类的所有方法共享。

单例模式主要分为2种饿汉模式和懒汉模式。

一.饿汉模式

饿汉模式是在类加载的时候创建出实例

java 复制代码
class Single{
    private static Single single = new Single();
    public static Single create(){
       return single;
    }
    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);
    }
}


Singlel类带有static,类属性,由于每个类对象是单例的,类对象的属性(static),也就是单例的。代码的执行时机是在Single类被jvm加载的时候,Single类会在jvm第一次使用的时候加载。如果我们继续new对象会发生什么?

我们发现new对象的时候编译器报错,这是应为Single类的构造方法是私有的,出了该类则不能访问。那么一定不能访问吗?

尽管该类的构造方法是私有的,但是可以使用反射去访问,创造出多个实例。反射是属于非常规的编程手段,正常开发的时候,不应该或者慎用反射。滥用反射,会带来极大的风险,会让代码变的抽象难以维护。java也有其他方式实现单例模式不怕反射。

二.懒汉模式(非线程安全)

懒汉模式是第一次使用实例的时候就创建,能不创建就不创建。

java 复制代码
class Single{
    private static Single single = null;
    public static Single create(){
     if(single==null){
        single = new Single();
     }
     return single;
    }
    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);


    }
}

三.线程安全下的懒汉模式

对比一下饿汉模式和懒汉模式思考一下谁是线程安全的,谁是不安全的。

先看饿汉模式多个线程读取single的值,上篇我们讲线程安全的时候谈过,造成线程安全问题,其中一点是多个线程同时修改同一变量,而我们饿汉模式只是读并没有修改,所以饿汉模式是线程安全的。

接着看懒汉模式

懒汉模式中涉及3种操作,读取,if(比较),创建实例。读取操作是线程安全的,if(比较)不是原子性的,线程安全问题中也有一点不是原子性的操作也会造成线程安全问题。其原理是:

当还没有创建实例时,2个线程都在调用create方法,通过if语句去判断是否已经创建出实例,由于并没有创建,2个线程都进入if语句,创建2次实例。那么你会有此疑问,创建2次实例就创建2次,虽然创建了2次实例,但是第二次创建的引用给single本质还是只有一个实例。这种想法是正确的,但是创建实例在代码量很多的时候效率很低,所以我们应该避免创建2次实例,给代码加锁就能有效避免这个问题。

java 复制代码
class Single{
    private static Single single = null;
   
    public static Single create(){
       synchronized (Single.class) {
           if (single == null) {
               single = new Single();
           }
       }
     return single;
    }

    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);
        
    }
}

上述虽然给代码加了锁避免了多个线程访问的时候创建出多个对象,但这种情况在多线程访问的时候即使已经创建出实例但每次判断的时候都要给代码加锁,加锁本来是一件效率很低的事情,我们应该避免无脑加锁,那么怎么让没有创建出实例的时候加锁,后面每次判断的时候不加锁呢?

java 复制代码
class Single{
    private static Single single = null;

    public static Single create(){
        if(single==null) {
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
            }
        }
     return single;
    }

    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);
        
    }
}

多加一个if条件判断是否要加锁然后加锁。第一次创建实例的时候,假设2个线程走到第一个if语句,线程1先拿到锁进入第二个if语句创建出实例,解锁后第二个线程拿到锁,进入第二个if语句发现不满足条件不创建实例,当后续代码进入第一个if语句该实例已经不为空了,直接返回,所以不会在继续加锁。保证了只创建出一个实例,同时也在第一次的时候加了一次锁。

四.懒汉模式下的内存可见性和指令重排性

虽然防止了重复加锁,但加锁操作可能阻塞,当第一次加锁时,第二个if条件和第一个if条件可能会间隔非常多的时间,在这个很长的时间间隔中,可能别的线程就把single的值给改了,single获取不到新修改的值导致会出现错判,为了防止这个操作加上volatile,让线程时刻获取着single的值。

java 复制代码
class Single{
    private volatile static Single single = null;

    public static Single create(){
        if(single==null) {
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
            }
        }
     return single;
    }

    private Single(){

    }
}
public class Test12 {
    public static void main(String[] args) {
         Single str1=Single.create();
         Single str2 = Single.create();
        System.out.println(str1==str2);

    }
}

加上voliatle还有一个作用防止指令重排性。

五.指令重排性

指令重排性也是编译器的一种优化手段,保持原有的逻辑不变的情况下,对代码执行顺序进行调整,使调整后的执行效率更高。比如超市买菜,我想买西红柿,萝卜,黄瓜,菜花。

如果我按照自己想买的顺序走那么会绕一大圈,如果我按照此顺序:西红柿,黄瓜,菜花,萝卜这种顺序走效率会提高,编译器也是这样在原有的逻辑不变的情况下对顺序进行调整。

这个代码:1.给对象创建出内存空间,得到内存地址

2.在空间上调用构造方法,对对象进行初始化

3.把内存地址付给single引用。

正常顺序是1,2,3。但是编译器优化后可能1,3,2并且执行3之后,2之前出现了线程切换,此时还没来得及给对象初始化,就调用给别的线程了,如果给single加上voliatle之后就不会出现指令重排性了。

相关推荐
_oP_i27 分钟前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx30 分钟前
android sqlite 数据库简单封装示例(java)
android·java·数据库
bryant_meng32 分钟前
【python】OpenCV—Image Moments
开发语言·python·opencv·moments·图片矩
武子康1 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
若亦_Royi1 小时前
C++ 的大括号的用法合集
开发语言·c++
资源补给站2 小时前
大恒相机开发(2)—Python软触发调用采集图像
开发语言·python·数码相机
豪宇刘2 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意2 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
m0_748247552 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
6.942 小时前
Scala学习记录 递归调用 练习
开发语言·学习·scala