Java并发单例模式与synchronized关键字与voliate关键字

Posted 王六六的IT日常

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发单例模式与synchronized关键字与voliate关键字相关的知识,希望对你有一定的参考价值。

单例模式-饿汉式(线程安全)

上来不管三七二十一直接创建对象再说。

1.先创建一个私有的构造函数(防止其它地方直接实例化)
2.定义私有变量
3.提供公共的获取实例的方法

为什么线程安全:
类加载的方式是按需加载,且只加载一次

因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。单例就是该类只能返回一个实例。

换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。

也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

出现问题: 一开始就实例化对象,如果实例化过程非常耗时,并且最后这个对象若没有被使用,白白造成资源浪费
解决方法: 不用提前实例化对象,在真正使用的时候再实例化就可以

单例模式-懒汉式(“懒加载”或者“延时加载”)

当程序启动之后并不会进行初始化,在什么时候调用什么时候初始化。
类加载的时候,没有立刻实例化,第一次调用getInstance()的时候,才真的实例化。

出现问题: 假如有多个线程中都调用了getInstance方法,那么都走到 if (instance== null) 判断时,可能同时成立,因为instance初始化时默认值是null。这样会导致多个线程中同时创建instance对象,instance对象被创建了多次,违背了只创建一个instance对象的初衷。
解决方法: 双重校验锁实现对象单例(线程安全)

单例模式-双重校验锁


出现问题:getInstance方法的这段代码,是按1、2、3、4、5这种顺序写的,希望也按这个顺序执行。但是java虚拟机实际上会做一些编译器优化,对一些代码指令进行重排。重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例。

编译器优化:
只有第一次读操作从内存中读取数据同时存放在CPU的寄存器中,因为从寄存器中读取数据速度远大于从内存中读取,所以后续的读操作就直接从寄存器中读取数据。

重排之后的代码可能如下:

解决方法: 可以在定义instance对象时加上volatile关键字

volatile的作用: 保持内存可见性,禁止编译器进行某种场景的优化(一个线程在读,一个线程在写,修改对于读线程来说可能没有生效)


volatile关键字:

  • 可以保证多个线程的可见性
  • 但是不能保证原子性(使用synchronized关键字修饰方法)
  • 同时它也能禁止指令重排

双重校验锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

可见性主要体现在:一个线程对某个变量修改了,另一个线程每次都能获取到该变量的最新值

原子性:
VolatileTest是一个Thread类的子类,它的成员变量stopFlag默认是false,在它的run方法中修改成了true。然后在main方法的主线程中,用vt.isStopFlag()方法判断,如果它的值是true时,则打印stop关键字。
用volatile关键字修饰变量stopFlag ------>让stopFlag的值修改了,在主线程中通过vt.isStopFlag()方法,能够获取最新的值

volatile的原子性问题:
使用多线程给count加1,代码如下:




执行结果每次都不一样。
这个例子中count是成员变量,虽说被定义成了volatile的,但由于add方法中的count++是非原子操作。在多线程环境中,count++的数据可能会出现问题。
由此可见,volatile不能保证原子性---->使用synchronized关键字修饰add()

volatile关键字和synchronized的区别

  1. volatile是线程同步的轻量级实现,性能比synchronized要好,但是只能修饰变量,synchronized修饰变量、代码块。但是,目前,随着JDK的更新,synchronized的性能得到很大提升,开发中使用比率很大。
  2. 多线程访问volitile不会发生阻塞,synchronized会阻塞。
  3. volatile保证了数据的可见性,但是不能保证操作的原子性,synchronized在保证原子性的同时间接保证了可见性。
  4. 重点: volatile保证了变量在各个线程之间的可见性,而synchronized主要保证了各个线程访问资源的同步性。

饿汉式和懒汉式的线程安全问题:

什么情况会导致线程不安全:
1.线程的调度是抢占式执行
2.修改操作不是原子性的
3.多个线程同时修改同一个变量
4.内存可见性
5.指令重排序

对于饿汉式来说,多线程同时调用getInstance(),由于getInstance()里只做了一件事:读取instance实例的地址,也就是多个线程在同时读取同一个变量,并没有构成多个线程同时修改同一个变量这一情况,所以说饿汉模=式是线程安全的。

而对于懒汉式来说,多线程调用getInstance(),getInstance()做了四件事情~
1.读取 instance 的内容
2.判断 instance 是否为 null
3.如果 instance 为null,就 new实例 (这就会修改 intance 的值,intance一开始为null)
4.返回实例的地址
由于懒汉式造成了多个线程同时修改同一个变量这一情况,所以说懒汉式是线程不安全的。

为了解决“多个线程同时修改同一个变量”造成线程不安全的问题,采用加锁的解决方案,这里使用sychronized来解决线程不安全的问题。

public class Single 
    //懒汉模式
    static class Singleton 
        private static Singleton instance;
        private Singleton() 
        public static Singleton getInstance() 
        	//加锁
            synchronized (Singleton.class) 
                if (instance == null) 
                    instance = new Singleton();
                
            
            return instance;
        
    


即使instance已经实例化了,但是每次调用getInstance()还是会涉及加锁解锁,实际上此时已经不需要了,所以要实现在instance实例化之前调用的时候加锁,之后不加锁,就引出了双重检验锁版本

懒汉模式在多线程情况下由于编译器优化还会出现一种特殊情况,某个线程可能会进行多次读操作。使用volatile来解决这种特殊情况带来的问题。

最终版本:

public class Single 
    //懒汉模式
    static class Singleton 
        //volatile 避免内存可见性引出的问题
        private volatile static Singleton instance = null;
		private Singleton() 
        public static Singleton getInstance() 
            //双重检验锁
            if (instance == null) 
                synchronized (Singleton.class) 
                    if (instance == null) 
                        instance = new Singleton();
                    
                
            
            return instance;
        
    

为了保证懒汉式线程安全,涉及到三个要点:
1.加锁(sychronized)保证线程安全
2.双重if保证效率
3.volatile避免内存可见性引来的问题

以上是关于Java并发单例模式与synchronized关键字与voliate关键字的主要内容,如果未能解决你的问题,请参考以下文章

你见过这样的单例模式吗?

spring的单例模式

Java中级知识归纳

Java 并发学习总结

Java中的单例模式

线程安全的单例模式