参考《分布式java应用》一书,简单过一遍并发包(java.util.concurrent)

ConcurrentHashMap

ConcurrentHashMap是线程安全的HashMap的实现。

1)添加

put(Object key , Object value)

ConcurrentHashMap并没有采用synchronized进行控制,而是使用了ReentrantLock。

public V
put(K key, V value) {
        if (value
==
null)
            throw new NullPointerException();
        int hash
= hash(key.hashCode());
        return segmentFor(hash).put(key,
hash, value,
false);
    }

这里计算出key的hash值,根据hash值获取对应的数组中的segment对象。接下来的工作都交由segment完成。

segment可以看成是HashMap的一个部分,(ConcurrentHashMap基于concurrencyLevel划分出了多个segment来对key-value进行存储)每次操作都只对当前segment进行锁定,从而避免每次put操作锁住整个map。

V
put(K key,
int hash,
V value,
boolean onlyIfAbsent)
{
            lock();
            try {
                int c
= count;
                if (c++
> threshold)
//
ensure capacity
                    rehash();
                HashEntry<K,V>[]
tab = table;
                int index
= hash & (tab.length -
1);
                HashEntry<K,V>
first = tab[index];
                HashEntry<K,V>
e = first;
                while (e
!=
null &&
(e.hash != hash || !key.equals(e.key)))
                    e
= e.next;
 
                V
oldValue;
                if (e
!=
null)
{
                    oldValue
= e.value;
                    if (!onlyIfAbsent)
                        e.value
= value;
                }
                else {
                    oldValue
=
null;
                    ++modCount;
                    tab[index]
=
new HashEntry<K,V>(key,
hash, first, value);
                    count
= c;
//
write-volatile
                }
                return oldValue;
            }
finally {
                unlock();
            }
        }

这个方法进来就上锁(lock),并在finally中确保释放锁(unlock)。

添加key-value的过程中,先判断当前存储对象个数加1后是否大于threshold,如果大于则进行扩容(对象数组扩大两倍,进行重新hash,转移到新数组)。

如果不大于,则进行后续操作。通过对hash值和对象数组大小减1的值进行按位与操作(取余),得到当前key需要放入数组的位置,接着寻找对应位置上的hashEntry对象链表,并进行遍历。

如果找到相同key值的Entry,则替换该Entry对象的value。

如果没有找到就创建一个Entry对象,赋值给对应位置的数组对象,并构成链表。

注意:采用segment这种方式,在并发操作过程中,可以在很多程度上减少阻塞现象。

2)删除

remove(Object key)

public V
remove(Object key) {
    int hash
= hash(key.hashCode());
        return segmentFor(hash).remove(key,
hash,
null);
    }

和put类似,删除也要根据hash先获得segment,然后在segment上执行remove操作。

V
remove(Object key,
int hash,
Object value) {
            lock();
            try {
                int c
= count -
1;
                HashEntry<K,V>[]
tab = table;
                int index
= hash & (tab.length -
1);
                HashEntry<K,V>
first = tab[index];
                HashEntry<K,V>
e = first;
                while (e
!=
null &&
(e.hash != hash || !key.equals(e.key)))
                    e
= e.next;
 
                V
oldValue =
null;
                if (e
!=
null)
{
                    V
v = e.value;
                    if (value
==
null ||
value.equals(v)) {
                        oldValue
= v;
                        //
All entries following removed node can stay
                        //
in list, but all preceding ones need to be
                        //
cloned.
                        ++modCount;
                        HashEntry<K,V>
newFirst = e.next;
                        for (HashEntry<K,V>
p = first; p != e; p = p.next)
                            newFirst
=
new HashEntry<K,V>(p.key,
p.hash,
                                                          newFirst,
p.value);
                        tab[index]
= newFirst;
                        count
= c;
//
write-volatile
                    }
                }
                return oldValue;
            }
finally {
                unlock();
            }
        }

segment的remove操作,首先加锁,然后对hash值与数组大小减1的值按位与操作,得到数组对应位置上的HashEntry对象,接下来遍历此链表,查找hash值相等并且key相等(equals)的对象。

如果没有找到,返回null,释放锁。

如果找到了,则重新创建位于删除元素之前的所有HashEntry,位于其后的不用处理。释放锁!

3)获取

get(Object key)

直接看看segment中的get操作,如下:

V
get(Object key,
int hash)
{
           if (count
!=
0)
{
//
read-volatile
               HashEntry<K,V>
e = getFirst(hash);
               while (e
!=
null)
{
                   if (e.hash
== hash && key.equals(e.key)) {
                       V
v = e.value;
                       if (v
!=
null)
                           return v;
                       return readValueUnderLock(e);
//
recheck
                   }
                   e
= e.next;
               }
           }
           return null;
       }

可以看出并没有加锁操作,只有v==null时,进入readValueUnderLock才有加锁操作。

这里假设一种情况,例如两条线程a、b,a执行get操作,b执行put操作。

当a执行到getFirst,与当前数组长度减1按位与操作后得到指定位置index,此时cpu将执行权交给b,b线程put一对key-value,导致扩容并重新hash排列,然后cpu又将执行权还给a,a然后根据之前的index去获取HashEntry就会发生问题。

当然这种情况发生的概率很小。

4)遍历

其实这个过程和读取过程类似,读取所有分段中的数据即可。

ConcurrentHashMap默认情况下采用将数据分为16个段进行存储,并且每个段各自拥有自己的锁,锁仅用于put和remove等改变集合对象的操作,基于voliate及hashEntry链表的不变性实现读取的不加锁。

这些方式使得ConcurrentHashMap能够保持极好的并发操作,尤其是对于读远比插入和删除频繁的map而言,而它采用的这些方法也可谓是对于java内存模型、并发机制深刻掌握的体现,是一个设计得非常不错的支持高并发的集合对象。

——摘自《分布式java应用》

CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的ArrayList。

1)添加

add(E e)

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();
    }
    }

这里同样没有使用synchronized关键字,而是使用ReentrantLock。

和ArrayList不同的是,这里每次都会创建一个新的object数组,大小比之前数组大1。将之前的数组复制到新数组,并将新加入的元素加到数组末尾。

2)删除

remove(Object o)

public boolean remove(Object
o) {
    final ReentrantLock
lock =
this.lock;
    lock.lock();
    try {
        Object[]
elements = getArray();
        int len
= elements.length;
        if (len
!=
0)
{
        //
Copy while searching for element to remove
        //
This wins in the normal case of element being present
        int newlen
= len -
1;
        Object[]
newElements =
new Object[newlen];//新建数组
 
        for (int i
=
0;
i < newlen; ++i) {
            if (eq(o,
elements[i])) {
            //
found one;  copy remaining and exit
            for (int k
= i +
1;
k < len; ++k)
                newElements[k-1]
= elements[k];
            setArray(newElements);
            return true;
            }
else
            newElements[i]
= elements[i];
        }
 
        //
special handling for last cell
        if (eq(o,
elements[newlen])) {
            setArray(newElements);
            return true;
        }
        }
        return false;
    }
finally {
        lock.unlock();
    }
    }

此方法为什么这么直接进行数组的复制呢?为何不适用system的arrayCopy来完成?

3)获取

get(int index)

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

这里有可能脏读。但是销量非常高。

//通过看集合包和并发包可以看出一些不同的编程思路。这里为什么就不事先做范围的检查?

从上可见,CopyOnWriteArrayList基于ReentrantLock保证了增加元素和删除元素动作的互斥。在读操作上没有任何锁,这样就保证了读的性能,带来的副作用是有时候可能会读取到脏数据。

CopyOnWriteArraySet

CopyOnWriteArraySet是基于CopyOnWriteArrayList的,可以知道set是不容许重复数据的,因此add操作和CopyOnWriteArrayList有所区别,他是调用CopyOnWriteArrayList的addIfAbsent方法。

public boolean addIfAbsent(E
e) {
   final ReentrantLock
lock =
this.lock;
   lock.lock();
   try {
       //
Copy while checking if already present.
       //
This wins in the most common case where it is not present
       Object[]
elements = getArray();
       int len
= elements.length;
       Object[]
newElements =
new Object[len
+
1];
       for (int i
=
0;
i < len; ++i) {
       if (eq(e,
elements[i]))
//如果存在,直接返回!
           return false;
//
exit, throwing away copy
       else
           newElements[i]
= elements[i];
       }
       newElements[len]
= e;
       setArray(newElements);
       return true;
   }
finally {
       lock.unlock();
   }
   }

由此可见,addIfAbsent需要每次都遍历,在add方面,CopyOnWriteArraySet效率要比CopyOnWriteArrayList低一点。

ArrayBlockingQueue

ArrayBlockingQueue是一个基于数组、先进先出、线程安全的集合类,其特点是实现指定时间的阻塞读写,并且容量是可以限制的。

1)创建

public ArrayBlockingQueue(int capacity,
boolean fair)
{
        if (capacity
<=
0)
            throw new IllegalArgumentException();
        this.items
= (E[])
new Object[capacity];
        lock
=
new ReentrantLock(fair);
        notEmpty
= lock.newCondition();
        notFull
=  lock.newCondition();
    }

初始化锁和两个锁上的Condition,一个为notEmpty,一个为notFull。

2)添加

offer(E e , long timeout , TimeUtil unit)

public boolean offer(E
e,
long timeout,
TimeUnit unit)
        throws InterruptedException
{
 
        if (e
==
null)
throw new NullPointerException();
    long nanos
= unit.toNanos(timeout);
        final ReentrantLock
lock =
this.lock;
        lock.lockInterruptibly();
        try {
            for (;;)
{
                if (count
!= items.length) {
                    insert(e);
                    return true;
                }
                if (nanos
<=
0)
                    return false;
                try {
                    nanos
= notFull.awaitNanos(nanos);
                }
catch (InterruptedException
ie) {
                    notFull.signal();
//
propagate to non-interrupted thread
                    throw ie;
                }
            }
        }
finally {
            lock.unlock();
        }
    }

这个方法将元素插入数组的末尾,如果数组满,则进入等待,只到以下三种情况发生才继续:

被唤醒、达到指定的时间、当前线程被中断。

该方法首先将等待时间转换成纳秒。然后加锁,如果数组未满,则在末尾插入数据,如果数组已满,则调用notFull.awaitNanos进行等待。如果被唤醒或超时,重新判断是否满。如果线程被interrupt,则直接抛出异常。

另外一个不带时间的offer方法在数组满的情况下不进去等待,而是直接返回false。

public boolean offer(E
e) {
       if (e
==
null)
throw new NullPointerException();
       final ReentrantLock
lock =
this.lock;
       lock.lock();
       try {
           if (count
== items.length)
               return false;
           else {
               insert(e);
               return true;
           }
       }
finally {
           lock.unlock();
       }
   }

同时还可以选择put方法,此方法在数组已满的情况下会一直等待,知道数组不为空或线程被interrupt。

public void put(E
e)
throws InterruptedException
{
        if (e
==
null)
throw new NullPointerException();
        final E[]
items =
this.items;
        final ReentrantLock
lock =
this.lock;
        lock.lockInterruptibly();
        try {
            try {
                while (count
== items.length)
                    notFull.await();
            }
catch (InterruptedException
ie) {
                notFull.signal();
//
propagate to non-interrupted thread
                throw ie;
            }
            insert(e);
        }
finally {
            lock.unlock();
        }
    }

3)获取

poll(long timeout, TimeUnit unit)

public E
poll(
long timeout,
TimeUnit unit)
throws InterruptedException
{
    long nanos
= unit.toNanos(timeout);
        final ReentrantLock
lock =
this.lock;
        lock.lockInterruptibly();
        try {
            for (;;)
{
                if (count
!=
0)
{
                    E
x = extract();
                    return x;
                }
                if (nanos
<=
0)
                    return null;
                try {
                    nanos
= notEmpty.awaitNanos(nanos);
                }
catch (InterruptedException
ie) {
                    notEmpty.signal();
//
propagate to non-interrupted thread
                    throw ie;
                }
 
            }
        }
finally {
            lock.unlock();
        }
    }

poll获取队列中的第一个元素,如果队列中没有元素,则进入等待。

poll首先将制定timeout转换成纳秒,然后加锁,如果数组个数不为0,则从当前对象数组中获取最后一个元素,在获取后将位置上的元素置为null。

如果数组中的元素个数为0,首先判断timeout是否小于等于0,若小于0则直接返回null。若大于则进行等待,如果被唤醒或者超时,重新判断数据元素个数是否大于0。

如果线程被interrupt,则直接抛出InterruptedException。

和offer一样,不带时间的poll方法在数组元素个数为0直接返回null,不进行等待。

take方法在数据为空的情况下会一直等待,只到数组不为空或者interrupt。

java中并发包简要分析01的更多相关文章

  1. java中继承的内存分析

    本文主要讲述java中继承的内存分析. 示例1,代码如下: public class EncapsulationTest { public static void main(String[] args ...

  2. [Java] Hashtable 源码简要分析

    Hashtable /HashMap / LinkedHashMap 概述 * Hashtable比较早,是线程安全的哈希映射表.内部采用Entry[]数组,每个Entry均可作为链表的头,用来解决冲 ...

  3. Java中ArrayList源码分析

    一.简介 ArrayList是一个数组队列,相当于动态数组.每个ArrayList实例都有自己的容量,该容量至少和所存储数据的个数一样大小,在每次添加数据时,它会使用ensureCapacity()保 ...

  4. Java中json工具对比分析

    Java中几个json工具分析 1, 环境 JDK1.6+IDE(IntelliJ IDEA)+windowsXP+GBK编码 2,分析对象 jackson1.8.2 http://jackson.c ...

  5. Eclipse中的快捷键快速生成常用代码(例如无参、带参构造,set、get方法),以及Java中重要的内存分析(栈、堆、方法区、常量池)

    (一)Eclipse中的快捷键:  ctrl+shift+f自动整理选择的java代码 alt+/ 生成无参构造器或者提升信息 alt+shift+s+o 生成带参构造 ctrl+shift+o快速导 ...

  6. 大杂烩 -- Java中Iterator的fast-fail分析

    基础大杂烩 -- 目录 Java中的Iterator非常方便地为所有的数据源提供了一个统一的数据读取(删除)的接口,但是新手通常在使用的时候容易报如下错误ConcurrentModificationE ...

  7. Java中Iterator的fast-fail分析

    1.fail-fast简介 fail-fast机制是java集合(Collection)中的一个错误机制.当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件. 例如:当某一个线 ...

  8. Java中的<< 和 >> 和 >>> 分析理解

    Java中的<< 和 >> 和 >>> 详细分析 <<表示左移移,不分正负数,低位补0: 注:以下数据类型默认为byte-8位 左移时不管正负,低 ...

  9. Java中I/O的分析

    Java中I/O的原理: 在java程序中,对于数据的输入/输出操作以”流“的方式进行的. 流是内存中一组有序数据序列 Java将数据从源读入到内存当中,形成了流,然后这些流可以写到目的地. Java ...

  10. [Java] LinkedHashMap 源码简要分析

    特点 * 各个元素不仅仅按照HashMap的结构存储,而且每个元素包含了before/after指针,通过一个头元素header,形成一个双向循环链表.使用循环链表,保存了元素插入的顺序. * 可设置 ...

随机推荐

  1. 根据Uri,Cursor没有获取到对应的属性

    Android: 背景:调用摄像头,拍摄视频,指定保存的地址,但是返回的Cursor文件,只有名称和大小的属性,没有其他诸如时长,连ID属性都没有 使用 cursor.getInt(cursor.ge ...

  2. .NET 8.0 前后分离快速开发框架

    前言 大家好,推荐一个.NET 8.0 为核心,结合前端 Vue 框架,实现了前后端完全分离的设计理念.它不仅提供了强大的基础功能支持,如权限管理.代码生成器等,还通过采用主流技术和最佳实践,显著降低 ...

  3. 《linux实用指令积累》持续更新。。。

    一.远程服务器文件拷贝 1.1.scp scp /home/a.txt root@127.0.0.1:/home/ 1.2.sshpass(适用于脚本调用,直接指定密码) 1.2.1.安装sshpas ...

  4. 音视频FAQ(三):音画不同步

    摘要 本文介绍了音画不同步问题的五个因素:编码和封装阶段.网络传输阶段.播放器中的处理阶段.源内容产生的问题以及转码和编辑.针对这些因素,提出了相应的解决方案,如使用标准化工具.选择强大的传输协议.自 ...

  5. 击败全球上千参赛队伍,合合信息获ICDAR“文本篡改检测”赛道冠军

    AI技术的快速发展激发了人们对于美好未来的畅享,也带来了潜在的危机,数据泄露.电信诈骗等系列风险与隐患开始浮出水面.利用科技手段构建可信的技术发展环境,保护使用者的信息及财产安全,正在成为行业共识. ...

  6. UC_Center整合单点登录后远程注册不激活问题的解决办法

    修改:bbs目录\uc_server\model\user.php 下方法add_user 如下: function add_user($username, $password, $email, $u ...

  7. 终于有人把Modbus讲明白了

    大家好!我是付工. 2012年开始接触Modbus协议,至今已经有10多年了,从开始的懵懂,到后来的顿悟,再到现在的开悟,它始终岿然不动,变化的是我对它的认知和理解. 今天跟大家聊聊关于Modbus协 ...

  8. docker安装运行kafka单机版

    这里我们安装一下kafka的单机版,由于kafka是基于zk进行管理的,如果我们没有安装过zk的话,需要进行安装好zk再安装kafka,当然如果已经安装过了, 那就没必要安装了.我们可以执行docke ...

  9. [快速阅读八] HDR->LDR:Matlab中tonemapfarbman函数的解析和自我实现。

    最近受朋友的委托,想自己实现Matlab里的一个HDR转LDR的函数,函数名是tonemapfarbman,乘着十一假期,稍微浏览下这个函数,并做了一点C++的实现和优化. 为了看到这个函数的效果,需 ...

  10. electron的两个进程

    electron 有两个类别的进程,一个是主进程,另一个是渲染进程 主进程: 启动后一直存在的,相当于一个树的主干并不会展示出来,是看不到的所有跟系统资源交互的操作都在这里进行操控渲染进程,新建或销毁 ...