volatile关键字经常用在多个线程并发写_多线程安全的单例模式

volatile关键字经常用在多个线程并发写_多线程安全的单例模式一.事先准备首先准备一个运行用的代码:publicclassSingleton{publicstaticvoidmain(String[]args){Thread[]thre

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

一.事先准备

首先准备一个运行用的代码:

public class Singleton {

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new myThread();
        }

        for (Thread thread : threads) {
            thread.start();
        }
    }

}

class myThread extends Thread {
    @Override
    public void run() {
        //打印实例的hashCode
        //运行不同的示例时替换类名即可
        System.out.println(Obj.getObj().hashCode());
    }
}

以下代码都在此基础上运行。

二.饿汉式

饿汉式是天生线程安全的。

/*
* 饿汉式
* */
class Obj{

    //一开始就直接初始化对象
    private static Obj obj = new Obj();

    //私有有化构造方法避免再new出一个对象
    private Obj() {
    }

    //通过get方法获取实例
    public static Obj getObj(){
        return obj;
    }
}

//输出
471895473
471895473
471895473
471895473
471895473
471895473
471895473
471895473
471895473
471895473

对应饿汉式,因为饿汉式在类加载时创建实例,而一个类在生命周期中只被加载一次,也就是说,饿汉式在线程访问前就已经创建好了唯一的那个实例,因此无论多少个线程同时访问,最终获取到的都是一个实例。

当然,实际上饿汉式可能导致系统最终创建了太多无用实例,所以懒汉式仍然还是必要的。

三.懒汉式

1.传统懒汉式

传统懒汉式是非线程安全的,示例如下:

/*
* 传统懒汉式
* */
class Obj2 {

    private static Obj2 obj;

    private Obj2() {
    }

    //get方法进行同步
    public static Obj2 getObj(){
        if (obj == null){
            obj = new Obj2();
        }
        return obj;
    }
}

//输出
1217036164 //创建了多个实例
102629730
1217036164
102629730
102629730
102629730
102629730
102629730

对于传统懒汉式,因为当某个线程创建实例但是还没来得及写入堆内存时,可能已经有多个线程进入了if代码块,因此可能最后会创建多个实例。

2.使用内部类

因为饿汉式是天生线程的,所以也可以通过内部类实现:

/**
 * 内部类
 */
class Obj5 {
    
    //内部类
    private static class InitBean {
        //将外部类作为成员变量,饿汉式创建
        private static Obj5 obj5 = new Obj5();
    }

    private Obj5() {
    }

    //工厂方法,实际上是懒汉式初始化内部类,并且从内部类获取实例
    public static Obj5 getObj() {
        return InitBean.obj5;
    }
}

//输出
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164
1217036164

对于这种方法的解释是这样的:

当调用getObj()方法时,会触发InitBean类的初始化。

由于Obj5是InitBean的类成员变量,因此在JVM调用InitBean类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。

总结一下,就是jvm会保障多线程情况下类的正确初始化,所以借助这一点,我们创建一个内部类,然后让内部类初始化的时候创建唯一个实例作为成员变量,而通过外部类的工厂方法来触发内部类的初始化并获取实例。

3.使用synchronize同步工厂方法

解决传统懒汉式问题的方法很简单,那就是直接给工厂方法加上synchronize关键字变成同步方法:

...
//直接对工厂方法进行同步
public static synchronized Obj2 getObj(){
    if (obj == null){
        obj = new Obj2();
    }
    return obj;
}

//输出
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314

从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,所以我们还得进一步缩小锁范围,因此可以考虑使用同步代码块来实现。

4.双重检查

要说锁粒度最小,那就是只锁实例化的代码,然后只在实例化前进行一次检查:

/*
 * 不双重检查
 * */
class Obj4 {

    private volatile static Obj4 obj4;

    private Obj4() {
    }

    public static Obj4 getObj() {
        //检查是否已有实例
        if (obj4 == null) {
            //没有实例就获取锁进行准备实例化
            synchronized (Obj3.class) {
                obj4 = new Obj4();
            }

        }
        return obj4;
    }
}

//输出
2140075580 //创建了多个实例
1285201119
1217036164
102629730
102629730
1000492474
1217036164
569477593
1217036164
1217036164

实际上这样跟传统懒汉式并无区别,因为只检查一次的话,同样会面对第一个示例还在创建,结果其他线程直接通过了if判断的情况,所以我们需要再在同步代码块中进行一次检查

...
//对工厂方法进行双重检查
public static Obj3 getObj() {
    //检查是否已有实例
    if (obj3 == null) {
        //没有实例就获取锁进行准备实例化
        synchronized (Obj3.class) {
            //再判断是否已经有获取过锁的线程实例化了对象
            if (obj3 == null) {
                obj3 = new Obj3();
            }
        }

    }
    return obj3;
}

//输出
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314
1696172314

四.双重检查中的volatile关键字

在双重检查中,必须使用volatile关键字修饰引用的单例,目的是jvm在创建实例的时候进行禁止指令重排

要理解指令重排,必须先理解jvm是如何处理new操作的:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 使变量指向对象

但是由于jvm会因为进行指令重排,所一实际上new操作的步骤可能发生变化

  1. 分配对象的内存空间
  2. 使变量指向对象
  3. 初始化对象

这就会导致现有的代码出现这样的问题:

  1. 线程一最先获取锁并执行初始化代码,但是发生了指令重排
  2. jvm执行完1后,先执行了3,但是2还没来得及执行锁就被线程二抢占了
  3. 此时线程二能够获取实例了,通过了代码检查,但是线程二获取的这个实例还没有初始化,是个不完整的实例
  4. 线程一抢占锁,执行构造函数完成变量初始化

显然,如果线程二拿着一个不完整的实例进了业务代码,就会引发各种bug,这种隐患正是由指令重排引起的,所以我们需要使用volatile指令修饰引用的单例

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/170821.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • UDP协议的详细解析「建议收藏」

    UDP协议的详细解析「建议收藏」UDP数据报一、UDP的概述二、UDP的首部格式UDP校验

  • restsharp.dll_restbed

    restsharp.dll_restbed一、RestSharp简绍RestSharp是一个轻量的,不依赖任何第三方的组件或者类库的Http的组件。RestSharp具体以下特性;1、支持.NET3.5+,Silverlight4,WindowsPhone7,Mono,MonoTouch,MonoforAndroid,CompactFramework3.5等  2、通过NuGet方便引入到任何项目(In…

  • Centos安装字体

    Centos安装字体

  • pycharm常见镜像源「建议收藏」

    pycharm常见镜像源「建议收藏」清华:https://pypi.tuna.tsinghua.edu.cn/simple阿里云:http://mirrors.aliyun.com/pypi/simple/中国科技大学:https://pypi.mirrors.ustc.edu.cn/simple/使用:pipinstall-i镜像源包…

  • Linux 配置Node环境变量[通俗易懂]

    Linux 配置Node环境变量[通俗易懂]修改/etc/profile文件,在末尾添加以下内容exportNODE_HOME=/usr/local/node//Node所在路径exportPATH=$NODE_HOME/bin:$PATH修改完成后需要重新登陆才能生效,也可以执行命令source/etc/profile或者./etc/profile来生效(注意。与/etc/profile中有一个空格)查看P…

  • JS中鼠标拖拽div(2)(setCapture()方法和releaseCapture()方法)

    JS中鼠标拖拽div(2)(setCapture()方法和releaseCapture()方法)接着鼠标拖拽div(1)解决问题,当在拖拽事件所在的页面按下键盘的ctrl+A全选后,再去拖拽div,浏览器会默认去搜索网页中的内容,拖拽功能就会失效,(搜索网页内容是浏览器的默认行为,所以要想不发生这种情况,就得将其取消,是谁执行之后触发了浏览器的默认行为,就在谁里面returnfalse即可取消浏览器的默认行为,但这种方式ie8及以下的版本不支持。)在ie8及以下版本浏览器中,如果调用了元素的setCapture()方法,那么点击任何事物都会来执行这个元素绑定的响应函数。例如:btn.oncl

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号