concurrentHashMap及CopyOnWriteArrayList、Collections.synchronizedList

本文详细解析了ConcurrentHashMap在Java 1.8中的线程安全实现机制,包括其内部结构、put方法流程、size计算方式及与HashMap、Hashtable的区别。重点介绍了1.8版本中采用的CAS+Synchronized混合锁策略,以及对并发写操作的优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

concurrentHashMap是线程安全的hashmap,在java1.8之前的版本,concurrentHashMap结构为一个segment数组默认初始长度为16,每一个segment都是一个hashmap,Segment继承了ReentranLock来加锁,它的put、remove操作都需要获取segment数组的reentranLock锁,get操作不加锁,因为每个键值对的value是volitale,具有可见性,当它被修改后会及时更新进内存可以被其他线程发现,所以get操作不加锁。它的size()即获取键值对的总数量的计算方法是,会先不加锁获取整个segments的每个hashmap长度计算加和2次,如果两次长度相同则认为它的长度为该值,否则会将每一个segment锁住,然后分别计算它们的长度,进行求和,因此在1.8之前的版本获取size最多计算3次。

而hashmap中size()方法返回的是size字段的值,当put时,size会加一,remove时size会减一。

1.7中的结构如图

3

在jdk1.8后,不再使用segments这种方式,而是使用了hashmap+synchronized+cas来实现线程安全。

以put方法为例:需要指出的是put操作中使用到了for循环,因为在table初始化和casTabAt的过程中可能有其它线程也在进行操作,需要不断尝试。

(1)首先会通过spread()方法计算key的hash值,与HashMap中不同的是它会将结果再与一个Hash_Base做&操作 hash^(hash>>>16) & Hash_Base;之后会判断数组table是否存在,如果不存在将会去创建,再这一过程中可能会存在多个线程同时去创建数组的情况,此时会使用cas的方式,将SIZECTL修改为-1,修改成功则说明获取了创建数组的锁,进行数组创建。

(2)如果数组存在,则会使用hash&(n-1)计算该key对应的数组下标,n为当前数组的长度,如果数组下标对应的node为null,则会使用casTabAt的方式根据key、value创建新的节点,尝试将新节点放入该下标之中,成功则跳出循环

(3)如果数组下标中n头节点node不为null,且它的hash状态是move则说明当前数组正在扩容,当前线程也会去帮助扩容。

(4)若下标node不为null且数组并未扩容,则尝试进入到synchronized方法块中,进行后续操作。该synchrozed修饰的是数组下标中的头节点node对象,想进入代码块,必须获取该节点的monitor对象使用权。进行代码块后,会根据下标节点存储的是链表还是红黑树进行遍历,如果存在key相同的节点则将value进行覆盖,否则创建新节点进行插入,红黑树插入后还会进行相应旋转。

(5)之后会判断链表的长度是否达到了8个,如果是则转化为红黑树。

(6)最后会调用addCount方法,将concurrentHashMap的长度加一,在这个里面还会判断是否需要扩容。ConcurrentHashMap中有两个元素去记录当前元素的总个数,一个是long 类型baseCount、一个CounterCells数组。首先会尝试使用cas的方式对baseCount进行+1操作;如果失败,当counterCells存在则根据Thread探针和m(counterCells数组长度-1)进行&操作获取数组对应下标元素,使用cas的方式将其中的元素值进行+1;

若前面的条件都不满足或执行失败则会进入到一个循环方法之中去,counterCells如果为null则会创建初始长度为2,之后会使用根据thread探针与counerCells长度-1进行与操作获取下标,如果该下标元素存在,则使用cas对其中数据进行+1,否则创建counterCell对象,初始值为1,以cas的形式将其放入对应下标,如果成功则跳出循环,如果多次失败则说明有其他线程也在执行长度+1,且发生了多次碰撞,此时将counterCells数组扩容长度×2,在执行相同的操作直到成功+1为止;

之后会判断当前数组中元素是否已到达负载因子限制,是的话数组会进行扩容。在扩容时,涉及到了节点重新映射的过程,它支持多线程的协助操作,在这个过程中多线程是分段领取任务,比如说当前数组长度为32,则线程1领取24-31,这8个下标的重hash工作,线程2领取16-23下标中元素的重hash,领取的时候是使用cas的方式尝试修改当前没有被领取最大下标的值,比如一开始是cas(filed,31,23),修改成功之后就说明之后0到23还没人认领。

对于数组每一个下标的链表或红黑树来说,和hashmap类似,也是通过key的hash值与原数组长度n取与看是否等于0,将节点分为low和high两个list,然后将list放入新数组原下标i和i+n的位置。

1.8 size()方法

调用sumCount()方法,它其实是把baseCount的值和countercells数组中元素的值进行了加和,不过baseCount和countercell中存储的是long类型元素,size的返回值是int,当加和返回值大于int最大限制时,直接返回的Integer.MAX_VALUE,在ConcurrentHashMap中推荐使用的获取元素数量的方法是MappingCount()方法,它也是调用了sumCount方法,不过返回值是long类型,不会损失精度。

为什么在1.8即使用了cas也使用了synchronized

通过cas的方式来尝试获取锁,当前线程并没有被阻塞,但如果并发量比较大的情况下,对cpu资源消耗比较大,以put方法为例,cas主要用于发生频率比较小或执行比较快的的情景下,比如像创建数组、创建数组下标头节点、数组扩容、size的变化等,避免了将线程阻塞再唤醒造成的资源消耗,而像数组下标链表及红黑树中元素的put这种比较复杂且耗时操作还是使用的synchronized进行加锁,避免大量线程长时间进行自旋获取锁造成cpu资源浪费。

于1.7中使用reentrantLock对segment整个hashmap进行加锁,1.8 使用cas+syncronized的方式,且synchronized仅对数组下标头节点元素加锁,更灵活且粒度更小,减少了大量线程在获取同一个锁时发生阻塞的概率,同时使用cas也避免了大量线程在不必要的地方陷入过早阻塞,影响执行速度

hashtable中put、get、size所有方法都用了synchronized加锁。

4

Java并发编程总结4——ConcurrentHashMap在jdk1.8中的改进 - everSeeker - 博客园

http://www.importnew.com/28263.html

java为我们提供了一些线程同步的list如Collections.SynchronizedList和CopyOnWriteArrayList

CopyOnWriteArrayList内部有一个ReenTranLock,读操作不加锁,它的set、add、remove操作是需要加锁的,并且会创建一个新的数组,进行操作后将新数组赋值给原数组引用,因此写操作效果很差

List<String> arrayList = Collections.synchronizedList(new ArrayList<String>());它的读写操作都会被synchronized关键字修饰,读性能很差

java.util.concurrent包下

Callable、Future、Executor

package callable;

import stu.people;

import java.util.concurrent.*;

/**
 * The xxx class for xxx.
 *
 * @author sunweiliang
 * @version 1.0, 2019/3/11
 * @since untitled 1.0.0
 */
public class CallableTest  {

    public static void main(String[] args) {
       // ExecutorService es1 = Executors.newCachedThreadPool();
        ExecutorService es = Executors.newCachedThreadPool();
        ExecutorService es1 = Executors.newFixedThreadPool(1);
        ExecutorService es2 = Executors.newSingleThreadExecutor();
        ExecutorService es3 = Executors.newScheduledThreadPool(10);
        callableThread cThread = new callableThread();
        Future<people> future = es.submit(cThread);
        //ScheduledExecutorService

        try{
            people str = future.get();
            System.out.println(str);
        } catch(Exception e) {

        }
    }

}

class callableThread implements Callable<people> {

    @Override
    public people call() throws Exception {
        people p = new people();
        p.setName("swl");
        p.setOld(123);
        return p;
    }
}

ReenTranLock、Condition

Executors、ThreadPoolExecutor、ScheduledThreadPoolExecutor

ConcurrentHashMap、CopyOnWriteArrayList

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值