Android并发编程 原子类与并发容器
在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。
前言
上一篇博文中,主要说了些线程以及锁的东西,我们大多数的并发开发需求,基本上可以用synchronized或者volatile解决,虽然synchronized已经被JDK优化了,但有的时候我们还是觉得synchronized太重了,
比如说一个电影院卖票,这个票数是一定的而且共享的,我想尽快的卖票并且知道还有多少余票。在程序员看来这就是个票数自减以及获取最新票数的操作。
private static Long sCount = 10000L;
final Object obj = new Object();
//这里开了1000个线程对sCount并发操作
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
//这里加锁保证同步,使用synchronized总觉得没有
//必要,毕竟就是自减操作,如果不使用synchronized又有什么办法呢?
sCount--;
}
}
}).start();
}
Thread.sleep(5000);
System.out.println(sCount);
再有,我们平常使用的容器类List以及Map,如ArrayList、HashMap这些容器是非线程安全的,那我们如果需要支持并发的容器,我们该怎么办呢??读者莫急,这正是本篇分享的内容。
原子类
我们先来解决第一个问题,JDK1.5之后为我们提供了一系列的原子操作类,位于java.util.concurrent.atomic包下。
原子操作基本类型类
- AtomicBoolean:原子更新布尔类型。
- AtomicInteger:原子更新整型。
- AtomicLong:原子更新长整型。
以上3个类提供的方法几乎一模一样,所以本篇仅以AtomicInteger为例进行讲解,
AtomicInteger的常用方法如下。
- int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的
value)相加,并返回结果。 - boolean compareAndSet(int expect,int update):如果当前值(调用该函数的值)等于预期值(expect),则以原子方式将当前值(调用该函数的值)设置为更新的值(update)。
- int getAndIncrement():以原子方式将当前值加1,返回旧值。
- int incrementAndGet()以原子方式将当前值加1,返回新值。
- int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
那按照上面的知识重新对上面的卖票问题编程如下
private static AtomicLong sAtomicLong = new AtomicLong(10000L);
//这里开了1000个线程对sCount并发操作
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
sAtomicLong.decrementAndGet();
}
}).start();
}
Thread.sleep(5000);
System.out.println(sAtomicLong);
上面的是原子更新基本类型,那对于对象呢,JDK也提供了原子更新对象引用的原子类
原子更新引用类型
- AtomicReference:原子更新引用类型。
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
- AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类
型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean
initialMark)。
以上几个类提供的方法几乎一样,所以本节仅以AtomicReference为例进行讲解
boolean compareAndSet(V expect, V update):如果当前对象(调用该函数的对象)等于预期对象(expect),则以原子方式将当前对象(调用该函数的对象)设置为更新的对象(update)。
V get():获取找对象
void set(V newValue):设置对象
V getAndSet(V newValue):以原子方式将当前对象(调用该函数的对象)设置为指定的对象(newValue),并返回原来的对象(设置之前)
那这个东西用在哪里呢,我在著名的Rxjava源码中看到了原子更新对象的用法。
CachedThreadScheduler.java
//原子引用AtomicReference
AtomicReference<CachedWorkerPool> pool;
static final CachedWorkerPool NONE;
static {
NONE = new CachedWorkerPool(0, null);
NONE.shutdown();
}
public CachedThreadScheduler() {
this.pool = new AtomicReference<CachedWorkerPool>(NONE);
start();
}
@Override
public void start() {
CachedWorkerPool update = new CachedWorkerPool(KEEP_ALIVE_TIME, KEEP_ALIVE_UNIT);
//调用AtomicReference的compareAndSet方法
if (!pool.compareAndSet(NONE, update)) {
update.shutdown();
}
}
在创建线程调度器的时候把初始的工作线程池更新为新的工作线程池
AtomicReferenceFieldUpdater以原子方式更新一个对象的属性值
AtomicMarkableReference是带有标记的原子更新引用的类,可以有效解决ABA问题,什么是ABA问题,
我们就以上面的代码为例
假设pool.compareAndSet调用之前,pool内的对象NONE被更新成了update,然后又更新成了NONE,那么在调用pool.compareAndSet的时候还是会把pool内的对象更新为update,也就是说AtomicReference不关心对象的中间历程,这对于一些以当前对象是否被更改过为判断条件的特殊情境,AtomicReference就不适用了。
所以JDK提供了AtomicMarkableReference
那除了上面的原子更新引用类型之外,JDK还为我们提供了原子更新数组
原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类。
- AtomicIntegerArray:原子更新整型数组里的元素。
- AtomicLongArray:原子更新长整型数组里的元素。
- AtomicReferenceArray:原子更新引用类型数组里的元素。
- AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下。
- int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
- boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子
方式将数组位置i的元素设置成update值。
以上几个类提供的方法几乎一样,所以本节仅以AtomicIntegerArray为例进行讲解
public class AtomicIntegerArrayTest {
static int[] value = new int[]{2, 3};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 4);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}
以下是输出的结果。
4
2
更快的原子操作基本类LongAdder DouleAdder
JDK1.8为我们提供了更快的原子操作基本类LongAdder DouleAdder,
LongAdder的doc部分说明如下
This class is usually preferable to {@link AtomicLong} when
multiple threads update a common sum that is used for purposes such
as collecting statistics, not for fine-grained synchronization
control. Under low update contention, the two classes have similar
characteristics. But under high contention, expected throughput of
this class is significantly higher, at the expense of higher space
consumption
上面那段话翻译过来就是
当我们的场景是为了统计计数,而不是为了更细粒度的同步控制时,并且是在多线程更新的场景时,LongAdder类比AtomicLong更好用。 在小并发的环境下,论更新的效率,两者都差不多。但是高并发的场景下,LongAdder有着明显更高的吞吐量,但是有着更高的空间复杂度。
从LongAdder的doc文档上我们就可以知道LongAdder更适用于统计求和场景,而不是细粒度的同步控制。
并发容器
我们在开发中遇到比较简单的并发操作像自增自减,求和之类的问题,上一节原子类已经能比较好的解决了,但对于本篇文章来说只是开胃小菜,下面正菜来喽
ConcurrentLinkedQueue(并发的队列)
ConcurrentLinkedQueue是一个基于链表的无界线程安全队列,它采用先进先出的规则对节点进行排序,我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。
我们先来看一下ConcurrentLinkedQueue的类图
ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点
以下源码来自JDK1.8
public ConcurrentLinkedQueue() {
//默认情况下head节点存储的元素为空,tail节点等于head节点,哨兵节点
head = tail = new Node<E>(null);
}
private static class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
//设置item值
//这种的设置方式类似于C++的指针,直接操作内存地址,
//例如此行代码,就是以CAS的方式把值(item)赋值给当前对象即Node地址偏移itemOffset后的地址
//下面出现的casItem以及casNext也是同理
UNSAFE.putObject(this, itemOffset, item);
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = Node.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
看完了初始化,我们来看一下这个线程安全队列的进队和出队方法
offer(E e) 以及 poll() 方法
offer(E e)
public boolean offer(E e) {
checkNotNull(e);// 检查,为空直接异常
// 创建新节点,并将e 作为节点的item
final Node<E> newNode = new Node<E>(e);
// 这里操作比较多,将尾节点tail 赋给变量 t,p
for (Node<E> t = tail, p = t;;) {
// 并获取q 也就是 tail 的下一个节点
Node<E> q = p.next;
// 如果下一个节点是null,说明tail 是处于尾节点上
if (q == null) {
// 然后用cas 将下一个节点设置成为新节点
// 这里用cas 操作,如果多线程的情况,总会有一个先执行成功,失败的线程继续执行循环。
// <1>
if (p.casNext(null, newNode)) {
// 如果p.casNext有个线程成功了,p=newNode
// 比较 t (tail) 是不是 最后一个节点
if (p != t)
// 如果不等,就利用cas将,尾节点移到最后
// 如果失败了,那么说明有其他线程已经把tail移动过,也是OK的
casTail(t, newNode);
return true;
}
// 如果<1>失败了,说明肯定有个线程成功了,
// 这时候失败的线程,又会执行for 循环,再次设值,直到成功。
}
else if (p == q)
// 有可能刚好插入一个,然后P 就被删除了,那么 p==q
// 这时候在头结点需要从新定位。
p = (t != (t = tail)) ? t : head;
else
// 这里是为了当P不是尾节点的时候,将P 移到尾节点,方便下一次插入
// 也就是一直保持向前推进
p = (p != t && t != (t = tail)) ? t : q;
}
}
private boolean casTail(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val);
}
从上述代码可知入队列过程可归纳为3步
- 定位出尾节点;
- 使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试
- 更新尾节点
1.定位尾节点
tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点。
尾节点可能是tail节点,也可能是tail节点的next节点。
2.设置入队节点为尾节点
p.casNext(null, newNode)方法用于将入队节点设置为当前队列尾节点的next节点,如果p是null,
表示p是当前队列的尾节点,如果不为null,表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点3.更新尾节点
casTail(t, newNode);将尾节点移到最后(即把tail指向新节点)
如果失败了,那么说明有其他线程已经把tail移动过,此时新节点newNode为尾节点,tail为其前驱结点
poll()
public E poll() {
// 设置起始点
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 利用cas 将第一个节点设置为null
if (item != null && p.casItem(item, null)) {
// 和上面类似,p的next被删了,
// 然后然后判断一下,目的为了保证head的next不为空
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) {
// 有可能已经被另外线程先删除了下一个节点
// 那么需要先设定head 的位置,并返回null
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
// 和offer 类似,保证下一个节点有值,才能删除
p = q;
}
}
}
ConcurrentHashMap(并发的HashMap)
JDK1.7与JDK1.8 ConcurrentHashMap的实现还是有不小的区别的
JDK1.7
在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成。
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样.
JDK1.8
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:
put实现
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//① 只有在执行第一次put方法时才会调用initTable()初始化Node数组
tab = initTable();
//② 如果相应位置的Node还未初始化,则通过CAS插入相应的数据;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//③ 如果相应位置的Node不为空,且当前该节点处于移动状态 帮助转移数据
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//④ 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
//⑤ 如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//⑥ 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
/**
*如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,
*通过treeifyBin方法转化为红黑树,
*如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
*/
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
CopyOnWriteArrayList(线程安全的ArrayList)
JDK1.8中关于CopyOnWriteArrayList的官方介绍如下
A thread-safe variant of {@link java.util.ArrayList} in which all mutative
operations ({@code add}, {@code set}, and so on)
are implemented bymaking a fresh copy of the underlying array.中文翻译大致是
CopyOnWriteArrayList是一个线程安全的java.util.ArrayList的变体,
add,set等改变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();
}
}
有了前面的积淀,这段代码可以说没有任何难度
- 获取重入锁(线程互斥锁)
- 创一个新的数组(在原有数据长度的基础上加1)并把原数组的数据拷贝到新数组
- 把新数组的引用设置为老数组
注 写入过程中,若有其他线程读取数据,那么读取的依然是老数组的数据
使用场景
由上面的结构以及源码分析就知道CopyOnWriteArrayList用在读多写少的多线程环境中。
本篇总结
本篇分享了一些原子操作类以及并发容器,这些在多线程开发中都很有作用。希望帮到你。
下篇预告
Android 并发工具类与线程池
参考博文
此致,敬礼
Android并发编程 原子类与并发容器的更多相关文章
- Java并发编程系列-(5) Java并发容器
5 并发容器 5.1 Hashtable.HashMap.TreeMap.HashSet.LinkedHashMap 在介绍并发容器之前,先分析下普通的容器,以及相应的实现,方便后续的对比. Hash ...
- Java并发编程入门与高并发面试(三):线程安全性-原子性-CAS(CAS的ABA问题)
摘要:本文介绍线程的安全性,原子性,java.lang.Number包下的类与CAS操作,synchronized锁,和原子性操作各方法间的对比. 线程安全性 线程安全? 线程安全性? 原子性 Ato ...
- [并发编程 - socketserver模块实现并发、[进程查看父子进程pid、僵尸进程、孤儿进程、守护进程、互斥锁、队列、生产者消费者模型]
[并发编程 - socketserver模块实现并发.[进程查看父子进程pid.僵尸进程.孤儿进程.守护进程.互斥锁.队列.生产者消费者模型] socketserver模块实现并发 基于tcp的套接字 ...
- 并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理
在jdk中,为并发编程提供了CyclicBarrier(栅栏),CountDownLatch(闭锁),Semaphore(信号量),Exchanger(数据交换)等工具类,我们在前面的学习中已经学习并 ...
- JAVA并发编程的艺术 Java并发容器和框架
ConcurrentHashMap ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成. 一个ConcurrentHashMap里包含一个Segment数组, ...
- Java并发编程(八)同步容器
为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch) 一.为什么会出现同步容器? ...
- 那些年读过的书《Java并发编程的艺术》一、并发编程的挑战和并发机制的底层实现原理
一.并发编程的挑战 1.上下文切换 (1)上下文切换的问题 在处理器上提供了强大的并行性就使得程序的并发成为了可能.处理器通过给不同的线程分配不同的时间片以实现线程执行的自动调度和切换,实现了程序并行 ...
- 《Java并发编程的艺术》并发编程的挑战(一)
并发编程的挑战 并发编程的初衷是让程序运行的更快,但是更多的使用多线程真的会让程序变快吗? 1.线程上下文切换 关于线程上下文切换 多个线程在一个处理器里并不是同时进行的,而是非常快速地在线程之间进行 ...
- Java并发(二)—— 并发编程的挑战 与 并发机制的底层原理
单核处理器也可以支持多线程,因为CPU是通过时间片分配算法来循环执行任务 多线程一定比单线程快么?不一定,因为线程创建和上下文切换都需要开销. 如何减少上下文切换 无锁并发编程 CAS算法 使用最少线 ...
随机推荐
- 【转载】一个小时学会MySQL数据库
一个小时学会MySQL数据库 目录 一.数据库概要 1.1.发展历史 1.1.1.人工处理阶段 1.1.2.文件系统 1.1.3.数据库管理系统 1.2.常见数据库技术品牌.服务与架构 1.3.数 ...
- java_20 LinkedList类
LinkedList类特有的方法 (1)addLast() 将指定元素添加到此列表的结尾. addFirst() 将指定元素添加到此列表的开始. public static void main(St ...
- MySQL开发——【多表关系、引擎、外键、三范式】
多表关系 一对一关系 一对多或多对一关系 多对多关系 MySQL引擎 所谓的MySQL引擎就是数据的存储方式,常用的数据库引擎有以下几种: Myisam与InnoDB引擎之间的区别(面试) ①批量插入 ...
- 代码之髓读后感——类&继承
面向对象 语言中的用语并不是共通的,在不同语言中,同一个用语的含义可能会有很大差别. C++的设计者本贾尼·斯特劳斯特卢普对类和继承给予了正面肯定,然而,"面向对象"这个词的发明者 ...
- SVN忘记登陆用户
C:\Users\Yaolz\AppData\Roaming\Subversion\auth 删除里面所有文件
- 第五次spring会议
昨天所做之事: 昨天我对软件加上了换肤和透明度等功能. 今天所做之事: 我对软件加上了保密功能. private void 一键转密文ToolStripMenuItem_Click(object se ...
- Java:Hashtable
概要 前一章,我们学习了HashMap.这一章,我们对Hashtable进行学习.我们先对Hashtable有个整体认识,然后再学习它的源码,最后再通过实例来学会使用Hashtable.第1部分 Ha ...
- MFC字体样式和颜色设置
在编写MFC界面程序时,可能会使用不同大小或者颜色的字体,这里稍做记录. 使用方法 CFont *m_pFont;//创建新的字体 m_pFont = new CFont; m_pFont->C ...
- abaqus6.14导出网格inp以及导入inp以建模
建好part,组装后,划分网格,然后建立job,之后write input就可以在工作目录生成刚才新建网格的单元和节点编号信息了. *Heading ** Job name: buildmodel M ...
- Java面试题3
1.servlet执行流程 客户端发出http请求,web服务器将请求转发到servlet容器,servlet容器解析url并根据web.xml找到相对应的servlet,并将request.resp ...