Java多线程:同步集合与同步锁

版权声明:不为无益之事,何以遣有涯之生。 https://blog.csdn.net/lj402159806/article/details/83051518

Java多线程:同步集合与同步锁

同步集合

同步集合在多线程开发中扮演非常重要的角色,本文介绍些常用但被忽略的同步集合。

CopyOnWriteArrayList

Copy-On-Write是一种用于程序设计中的优化策略,基本思路是多个线程共享同一个列表,当某个线程想要修改这个列表的元素时会把列表中的元素Copy一份,然后进行修改,修改完后再讲新的元素设置给这个列表,是一种延时懒惰策略。好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加、移除任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。使用Copy-On-Write机制实现的并发容器有两个分别是:CopyOnWriteArrayList和CopyOnWriteArraySet。

下面来分析下CopyOnWriteArrayList的核心源码,首先看下add方法:

    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个副本出来。复制一份之后将新的元素设置到元素数组的len位置,然后再把最新的元素设置给该列表。

get方法:

    public E get(int index) {
        return get(getArray(), index);
    }

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

这种写时拷贝的原理优点是读写分离,并发场景下操作效率会提高,缺点是写操作时占用的内存空间翻了一倍,因此是以空间换时间。

ConcurrentHashMap

HashTable是HashMap的线程安全实现,但是HashTable使用synchronized来保证线程安全,这就会导致它的效率非常低下,因为当线程1使用put添加元素,线程2不但不能使用put添加元素,同时也不能使用get获取元素,竞争越激烈效率越低。

因此替代HashTable的ConcurrentHashMap就出现了,ConcurrentHashMap的优点在于容器里有多把锁,每一把锁用于锁容器其中一部分数据,当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率。它的原理是将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。有些方法需要跨段,如size()和containsValue(),他们可能需要锁定整个表而不仅是某个段,这需要按顺序锁定所有段,操作完毕后又按顺序释放所有段的锁。

BlockingQueue

阻塞队列是生产者-消费者的一个实现,当队列满了时,再次调用put函数添加元素,那么调用线程将会阻塞,直到队列不再是填满状态。避免了手动判断以及同步操作。

函数名 作用
add(e) 把元素e添加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则抛异常
offer(e) 把元素e添加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则返回false
offer(e,time,unit) 把元素e添加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则在等待指定的时间之后继续尝试添加,如果失败则返回false
put(e) 把元素e添加到BlockingQueue里,如果BlockingQueue不能容纳,则调用此方法的线程被阻塞直到BlockingQueue里面有空间再继续添加
take() 取走BlockingQueue里排在队首的对象,若BlockingQueue为空,则进入等待状态直到BlockingQueue有新的对象被加入为止
poll(time,unit) 取出并移除队列中的队首元素,如果设定的阻塞时间内还没有获得数据,那么返回null
element() 获取队首元素,如果队列为空,那么抛出NoSuchElementException异常
peek() 获取队首元素,如果队列为空,那么返回null
remove() 获取并移除队首元素,如果队列为空,那么抛出NoSuchElementException异常

BlockingQueue多种常用实现:

  • ArrayBlockingQueue 数组实现的、线程安全的、有界的阻塞队列
    按FIFO(先进先出)原则对元素进行排序,元素从尾部插入到队列,从头部开始返回。
  • LinkedBlockingQueue 单向链表实现的队列
    按FIFO(先进先出)原则对元素进行排序,元素从尾部插入到队列,从头部开始返回。吞吐量高于ArrayBlockingQueue,但是在大多数并发应用程序中其可预知的性能要低,功能类似的有ConcurrentLinkedQueue
  • LinkedBlockingDeque 双向链表实现的双向并发阻塞队列
    同时支持FIFO和FILO,即可以从队列的头和尾同时操作(插入/删除),支持线程安全。可以指定队列容量(默认容量大小等于Integer.MAX_VALUE)

猜你喜欢

转载自blog.csdn.net/lj402159806/article/details/83051518