共享可变性设计中存在风险以及解决方法(一)

本篇是《Java虚拟机并发编程》第五章的阅读笔记

首先看代码

这是一段控制一个高档电源的代码,改代码允许用户使用电池里的能量,并定期对电源进行自动充电

package com.originalcode;

//Bad code
public class EnergySource {
    private final long MAXLEVEL = 100;
    private long level = MAXLEVEL; //电量
    private boolean keepRunning = true;

    public EnergySource(){
        new Thread(new Runnable(){
            public void run(){
                relenish(); //充电
            }
        }).start();
    }

    public long getUnitsAvailable(){
        return level;
    }

    /*
    *使用电池
    *@param units 使用的电量
    */
    public boolean useEnergy(final long units){
        //使用的电量不能为负,电池用完不能为负
        if(units>0 && level>=units){
            level -= units;
            return true;
        }
        return false;
    }

    //停止给电池充电
    public void stopEnergySource(){
        keepRunning = false;
    }

    /*
    *充电,这里是私有方法,因为充电是电池内部持续充电,外部提供一个
    *接口,开关stopEnergySource来控制是否充电
    private void relenish(){
        while(keepRunning){
            if(level < MAXLEVEL){
                level++;
            }
            try{
                Thread.sleep(1000);//模拟电池充电过程
            }catch(InterruptedException ex){}
        }
    }
}

首先我们应该知道,EnergySource中定义的方法可能会被多个线程同时调用,所以该类中的私有变量level就成为一个非线程安全的共享可变变量

上述代码存在的问题

1.变量可见性问题

从线程安全性角度来说,在该类的大多数方法中都没有对该变量的访问进行保护,所以这种做法会产生变量可见性问题。

可见性问题是指:由于我们并未强制要求现在访问变量时一定要穿越内存栅栏,所以调用这些方法可能无法及时看到level值的变化

内存栅栏:简单来说就是从本地或工作内存到主存之间的拷贝动作(这里涉及到Java内存模型)

Java内存模型(借用别人博客的图)
内存模型
可以看到每个线程里面都有自己的内存,而里面存放的就是线程所访问的共享变量副本,而真正的共享变量的值是在主内存中,上面所说的可见性问题通俗来说就是,在线程更改共享变量值的时候,只是将副本更改了,没有及时的和主内存的共享变量同步。

2.资源浪费问题

虽然replenish()函数在绝大部分时间都处于睡眠状态,但是在上述代码中,我们还是得浪费一个线程在它身上。

这时候,如果我们试图创建大量的EnergySource对象,由于JVM通常只允许我们创建几千个线程,所以程序必将因为创建了过多的线程而抛出OutOfMemoryError

3.破坏了类不变式(class invariant)

类不变式:一个精心构建的对象可以保证,在其自身没有恢复到有效状态之前,它的任何方法都不能访问(类不变式是每一个类的对象在任何时候都要遵守的一个条件)

而该类的构造函数就违反了这一原则,因为在EnergySource实例对象的构造函数还未完成之前就可能有其他线程调用replenish()。从而导致level值的变化。

也就是,提前将未构造完全的对象暴露给其他线程,因为Thread类的start函数会自动插入一个内存栅栏(工作内存到主存的拷贝动作),导致该对象变成全局可见

同样,我们在构造函数中创建的那个线程可能会在创建它的对象尚未构造完成之前就调用其成员方法

解决方法

破坏了类不变式的解决方法

我们需要将线程的启动代码从构造函数里搬到一个独立的函数中,然后这种做法又会带来许多问题

  • 我们需要正确处理那些在线程启动之前就到达的函数调用
  • 可能有些人会干脆忘记调用那些个线程启动函数,虽然我们可以通过在代码中加标记的方法来解决这些问题,但同时也会导致参数很多丑陋的重复代码
  • 我们还得避免同一个实例上的线程启动函数被多次重复调用

一方面,我们不应该从构造函数中启动线程,另一方面,我们也不愿意在对象没有完全创建好之前就启用它

本书指出在《Effective Java》一书中解决方案:考虑使用静态工厂函数代替构造函数(即在一个静态工厂函数中创建对象实例,并在将对象实例返回给调用方法之前启动线程)

package com.fixingconstructor;
//Fixing constructor...other issues pending
private EnergySource(){}

private void init(){
    new Thread(new Runnable(){
        public void run(){
            replenish();
        }
    }).start();
}

public static EnergySource create(){
    final EnergySource energySource = new EnergySource();
    energySource.init();
    return energySource;
}
  1. 通过将缺省构造函数置为private,实现了既能在构造函数中进行一些简单运算,又能在初始化过程中免受挖补方法调用的打扰
  2. 其中,私有方法init实现了前一版本的构造函数的功能。通过静态工厂函数create()中调用该函数,就可以避免所有违反不变式的情况发生。
  3. 这种方法也能保证后台任务能够就在对象创建完成之后启动,从而保证了对象在创建和初始化过程中一直处于有效状态

猜你喜欢

转载自blog.csdn.net/qq_24986539/article/details/52432069