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

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

接着(一)中提出的几个问题,对代码进行重构

(一)中最终重构的代码是

package com.fixingconstructor;

public class EnergySource {
    private final long MAXLEVEL = 100;
    private long level = MAXLEVEL;
    private boolean keepRunning = true;

    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;
    }

    public long getUnitsAvailable(){
        return level;
    }

    public boolean useEnergy(final long units){
        if(units>0 && level>=units){
            level -= units;
            return true;
        }
        return false;
    }

    public void stopEnergySource(){
        keepRunning = false;
    }

    private void replenish(){
        while(keepRunning){
            if(level < MAXLEVEL){
                level++;
            }
            try{
                Thread.sleep(1000);
            }catch(InterruptedException ex){

            }
        }
    }
}

解决方法

解决资源浪费的问题

由于线程资源是有限的,所以我们创建的时候就能太过随意。

因为每个EnergySource对象实例都会在replenish()函数上浪费一个线程,这将大大限制我们在整个程序中可创建的实例数量。如果继续创建实例,我们很快就会遇到资源枯竭的问题。

通过代码我们可以看到,这个充电操作时既代码短又执行块,所以将其放在一个timer里运行应该是一个不错的选择

timer方面我们可以选用java.util.Timer。为了能获得更大的吞吐量,尤其是在需要创建大量实例的情况下,我们最好还是通过线程池来实现线程复用。

这里给出一个方法:ScheduledThreadPoolExecutor提供了一个用于执行周期性任务的方案。需要注意的是,我们必须确保每个任务度都能处理好方法里抛出的异常。否则,那些把异常丢到外层的周期任务将会被停掉。

将replenish()函数改成在一个周期任务中执行

package com.periodictask;

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class EnergySource {
    private final long MAXLEVEL = 100;
    private long level = MAXLEVEL;
    private static final 
        ScheduledThreadPoolExecutor replenishTimer 
            = new ScheduledThreadPoolExecutor(10); 
    private ScheduledFuture<?> replenishTask;

    private EnergySource(){}

    private void init(){
        replenishTask = replenishTimer.scheduleAtFixedRate(new Runnable(){
            public void run(){
                System.out.println(System.nanoTime()/1.0e9);
                replenish();
            }
        }, 0, 1, TimeUnit.SECONDS);
    }

    public static EnergySource create(){
        final EnergySource energySource = new EnergySource();
        energySource.init();
        return energySource;
    }

    public long getUnitsAvailable(){
        return level;
    }

    public boolean useEnergy(final long units){
        if(units>0 && level>=units){
            level -= units;
            return true;
        }
        return false;
    }

    public void stopEnergySource(){
        replenishTask.cancel(false);
    }

    private void replenish(){
        if(level < MAXLEVEL){
            level++;
        }
    }

    public static void main(String[] args){
        EnergySource.create().useEnergy(10);
    }
}
  1. 可以看出上述代码去掉了keepRunning字段,并可以在stopEnergySource()函数中直接停掉周期任务。
  2. 在新的init()函数中,我们不在为每个EnergySource实例都启动一个线程,而是调度了一个timer来定期执行replenish()函数。因此,我们可以不再关系线程的休眠或计时调度问题,从而能够将注意力集中到增加能量水平的逻辑中去。
  3. 通过将replenishTimer的引用声明为static字段,在多个EnergySource实例上执行的replenish()操作就可以共享同一个线程池了。我们可以根据需要调整线程池内的线程数量。
    • 在本例中,因为replenish任务非常轻量,所以线程数10的线程池就够用了。
  4. public void scheduleAtFixedRate(TimerTask task,long delay,long period)参数含义
    • task — 是你要执行的任务
    • delay — 以毫秒为单位的延迟之前的任务执行
    • period — 连续执行任务之间的毫秒的时间
  5. 而replenishTask.cancel(false)这句话的是说试图取消对此任务的执行,如果任务已经启动,则参数确定是否应该以试图停止任务的方式来中断执行此任务的线程。这里false就是等待任务完成,而任务是周期的所有会一直执行。
  6. 其中ScheduledExecutorService接口的实现类是
    ScheduledThreadPoolExecutor,所以我们也可以这么写
private static final ScheduledThreadPoolExecutor replenishTimer = 
    Executors.newScheduledThreadPool(10);

出现的新问题

虽然将replenishTimer的引用声明为static可以帮助我们在ScheduledThreadPoolExecutor中共享线程池里面的线程,但同时我们也必须想出一个将replenishTimer关闭的方法,而JVM是不会帮助我们停掉这些线程。

这里至少有两种方案可以帮助我们解决这个问题:

第一种

在EnergySource类中提供一个静态方法,例如:

public static void shutdown(){
    replenishTimer.shutdown();
}

但是这种方法会有两个问题:一是EnergySource的用户得记得要调用这个方法。而是我们必须添加一些逻辑,以便在调用shutdown()之后处理那些已创建的EnergySource实例。

第二种

给newScheduledThreadPool(10)函数传递一个额外的ThreadFactory参数。该线程工厂能够保证其创建的所有线程都是守护线程。

private static final ScheduledThreadPoolExecutor replenishTimer = 
            new ScheduledThreadPoolExecutor(10, 
            new java.util.concurrent.ThreadFactory(){
                public Thread newThread(Runnable runnable){
                    Thread thread = new Thread(runnable);
                    thread.setDaemon(true);
                    return thread;
                }
            });

该方法的主要缺点就是需要多敲几行代码且稍微增加了一些维护成本


我们都知道Java中有两类线程:用户线程(非守护线程)和守护线程,其中守护线程顾名思义就是用来守护的,没有了守护的东西就没有存在的一样,所以当用来操作的非守护线程都执行完毕,守护线程就自动关闭消失(JVM虚拟机就会退出)。而默认情况下,执行线程都是非守护线程,如果没有显示的将其关闭,即使应用停止,它依然活跃着(只要有一个非守护线程存在,JVM虚拟机就不会退出)。

猜你喜欢

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