Java中的copy on write(COW )是什么?

前言

在电商微服务项目中,多线程并发访问共享数据时,可能会出现并发问题导致程序崩溃、数据异常等情况。为了避免这些问题,Java中提供了多种并发控制方法,其中Copy-On-Write(COW)机制就是一种常用的技术。本文将详细介绍COW机制的概念、如何保证线程安全、相对于锁机制的优势。

一、概念:

1、什么是Copy on Write (COW)

Copy on Write (COW) 是一种在并发编程中常用的技术,它可以在不使用锁的情况下,实现对共享数据的并发访问。在 Java 中,Copy on Write 通常用于对 List、Map 等集合进行并发访问。

2、Copy on Write (COW)实现思想

Copy on Write 的基本思想是,当需要修改某个共享数据时,先将原始数据复制一份,并在副本上进行修改,修改完成后再将副本的引用赋值给原始数据的引用。当一个程序退出时,它所修改的部分会被保留下来,其他程序仍然可以访问它们。这样做的好处是,当多个线程同时读取共享数据时,它们各自持有原始数据的引用,不会发生互相干扰的情况;而在需要修改共享数据时,只有一个线程在修改,其他线程在读取,因此也不需要加锁。

3、Copy on Write (COW)优缺点

COW技术的主要优点是节省内存和提高效率。由于只有一个副本需要被修改,所以它比传统的复制算法更节省内存。此外,由于其他程序可以继续访问原始的共享内存,所以COW技术也提高了程序的效率。

COW技术的主要缺点是可能会导致数据不一致的问题。如果一个程序修改了共享内存,但另一个程序没有正确地复制该内存区域,那么后者可能会读取到错误的数据。为了解决这个问题,通常需要使用一些同步机制来确保所有程序都正确地处理共享内存。

4、COW如何保证线程安全

COW机制通过避免加锁、延迟复制等策略来保证线程安全。它的基本流程如下:

  1. 当需要对共享数据进行写操作时,先将原数据进行拷贝;
  2. 在新的数据副本上执行写操作;
  3. 写完后,利用原子操作将旧的数据结构替换为新的数据结构。

由于多个线程在修改时对应不同的数据副本,因此不会出现并发访问共享数据的情况。当然,这也意味着每次修改都会创建一个新的数组,可能会带来空间和时间的开销。

5、COW相对锁机制有什么优势

相对于传统的锁机制,COW机制有以下优势:

  1. 读操作无需加锁,提高了读取效率;
  2. 可以避免锁竞争、死锁等问题,提高了程序的稳定性;
  3. 可以避免多个线程同时读取同一份数据而引起的并发问题。
  4. 但是,需要明确的是,COW机制不适用于经常进行写操作、数据量较大的场景。

二、CopyOnWrite容器

从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

1、什么是CopyOnWrite容器

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

2、CopyOnWriteArrayList的实现原理

  • 先来看一下CopyOnWriteArrayList 的源码究竟是如何实现的。
public boolean add(E e) {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
    
    
            lock.unlock();
        }
    }
  • 可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
public E get(int index) {
    
    
        return get(getArray(), index);
    }

读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

3、CopyOnWriteArraySet容器

CopyOnWriteArraySet基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent(若没有则增加)方法

它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。

CopyOnWriteArraySet示例

  • 下面,我们通过一个例子去对比HashSet和CopyOnWriteArraySet。
import java.util.*;
import java.util.concurrent.*;
/*
 *  CopyOnWriteArraySet是“线程安全”的集合,而HashSet是非线程安全的。
 *  下面是“多个线程同时操作并且遍历集合set”的示例
 *  (01) 当set是CopyOnWriteArraySet对象时,程序能正常运行。
 *  (02) 当set是HashSet对象时,程序会产生ConcurrentModificationException异常。
 */

public class CopyOnWriteArraySetTest1 {
    
    
  // TODO: set是HashSet对象时,程序会出错。
  //private static Set<String> set = new HashSet<String>();
  private static Set<String> set = new CopyOnWriteArraySet<String>();
  public static void main(String[] args) {
    
    
    // 同时启动两个线程对set进行操作!
    new MyThread("ta").start();
    new MyThread("tb").start();
  }

  private static void printAll() {
    
    
    String value = null;
    Iterator iter = set.iterator();
    while(iter.hasNext()) {
    
    
      value = (String)iter.next();
      System.out.print(value+", ");
    }
    System.out.println();
  }

  private static class MyThread extends Thread {
    
    
    MyThread(String name) {
    
    
      super(name);
    }

    @Override
    public void run() {
    
    
        int i = 0;
      while (i++ < 10) {
    
    
        // “线程名” + "-" + "序号"
        String val = Thread.currentThread().getName() + "-" + (i%6);
        set.add(val);
        // 通过“Iterator”遍历set。
        printAll();
      }
    }
  }
}

结果说明:

由于set是集合对象,因此它不会包含重复的元素。
如果将源码中的set改成HashSet对象时,程序会产生ConcurrentModificationException异常。

4、自定义CopyOnWriteMap

JDK 没有提供自己的 CopyOnWriteMap 我们可以自己实现自己的 CopyOnWriteMap

public class CopyOnWriteMap<K,V> implements Map<K,V>,Cloneable {
    
    
    
    private volatile Map<K,V> internalMap;
    
    public CopyOnWriteMap() {
    
    
        internalMap = new HashMap<K, V>();
    }
    
    public V put(K key, V value) {
    
    
        
        synchronized (this) {
    
    
            Map<K,V> newMap = new HashMap<K,V>(internalMap);
            V val =newMap.put(key, value);
            internalMap = newMap;
            return val;
        }
        
    }
    
    public void putAll(Map<? extends K, ? extends V> m) {
    
    
        synchronized (this) {
    
    
            Map<K,V> newMap = new HashMap<K,V>(internalMap);
            newMap.putAll(m);
            internalMap = newMap;
        }
    }
    
    public V get(Object key) {
    
    
        return internalMap.get(key);
    }
 }

三、CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:

import java.util.Map;
 
import com.koo.my.CopyOnWriteMap;
 
/**
 * 黑名单服务
 *
 *
 */
public class BlackListServiceImpl {
    
    
 
    private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
            1000);
 
    public static boolean isBlackList(String id) {
    
    
        return blackListMap.get(id) == null ? false : true;
    }
 
    public static void addBlackList(String id) {
    
    
        blackListMap.put(id, Boolean.TRUE);
    }
 
    /**
     * 批量添加黑名单
     *
     * @param ids
     */
    public static void addBlackList(Map<String,Boolean> ids) {
    
    
        blackListMap.putAll(ids);
    }
 
}

代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。

  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

源码下载:
gitee.com/charlinchenlin/koo-erp

猜你喜欢

转载自blog.csdn.net/lovoo/article/details/130705933