程序猿阵线联盟-汇总各类技术干货事务面试

Java并发编程LockSupport使用实例

2018-07-27  本文已影响120人  小暴说

最近负责的项目需要实现一个Web页面监控功能,待监控的数据需要从数据库中统计出来。本身来讲这是一个很简单的功能点,但是考虑到监控端页面会被多人同时访问的业务场景,监控数据又要求每间隔一秒刷新一次,如果每个监控界面都实时去访问数据库,那么数据库的资源开销就太大了,若在白天的业务繁忙期遇到监控端用户数较多时有可能会影响正常的交易办理。为了避免数据库资源过度使用的问题我的设计是在web容器后台构建一块监控数据缓存,无论前台有多少个人访问监控页面,都只是从web容器缓存中获取监控数据,web容器后台有一个值守线程X每间隔一秒访问数据库轮询监控数据至内存中,示意图如下:


屏幕快照 2018-07-27 下午4.50.43.png

仅仅实现以上业务流程其实也非常简单,还用不上LockSupport支持,但是本着对系统资源的最低能耗及高性能需求,我有了更进一步的优雅实现愿景,当没有User监控请求访问容器时后台值守线程可以不干活让其处于阻塞状态,当容器收到User端监控请求时后台值守线程X立即从阻塞状态转变成Running状态,为此我们需要学习运用concurrent包中的LockSupport类来控制多线程间的运行状态切换以实现需求

LockSupport学习

LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。LockSupport很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。通过网上一些对LockSupport的源码分析可知,其实现是通过调用本地代码(C++)来做的,具有很强的OS平台相关性,因此性能应该是非常高的。对于JVM应用来说主要是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒操作的,当然实现线程间的阻塞和唤醒我们还可以用到对象锁,通过Synchronizer关键字来实现对象同步锁,使用对象的wait()和notify()方法来实现,但是此方式的实现在性能上会大打折扣而且有些并发控制不当非常容易引发线程间死锁,可以说非常不优雅。

LockSupport类核心方法

基于Unsafe类中的park和unpark方法

public static void park() {
        UNSAFE.park(false, 0L);
    }
 public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

LockSupport类测试Demo

如下编写一个ThreadPark类来验证park与unpark方法的成对使用

public class ThreadParkTest {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.setName("mt");
        mt.start();
        try {
            Thread.currentThread().sleep(10);
            mt.park();
            Thread.currentThread().sleep(10000);
            mt.unPark();
            Thread.currentThread().sleep(10000);
            mt.park();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    static class MyThread extends Thread {

        private boolean isPark = false;
        public void run() {
            System.out.println(" Enter Thread running.....");
            while (true) {
                if (isPark) {
                    System.out.println(Thread.currentThread().getName()+"Thread is Park.....");
                    LockSupport.park();
                }
                //do something
                System.out.println(Thread.currentThread().getName()+">> is running");
                try {
                    Thread.currentThread().sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        public void park() {
            isPark = true;
        }
        public void unPark() {
            isPark = false;
            LockSupport.unpark(this);
            System.out.println("Thread is unpark.....");
        }
    }
}

程序运行输出:

Enter Thread running.....
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mtThread is Park.....
Thread is unpark.....
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mt>> is running
mtThread is Park

park翻译过来即是停车的意思,我们可以这样理解,每个被应用程序启动的线程就是一辆在计算机总线赛道上奔驰着的跑车,当你想让某台车停下来休息会时那么就给它一个park信号,它就会立即停到赛道旁边的停车位中,当你想让它从停车位中驶出并继续在赛道上奔跑时再给它一个unpark信号即可

LockSupport的业务实际应用

我们对技术基础知识的掌握是为了更好,更优雅,更从容的实现业务需求,以最小的程序代价来实现业务最大收益化是计算机软件工程的永恒追求主题之一。 差不多给自己埋好坑了(围笑),不扯淡了,还是show me the code吧。
回到第一章的监控业务需求,首先我们需要编写后台值守线程X类,Daemon线程类Run()方法中除实现从数据库中加载监控数据到内存之外还必须实现具备满足一定条件时调用park()方法线程自动停车,同时对外要提供unpack()方法用于外部唤醒线程


后台值守线程MonitorWorkThread类代码编写:

class MonitorWorkThread extends Thread
    {
       //当前线程停车标志
        private volatile boolean isPark = false;
        
        //工作线程默认一秒钟加载一次,count即为工作监控线程每一次unpack之后会继续工作的时间,此值可根据实际需求配置化
    private int maxWorkCount = 300;
        
        @Override
        public void run() 
        {
            int indexCount=0;
            logger.info("成功启动审核任务监控工作线程,当前工作线程每次unpack连续工作的时间设定为"+maxWorkCount+"秒");
            while(true)
            {
                
                if(indexCount >= maxWorkCount)
                {
                    logger.info("当前监控工作线程已到达连续工作时间设定上限,现在进入pack休眠状态");
                    isPark =true;
                    indexCount=0;
                    LockSupport.park();
                }
                //从数据库中加载数据至内存
                try 
                {
                    loadDataFromDB();
                } catch (Exception e1) 
                {
                    logger.warn("从数据库中加载监控数据至内存发生异常", e1);
                }
                try 
                {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) 
                {
                    logger.warn("工作线程被异常中断唤醒", e);
                }
                indexCount++;
            }
        }
        /**
         * 假如当前线程正在运行状态,donothing
         */
        public void unPack()
        {
            if(isPark)
            {
                //唤醒当前监控工作线程,此处有并发唤醒动作需加锁
                synchronized (this)
                {
                     isPark = false;
                     LockSupport.unpark(this);
                     logger.info("当前监控工作线程已被唤醒");
                }
            }
        }
        
    }

接下来我们考虑编写监控业务实现类,大体思路是我们先需要定义内存缓存map用于装载数据库监控数据,业务实现类在容器实例化时自动启动上面的值守线程MonitorWorkThread,对外提供一个获取内存数据的公共方法,公共方法体中需要调用值守线程的unPack()方法以实现当容器收到客户端监控访问请求时若后台异步值守线程处于停车(阻塞)状态时,会被唤醒继续奔跑上路。
任务监控业务实现类TaskMonitorServiceImpl编写:

/**
 * 
 * @author lyp
 *  审核任务监控实现类
 */
@Service("TaskMonitorServiceImpl")
public class TaskMonitorServiceImpl 
{
    private static Logger logger= Logger.getLogger(TaskMonitorServiceImpl.class);
    
    //总任务状态缓存表
    private List<TaskStatusBean> totalStatus = new ArrayList<TaskStatusBean>();
    //审核柜员任务处理缓存表
    private Map<String,UserTaskCountDto> userTaskMap = new ConcurrentHashMap<String, UserTaskCountDto>();
    //监控工作线程引用
    private static MonitorWorkThread workThread=null;
    
    @PostConstruct
    private void initalizal()
    {
        //实例化之后执行的初始化动作,用于启动值守监控线程来刷新加载数据
        workThread = new MonitorWorkThread();
        workThread.setDaemon(true);
        workThread.setName("AuthTaskMonitor");
        workThread.start();
    }

    /**
     * 此为对外提供方法用于外部根据监控用户号获取内存中缓存的监控数据
     * @param userno           监控用户号
     * @return map key1:totalStatus key2:userno
     * @throws Exception
     */
    public Map<String,Object> monitorDataByUser(String userno) throws Exception
    {
        if(null == userno || "".equals(userno))
        {
            return null;
        }
        Map<String,Object> retMap = new HashMap<String, Object>();
        if(null !=workThread)
        {
           //每次请求都去看看异步值守线程是否需求唤醒
            workThread.unPack();
        }
        retMap.put("totalStatus", totalStatus);
        if(userTaskMap.containsKey(userno))
        {
            retMap.put(userno, userTaskMap.get(userno));
        }else
        {
            UserTaskCountDto dto =  new UserTaskCountDto(userno);
            retMap.put(userno, dto);
        }
        return retMap;
    }
    
    /**
     * 从数据库中加载内存数据至内存
     */
    private void loadDataFromDB () throws Exception
    {
        
        logger.info("开始从数据库中加载任务监控数据...");
        //do something about business....
        
        logger.info("从数据库中加载任务监控数据完毕...");
    }
    
    
    /**
     * 清理监控缓存数据map 
     */
    public void clearMonitorCache()
    {
        this.totalStatus.clear();
        this.userTaskMap.clear();
    }
}

写在最后

技术知识的学习本身就是枯燥无味的,靠解决问题的动力来驱动技术知识的掌握未尝不是一个值得尝试的高效学习方法。以上是我第一次在简书书写文章,选择加入简书的原因其实很简单,一是看美剧的时候被大量广告植入,二是简书的编辑器完美支持MarkDown语言写作。其实这也是我第一次使用MarkDown标记语言写作排版,MarkDown的写作方式对于程序员来说真的是太爽了,啊啊啊。
MarkDown语言

Markdown is intended to be as easy-to-read and easy-to-write as is feasible.
Readability, however, is emphasized above all else. A Markdown-formatted document should be publishable as-is, as plain text, without looking like it's been marked up with tags or formatting instructions.
Markdown's syntax is intended for one purpose: to be used as a format for writing for the web

写作不易,看完本文如果你觉得对你的工作生活有帮助请给个赞赏,不在乎多少,这会给予我写作无限的动力。
最后如果你需要转载此文,请标明原创出处,谢谢。

上一篇下一篇

猜你喜欢

热点阅读