java-多线程同时操作同一个对象之解决方法:读写锁ReadWriteLock的使用

说明一下,这边文章写得比较简单,只涉及到ReadWriteLock的使用,具体源码实现原理并不涉及。

1、使用场景:

           首先我这边是实际开发中使用到了,开发的环境情况为:有一个数据中心(暂且当做一个Map集合),有两个子线程A、B,其中A线程每5秒钟从其他地方获取到新来的数据然后和数据中心里面的数据进行一个融合,然后B线程进行隔5分钟从数据中心取出Map集合并解析其中的数据,一开始测试没什么问题,但是测试多了发现有一定几率发生“java.util.ConcurrentModificationException”异常,也就是同一个对象同时被两个线程操作,原因为:当B线程取到数据并进行遍历解析数据时,A线程去操作了数据中心的Map集合,因为他们最终引用的对象是一个,所以产生了上述问题。

        当时一开始想到的是,使用原型模式,进行Map的克隆(主要考虑到使用克隆的话属于牺牲内存,如果使用锁的话牺牲的是性能),而Map默认的是浅克隆(注意用的是HashMap,Map是个接口是没有克隆方法的),单独克隆后发现Map里面的对象还是引用的一个,所以需要把里面的对象也单独克隆(Map默认克隆是只克隆本身对象,如果map里面还有对象的话是不会克隆的),下面贴一下clone重写的方法。我边是直接重写的数据中心的clone方法,因为里面有Map集合;

# NetWorkDataBean :这个是数据中心,里面有三个Map集合this.udpMap、 this.tcpMap 、this.mediaMap
    @Override
    public Object clone() throws CloneNotSupportedException {
        NetWorkDataBean bean = (NetWorkDataBean) super.clone();
        if (null == this.udpMap) {
            this.udpMap = new HashMap<>();
        } else {
            bean.udpMap = new HashMap<>();
            for (Map.Entry<Long, Map<String, UDPNetworkAnalyBean>> entry : this.udpMap.entrySet()) {
                Map<String, UDPNetworkAnalyBean> mediaMapOld = entry.getValue();
                Long mediaKey = entry.getKey();
                Map<String, UDPNetworkAnalyBean> mediaMapNew = new HashMap<>();
                if (mediaMapOld != null && mediaMapOld.size() > 0) {
                    for (Map.Entry<String, UDPNetworkAnalyBean> dataBeanEntry : mediaMapOld.entrySet()) {
                        UDPNetworkAnalyBean dataBean = dataBeanEntry.getValue();
                        String key = dataBeanEntry.getKey();
                        mediaMapNew.put(key, (UDPNetworkAnalyBean) dataBean.clone());
                    }
                }
                bean.udpMap.put(mediaKey, mediaMapNew);
            }
        }
        if (null == this.tcpMap) {
            this.tcpMap = new HashMap<>();
        } else {
            bean.tcpMap = new HashMap<>();
            for (Map.Entry<Long, Map<String, NetworkAnalyBean.DataBean>> entry : this.tcpMap.entrySet()) {
                Map<String, NetworkAnalyBean.DataBean> mediaMapOld = entry.getValue();
                Long mediaKey = entry.getKey();
                Map<String, NetworkAnalyBean.DataBean> mediaMapNew = new HashMap<>();
                if (mediaMapOld != null && mediaMapOld.size() > 0) {
                    for (Map.Entry<String, NetworkAnalyBean.DataBean> dataBeanEntry : mediaMapOld.entrySet()) {
                        NetworkAnalyBean.DataBean dataBean = dataBeanEntry.getValue();
                        String key = dataBeanEntry.getKey();
                        mediaMapNew.put(key, (NetworkAnalyBean.DataBean) dataBean.clone());
                    }
                }
                bean.tcpMap.put(mediaKey, mediaMapNew);
            }
        }
        if (null == this.mediaMap) {
            bean.mediaMap = new HashMap<>();
        } else {
            bean.mediaMap = new HashMap<>();
            for (Map.Entry<Long, Long> entry : this.mediaMap.entrySet()) {
                bean.mediaMap.put(entry.getKey(), entry.getValue());
            }
        }
        return bean;
    }

然后想着B线程获取到Map后再操作应该就是一个单独的对象了,无论A那边何时操作应该都影响不到了吧。。。。

结果测试发现还是有一定的几率产生上面那个异常问题。经排查:竟是当B线程获取map集合时,这个数据Map集合克隆的时候A线程过来进行操作了,在这个点上发生了异常,经过个人想想,感觉还是得加锁,与其如此,不过直接加锁,把克隆去了。

2 读写锁ReadWriteLock的使用

       然后就找适合本开发环境的锁的使用,发现ReadWriteLock这个正好适合,读写锁:即读和读可同时,读和写排斥,写和写排斥,这时候我们就可以在读的时候使用读锁,写的时候使用写锁了。

下面先验证一下读和读:

    ReadWriteLock perodicRwLock = new ReentrantReadWriteLock();
    public void get(Thread thread) {
        perodicRwLock.readLock().lock();
        try{
            System.out.println("start time:"+System.currentTimeMillis());
            for(int i=0; i<5; i++){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行读操作……");
            }
            System.out.println(thread.getName() + ":读操作完毕!");
            System.out.println("end time:"+System.currentTimeMillis());
        }finally{
            perodicRwLock.readLock().unlock();
        }
    }

    @Override
    public void onClick(View v) {

        if ( v.getId( ) == R.id.request1 ) {
            new Thread( new Runnable( ) {
                @Override
                public void run() {
                    get( Thread.currentThread( ) );
                }
            } ).start( );

            new Thread( new Runnable( ) {
                @Override
                public void run() {
                    get( Thread.currentThread( ) );
                }
            } ).start( );
        }
    }

看下日志:

08-10 14:42:29.056 30623-30894/ I/System.out: start time:1533883349068
08-10 14:42:29.056 30623-30895/ I/System.out: start time:1533883349068
08-10 14:42:29.076 30623-30894/ I/System.out: Thread-158:正在进行读操作……
08-10 14:42:29.076 30623-30895/ I/System.out: Thread-159:正在进行读操作……
08-10 14:42:29.096 30623-30894/ I/System.out: Thread-158:正在进行读操作……
08-10 14:42:29.096 30623-30895/ I/System.out: Thread-159:正在进行读操作……
08-10 14:42:29.116 30623-30894/ I/System.out: Thread-158:正在进行读操作……
08-10 14:42:29.116 30623-30895/ I/System.out: Thread-159:正在进行读操作……
08-10 14:42:29.136 30623-30894/ I/System.out: Thread-158:正在进行读操作……
08-10 14:42:29.136 30623-30895/ I/System.out: Thread-159:正在进行读操作……
08-10 14:42:29.156 30623-30894/ I/System.out: Thread-158:正在进行读操作……
08-10 14:42:29.156 30623-30894/ I/System.out: Thread-158:读操作完毕!
08-10 14:42:29.156 30623-30894/ I/System.out: end time:1533883349169
08-10 14:42:29.156 30623-30895/ I/System.out: Thread-159:正在进行读操作……
08-10 14:42:29.156 30623-30895/ I/System.out: Thread-159:读操作完毕!
08-10 14:42:29.156 30623-30895/ I/System.out: end time:1533883349169

由图可以看出两个读线程是同时进行的,下面看一下两个写锁同时运行会怎么样:(代码就不贴了,只是把上面的“perodicRwLock.readLock().lock()”改成“perodicRwLock.writeLock().lock()”就行)

08-10 14:47:28.826 31870-31976/ I/System.out: start time:1533883648836
08-10 14:47:28.846 31870-31976/ I/System.out: Thread-144:正在进行写操作……
08-10 14:47:28.866 31870-31976/ I/System.out: Thread-144:正在进行写操作……
08-10 14:47:28.886 31870-31976/ I/System.out: Thread-144:正在进行写操作……
08-10 14:47:28.906 31870-31976/ I/System.out: Thread-144:正在进行写操作……
08-10 14:47:28.926 31870-31976/ I/System.out: Thread-144:正在进行写操作……
08-10 14:47:28.926 31870-31976/ I/System.out: Thread-144:写操作完毕!
08-10 14:47:28.926 31870-31976/ I/System.out: end time:1533883648938
08-10 14:47:28.926 31870-31977/ I/System.out: start time:1533883648939
08-10 14:47:28.946 31870-31977/ I/System.out: Thread-145:正在进行写操作……
08-10 14:47:28.966 31870-31977/ I/System.out: Thread-145:正在进行写操作……
08-10 14:47:28.986 31870-31977/ I/System.out: Thread-145:正在进行写操作……
08-10 14:47:29.006 31870-31977/ I/System.out: Thread-145:正在进行写操作……
08-10 14:47:29.026 31870-31977/ I/System.out: Thread-145:正在进行写操作……
08-10 14:47:29.026 31870-31977/ I/System.out: Thread-145:写操作完毕!
08-10 14:47:29.026 31870-31977/ I/System.out: end time:1533883649041

由图可以看出,一个线程先写完然后释放写锁后另一个线程才开始,注:读和写同时操作也是这样的。

具体我的项目中怎么用的这里就不贴了,因为我这读了后还有使用分析的操作,所以把上面的clone也是用上了的,只不过是在读的操作里面直接就克隆后返回的。

 

 

    原文作者:java锁
    原文地址: https://blog.csdn.net/shaoenxiao/article/details/81562577
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞