同步容器(使用的是synchronized,并且不一定是百分百安全)

本篇续 -- 线程之间的通信 ,介绍java提供的并发集合,既然正确的使用wait和notify比较困难,java平台为我们提供了更高级的并发容器来替代

Vector&ArrayList

  • Vector虽然它的set和get方法都被Synchronized修饰,但是开启两条线程并发访问,一条线程拼命往里写,另一台循环往移除,这样并发访问不一定是百分百的线程安全的,很可能出现数组越界异常,而且现在基本已经不适用它了,它基本被ArrayList替代掉了
  • ArrayList是线程不安全的,并多线程并发访问的情况下会报 java.util.ConcurrentModificationException, 并发修改异常( 初始大小10, 每次扩容量都是扩容前的一倍, 使用native方法 Arrays.copyOf()方法)

将ArrayList转换成线程安全的

    ArrayList al = new ArrayList();

    Connections.syncchronizedList(al);

Map

  • 效率低下的HashTable,同Vector类似,它被HashMap替代掉了

    HashTable底层是通过synchronized关键字来实现的,虽然线程安全,但是在高并发的情况下,效率却超级低,因为当一个线程访问HashTable的同步方法时,其他线程拿不到锁,就访问不了它的同步方法,比如线程1使用HashTable的put方法,其他线程不仅不能使用put,就连get()也被阻塞不可以存储null键值

  • 线程不安全的HashMap

    在高并发访问的情况下,不会使用HashMap,线程不安全,但是可以存储null键值

将HashMap转换成线程安全的

    HashMap map = new HashMap<>();
Map map1 = Collections.synchronizedMap(map);

并发容器J.U.C

并发List--CopyOnWriteArrayList

JUC之写时复制技术, 它是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。

  • 我们着重的关注点是啥呢? 可能会出现线程安全性问题的方法 set() add() remove()

基本的使用;

同样实现了List接口,那么其实它的使用和ArrayList完全一样的,只是内部的实现不一样

实现原理

分析add方法

    /**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock; //获取锁ReentrantLock
lock.lock();
try {
Object[] elements = getArray(); // 获取CopyOnWriteArrayList内部维护的数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制该数组到 newElements数组里面 并扩容
newElements[len] = e; //在数组最后添加新的元素
setArray(newElements); //让替换掉原来的数组
return true; //添加成功
} finally {
lock.unlock();
}
} /**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}

可以看到,其实类似读写分离, add()可能会出现线程安全问题,因此给它一把锁,get()不会出现线程安全性问题,因此没有锁,如果非要给get也加上锁,那么在add的时候,就不能get了,因为它拿不到锁对象

如果是读取的话,直接在通过原数组的引用找到数组对象的位置进行读取

add()写的时候,其实不是往原array里面写,而是分如下几步

  1. 加锁
  2. 拷贝原数组,创建新的数组
  3. 添加新的元素
  4. 新数组替换原数组
  5. 释放锁

此外,CopyOnWriteArrayList底层维护的Object类型的数组被 volatile修饰

分析remove方法

public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0) //移除最后一个
setArray(Arrays.copyOf(elements, len - 1));
else { // 移除其它的,当前元素后面的需要往前移动
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue; // 移除谁,返回谁
} finally {
lock.unlock();
}
}

总结一下

  • 假如说多线程并发的应用中,绝大部分都是读操作,那么,CopyOnWriteArrayList效率明显高,因为他是读写分离,读没有锁,是在原数组上进行的,写也是线程安全的(加了lock),其中有一点就是它虽然能保证数据的最终是同步的,但是却保证不了实时同步性
  • 假如说绝大部分操作是写操作,可以看到,还是挺吃内存的,数组过大,他的效率可能就不一定比同步容器高,在不知道往里面存储多少数据的情况下,慎用.在高并发的互联网环境下这种操作分分钟就导致故障

并发Set

  • 和List步调一致的是,java平台为set集合提供了CopyOnWriteArraySet,它实现了set接口,底层完全依赖CopyOnWriteArrayList因此,它的特性和CopyOnWriteArrayList一致,同样在读多写少的高并发环境下,拥有很高的效率
  • 同样,在写多读少的高并发环境下,我们可以考虑下面的转换
 Set<Object> set = Collections.synchronizedSet(new HashSet<>());

并发Map

  • 同样可以使用Collections获取到一个同步Map,但是这个Map的性能依然不是最优的
  Collections.synchronizedMap();
  • jdk同样提供了一个高效同步Map,ConcurrentHashMap不允许空值

jdk1.7中ConcurrentHashMap内部实行了锁分离,分段式存储数据,然后每一段数据都会加上不同步的锁,所以当其中一条线程访问其中一段数据的时候,其他数据仍然可以被别的线程访问,同时,它的get()方法也是无锁的

jdk1.8中ConcurrentHashMap内部抛弃了锁分离而使用红黑树实现

并发Queue

在并发队列上,java提供了两套实现,一个是ConcurrentLinkedQueue的非阻塞型的队列,另一个是BlockingQueue接口,阻塞队列,同样他们继承了Queue接口

ConcurrentLinkedQueue 非阻塞队列

保证了在高并发的情况下,对link底层维护的链表的增删改各个节点的安全性

  • API和LinkedList等完全一样...

实现原理

它通过无锁的方法,底层使用的是UNSAFE实现(保证线程的安全性)

入队offer

/**
* Inserts the specified element at the tail of this queue.
* As the queue is unbounded, this method will never return {@code false}.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e); // 检查新添加的内容是否为空
final Node<E> newNode = new Node<E>(e); // 将其放入新节点 for (Node<E> t = tail, p = t;;) { // 整个一个for循环, 循环遍历链表,寻找适当的位置,插入新节点
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK. //可以看到,在这种会出现线程安全性问题的地方,使用的是cas进行操作,保证了线程的安全性
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
// 可以看到,casXXX 底层使用的是UNSAFE实现(保证线程的安全性)
private boolean casTail(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val);
} private boolean casHead(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val);
}

出队

public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item; if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}

阻塞队列BlockingQueue

阻塞队列的性质

  1. 如果阻塞队列为空, 再有新的线程想从阻塞队列里面获取元素就会被阻塞
  2. 如果队列满了, 再有线程想往里面put元素就会被阻塞

常见的阻塞队列

  1. ArrayBlockingQueue : 底层是一个基于数组结果的有界阻塞队列, 有FIFO特性
  2. LinkedBlockingQeque: 底层是基于链表存储结构实现的阻塞队列, 它有界,但是界限是Integer.MAXVALUE, 故容器产生OOM异常
  3. PriorityBlockingQueue: 带优先级的阻塞队列
  4. SynchronousQueue : 是一个不存储元素的阻塞队列, 每次的插入操作都必须等待另一个线程将元素移除出队列
  5. LinkedBlockingDueue : 是基于一个双向的链表,可以先进先出(队列),也可以先进后出 (栈)不允许插入null,基本原理和方法都和LinkedBlockingQueue差不多
  6. DelayQueue: 在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素。没有过期元素的话,使用poll()方法会返回null值,超时判定是通过getDelay(TimeUnit.NANOSECONDS)方法的返回值小于等于0来判断。延时队列不能存放空元素。

核心方法

方法类型 抛出异常 boolean 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek()

主要应用场景:

生产者和消费者模式

BlockingQueue是一个接口,因此我们学习ArrayBlockingQueue,它的底层维护着一个数组的阻塞队列

一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。

此类支持对等待的生产者线程和使用者线进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。

此类及其迭代器实现了 Collection 和 Iterator 接口的所有可选 方法。

既然叫阻塞队列,也就是说,他支持在多线程的条件下,多线程并发添加移除数组的元素,会被阻塞等待,而不会抛出空值异常或者报错,且数组的长度不可变

  • 两种情况发生阻塞

    • 队列满的时候,进行入队操作,也就是说,队列满了,但是有一个线程往队列里面put的时候,他会被阻塞,除非有别的线程做了take的操作
    • 队列为空,进行出队操作
可阻塞方法 描述
put 将指定元素插入此队列中,将等待可用的空间(如果有必要)。
take 获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。

可以直接使用它,实现消费者生产者模式,他的底层是用Condition ReentrantLock实现的--,方法被lock()和unlock()锁住, 数组满就await , 出队后signal

不发生阻塞 描述
add 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException
remove 从此队列中移除指定元素的单个实""(如果存在)。同样会抛异常
不发生阻塞 描述
offer 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false。
poll 获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。没有元素,返回null

五 .并发Deque

jdk1.6开始java提供的双端队列Deque(Double Ended Queue)允许在队列的头部或者尾部进行入队或者出队的操作.

他是是实现类:

  • ArrayDeque

    • 使用数组实现了双端队列,拥有更好的随机访问性,但是当队列的增大时,他需要重新分配内存,然后进行数组的复制
  • LinkedList
    • 使用链表实现了双端队列,因此它相对于ArrayDeque来说,没有了内存调整,数组复制的负担

无论是ListedList还是ArrayDeque,都是线程不安全的

  • LinkedBlockingDeque

线程安全,但是它没有进行读写分离,也就说,同一时间,只允许一条线程对其操作,因此在并发中,它的性能,远远底于ConcurrentLinkQueue


参考 博文

你不就像风一样

多线程六 同步容器&并发容器的更多相关文章

  1. JAVA 多线程随笔 (三) 多线程用到的并发容器 (ConcurrentHashMap,CopyOnWriteArrayList, CopyOnWriteArraySet)

    1.引言 在多线程的环境中,如果想要使用容器类,就需要注意所使用的容器类是否是线程安全的.在最早开始,人们一般都在使用同步容器(Vector,HashTable),其基本的原理,就是针对容器的每一个操 ...

  2. java并发:同步容器&并发容器

    第一节 同步容器.并发容器 1.简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同 ...

  3. java并发编程笔记3-同步容器&并发容器&闭锁&栅栏&信号量

    一.同步容器: 1.Vector容器实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施.保证了线程 ...

  4. java多线程系列五、并发容器

    一.ConcurrentHashMap 1.为什么要使用ConcurrentHashMap 在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,HashMap在 ...

  5. 第六章 Java并发容器和框架

    ConcurrentHashMap的实现原理与使用 ConcurrentHashMap是线程安全且高效的hashmap.本节让我们一起研究一下该容器是如何在保证线程安全的同时又能保证高效的操作. 为什 ...

  6. Java并发—同步容器和并发容器

    简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector.Ha ...

  7. Java并发容器篇

    作者:汤圆 个人博客:javalover.cc 前言 断断续续一个多月,也写了十几篇原创文章,感觉真的很不一样: 不能说技术有很大的进步,但是想法确实跟以前有所不同: 还没开始的时候,想着要学的东西太 ...

  8. Java多线程(六) —— 线程并发库之并发容器

    参考文献: http://www.blogjava.net/xylz/archive/2010/07/19/326527.html 一.ConcurrentMap API 从这一节开始正式进入并发容器 ...

  9. java多线程总结-同步容器与并发容器的对比与介绍

    1 容器集简单介绍 java.util包下面的容器集主要有两种,一种是Collection接口下面的List和Set,一种是Map, 大致结构如下: Collection List LinkedLis ...

随机推荐

  1. 多线程之美5一 AbstractQueuedSynchronizer源码分析<一>

    AQS的源码分析 目录结构 1.什么是CAS ? 2.同步器类结构 3.CLH同步队列 4.AQS中静态内部类Node 5.方法分析 ​ 5.1.acquire(int arg ) ​ 5.2.rel ...

  2. python 正则表达式re使用模块(match()、search()和compile())

    摘录 python核心编程 python的re模块允许多线程共享一个已编译的正则表达式对象,也支持命名子组.下表是常见的正则表达式属性: 函数/方法 描述 仅仅是re模块函数 compile(patt ...

  3. Codeforces Round #608 (Div. 2) 题解

    目录 Codeforces Round #608 (Div. 2) 题解 前言 A. Suits 题意 做法 程序 B. Blocks 题意 做法 程序 C. Shawarma Tent 题意 做法 ...

  4. jumpserver 资产管理及授权

    1.用户管理-添加[用户列表] 1.1点击创建用户 1.2创建用户 2.用户管理-添加[用户组] 2.1点击创建用户组 2.2创建用户组   3.资产管理添加资产 3.1添加节点 3.2添加资产(点击 ...

  5. Linux 周期任务

    一次性任务 在某个特定的时间,执行一次后被清除 相关命令/进程 at 命令 atd进程 在centos6中,系统服务的名称: /etc/init.d/atd 查看系统上该进程时候启动: [root@e ...

  6. pycharm安装第三方包问题解决

    pycharm安装第三方包问题解决 pycharm是一个基于python的非常好用的集成开发环境,而python有许多非常不错的开源第三方库,这就需要将一些这样的第三方库导入到我们的项目中去了.然而, ...

  7. SpringCloud Zuul2.X网关实现服务熔断降级(复制即用)

    版本: <properties> <spring-boot.version>.RELEASE</spring-boot.version> <spring-cl ...

  8. Python高级特性——生成器(generator)

    通过上节的学习,我们知道使用列表生成式,可以直接创建一个列表.但是,有些时候,受到内存的限制等实际情况,列表生成式无法满足.比如,一个长度为1000万的列表,普通内存根本就不够,又或者实际处理的过程中 ...

  9. sudo go 找不到命令

    我们使用 sudo 时,使用的配置文件是 /etc/sudoers. 解决配置 /etc/sudoers 文件中的 Defaults secure_path 这一项.将 $GOROOT/bin 目录加 ...

  10. 6.Ansible Roles角色实战

    ==Roles小技巧:== 1.创建roles目录结构,手动或使用ansible-galaxy init test roles 2.编写roles的功能,也就是tasks. nginx rsyncd ...