并发编程(1)-单例模式和volatile

来源:互联网 发布:淘宝商品怎么上架 编辑:程序博客网 时间:2024/05/22 03:38

个人博客: https://zongwenlong.github.io/ 欢迎访问 ^_^

  之前在找实习的时候,被N次问过,你用过什么设计模式么,或者你知道有什么设计模式么?每次我都只会羞愧的说,单例模式,面试官就会说,那就写一下吧。然后我就把直接背诵过得模板的写了出来。当时羞愧的原因是我只会一种设计模——单例模式。关于设计模式这个坑会在之后进行填补,现在的我才意识到还有个地方更应该感到羞愧——唯一看过的单例模式还写的有问题。

痛苦的回顾

public class Singleton{    private static Singleton instance;    private Singleton(){}    public Singleton getInstance(){        if(instance == null){            synchronized(Singleton.class){                if(instance == null){                    instance = new Singleton();                }            }        }        return instance;    }}

  这是我之前面试时写过的代码,记得当时还有面试官问过我这样写有什么问题么,甚至他还在提示我,多线程下会有什么问题么?我信誓旦旦的觉得反正加了synchronized了,就没有问题了。事实证明,我是too young, too simple!

问题的解决

public class Singleton{    private static volatile Singleton instance;    private Singleton(){}    public Singleton getInstance(){        if(instance == null){            synchronized(Singleton.class){                if(instance == null){                    instance = new Singleton();                }            }        }        return instance;    }}

  上面的代码中,如果我们在为成员变量singleton添加volatile关键字进行修饰,原本的线程安全问题就会被解决,为什么呢?

问题的由来

public class Singleton{    private static Singleton instance;    private Singleton(){}    public Singleton getInstance(){        if(instance == null){           //Point1: 线程A            instance = new Singleton(); //Point2: 线程B        }        return instance;    }}

  对于这段代码,大家很容易就能明白,多线程环境下,这个单例是存在问题的。比如:假设线程A执行到Point1,线程B执行到Point2,那么线程A可能看到还未被线程B初始化完毕的singleton。所以就会出现问题。
对于instance = new Singleton();这一条语句可以大致分解为三步:

memory = allocate();   //1:分配对象的内存空间initObject(memory);    //2:初始化对象instance = memory;     //3:设置instance指向刚分配的内存地址

  线程B可能正在初始化instance,线程A就得到了空的对象,也开始了初始化,这当然是违背单例的思想的。那是不是像我在开始部分的代码中加入synchronized同步块就可以了呢,因为这样可以保证这段代码在同一时间只有一个线程访问。
  其实,这三条语句的顺序,并不一定是1,2,3顺序执行,由于编译器的原因,顺序可能变为1,3,2。也就是所谓的指令重排。如果是这样的话,那问题就严重了。线程B还没有初始化instance,线程A就可以再不进入同步块的前提下得到了一个非空的对象,然后就去使用了,这显然是有问题的。

Volatile

为什么加上volatile后问题就不存在了呢?
volatile在Java中的保证如下:
* 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。对volatile成员变量的读和写都是原子的。不会读到没有写入的值:只有读和写两个操作是原子的,像x++、x+=2这种表达式还是相当于先读,然后再写,两次进行,可以读到中间的值。即使是x=2这样的赋值操作,JVM并不能保证long,double这种64位数据类型(引用类型除外,它的读写始终都是原子的)读写的原子性,需要程序自己控制。另外过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32 bit values. For efficiency’s sake, this behavior is implementation specific; Java virtual machines are free to perform writes to long and double values atomically or in two parts.
For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64 bit value from one write, and the second 32 bits from another write. Writes and reads of volatile long and double values are always atomic. Writes to and reads of references are always atomic, regardless of whether they are implemented as 32 or 64 bit values.
VM implementers are encouraged to avoid splitting their 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.
—— from JLS section 17.7: Non-atomic Treatment of double and long

  • 禁止指令重排序,对所有的volatile的变量的所有次读写操作,组成一个全局的全序关系。全序关系的意思是:任何两个操作之间都可以比较先后关系。这个全序关系叫“同步顺序”(synchronization order)。这个同步顺序和“程序顺序”(program order,也就是单个线程里各个操作的顺序)是一致的。根据这个顺序,每次读操作,看到的一定是它之前最后一次对同一个变量写的值,如果它之前没有对这个变量的写操作,就读到初始值(0、null、false)。这可以避免指令重排问题。

总之,当我们加上volatile后,可以禁止语句2,3的重排序,也就可以避免多线程不安全问题。
既然谈到了volatile,它除了双重检查锁定外还有一些其他用途,如:状态标记量

volatile boolean flag = false;while(!flag){    doSomething();}public void setFlag() {    flag = true;}

其他的应用可以阅读:Java 理论与实践: 正确使用 Volatile 变量

Other Way?

  当修复了多线程安全问题后,是不是说上述方式就是单例模式的最佳实现呢?其实不是的,还有其他的单例模式的解决方案:
(PS: 前面提到的解决方案被成为Lazy initialization)

Eager initialization

public final class Singleton {    private static final Singleton INSTANCE = new Singleton();    private Singleton() {}    public static Singleton getInstance() {        return INSTANCE;    }}

在类进行初始化时,static变量也会被初始化,并且JVM可以保证线程安全,即static部分只会被初始化一次,并且只有被初始化后才能被本线程或其他线程使用

Initialization-on-demand holder idiom

public final class Singleton {    private Singleton() { }    /**     * Initializes singleton.     *     * {@link SingletonHolder} is loaded on the first execution of {@link Singleton#getInstance()} or the first access to     * {@link SingletonHolder#INSTANCE}, not before.     */    private static class SingletonHolder {            public static final Singleton INSTANCE = new Singleton();    }    public static Singleton getInstance() {            return SingletonHolder.INSTANCE;    }}

对于静态内部类SingletonHolder,并不会在Singleton初始化时就初始化,只有SingletonHolder被第一次使用时即getInstance()被调用时,INSTANCE才会被初始化。

The enum way

public enum Singleton {    INSTANCE;    public void execute (String arg) {        // Perform operation here     }}

《Effective Java》一书中建议的实现Singleton的最佳方法

参考文献

  1. Singleton pattern
  2. 双重检查锁定与延迟初始化
  3. 歪楼的volatile
  4. Java并发编程:volatile关键字解析
0 0
原创粉丝点击