保证线程安全的三种方式

分析问题:4w1h :what where why when how

摘要:
1,什么是线程安全
2,如何保证线程安全
3,线程安全的三种实现方式具体实现
4,总结

1,我们来看一下什么是线程安全

参考
《Java并发编程实践》中对线程安全的定义:

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

2,如何保证线程安全

  • 第一种方法,使用Hashtable线程安全类;

  • 第二种方法,使用Collections.synchronizedMap方法,对方法进行加同步锁;

  • 第三种方法,使用并发包中的ConcurrentHashMap类;

3,分别介绍三种方式

1)Hashtable

Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!

相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!
保证线程安全的三种方式_第1张图片

2)Collections.synchronizedMap

使用Collections.synchronizedMap方法,我们打开 JDK 源码,部分内容如下:
保证线程安全的三种方式_第2张图片
可以很清晰的看到,如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!

使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用

上面 2 种方法,由于都是对方法进行全表锁,所以在多线程环境下容易造成性能差的问题,因为** hashMap 是数组 + 链表的数据结构,如果我们把数组进行分割多段,对每一段分别设计一把同步锁,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样是不是可以有效的提高性能?**

3)ConcurrentHashMap

ConcurrentHashMap 类所采用的正是分段锁的思想**,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment**, 如下图:
保证线程安全的三种方式_第3张图片
当然,JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!

JDK1.8 对 HashMap 做了改造,当冲突链表长度大于 8 时,会将链表转变成红黑树结构,上图是 ConcurrentHashMap 的整体结构,参考 JDK1.7!

我们再来看看 JDK1.8 中 ConcurrentHashMap 的整体结构,内容如下:
保证线程安全的三种方式_第4张图片
JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是**数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)**结构。

ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

3.1)源码分析 JDK1.7 中的 ConcurrentHashMap

JDK 1.7 的 ConcurrentHashMap 采用了非常精妙的分段锁策略,打开源码,可以看到 ConcurrentHashMap 的主存是一个 Segment 数组。

保证线程安全的三种方式_第5张图片

我们再来看看 Segment 这个类,在 ConcurrentHashMap 中它是一个静态内部类,内部结构跟 HashMap 差不多,源码如下:

保证线程安全的三种方式_第6张图片

存放元素的 HashEntry,也是一个静态内部类,源码如下:

保证线程安全的三种方式_第7张图片

HashEntry和HashMap中的 Entry非常类似,唯一的区别就是其中的核心数据如value ,以及next都使用了volatile关键字修饰,保证了多线程环境下数据获取时的可见性!

从类的定义上可以看到,Segment 这个静态内部类继承了ReentrantLock类,ReentrantLock是一个可重入锁,如果了解过多线程的朋友们,对它一定不陌生。

ReentrantLock和synchronized都可以实现对线程进行加锁,不同点是:ReentrantLock可以指定锁是公平锁还是非公平锁,操作上也更加灵活,关于此类,具体在以后的多线程篇幅中会单独介绍。

因为ConcurrentHashMap的大体存储结构和HashMap类似,所以就不对每个方法进行单独分析介绍了,关于HashMap的分析,有兴趣的朋友可以参阅小编之前写的《深入分析 HashMap》一文。

ConcurrentHashMap 在存储方面是一个 Segment 数组,一个 Segment 就是一个子哈希表,Segment 里维护了一个 HashEntry 数组,其中 Segment 继承自 ReentrantLock,并发环境下,对于不同的 Segment 数据进行操作是不用考虑锁竞争的,因此不会像 Hashtable 那样不管是添加、删除、查询操作都需要同步处理。

理论上 ConcurrentHashMap 支持 concurrentLevel(通过 Segment 数组长度计算得来) 个线程并发操作,每当一个线程独占一把锁访问 Segment 时,不会影响到其他的 Segment 操作,效率大大提升!

上面介绍完了对象属性,我们继续来看看 ConcurrentHashMap 的构造方法,源码如下:

保证线程安全的三种方式_第8张图片

this调用对应的构造方法,源码如下:

保证线程安全的三种方式_第9张图片

从源码上可以看出,ConcurrentHashMap 初始化方法有三个参数,initialCapacity(初始化容量)为 16、loadFactor(负载因子)为 0.75、concurrentLevel(并发等级)为 16,如果不指定则会使用默认值

其中,值得注意的是 concurrentLevel 这个参数,虽然 Segment 数组大小 ssize 是由 concurrentLevel 来决定的,但是却不一定等于 concurrentLevel,ssize 通过位移动运算,一定是大于或者等于 concurrentLevel 的最小的 2 的次幂!

通过计算可以看出,按默认的 initialCapacity 初始容量为 16,concurrentLevel 并发等级为 16,理论上就允许 16 个线程并发执行,并且每一个线程独占一把锁访问 Segment,不影响其它的 Segment 操作!

3.2)源码分析 JDK1.8 中的 ConcurrentHashMap

虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢!

在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于 8 时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。

保证线程安全的三种方式_第10张图片

因为 HashMap 并不支持在多线程环境下使用, JDK1.8 中的 ConcurrentHashMap 和往期 JDK 中的 ConcurrentHashMa 一样支持并发操作,整体结构和 JDK1.8 中的 HashMap 类似,相比 JDK1.7 中的 ConcurrentHashMap, 它抛弃了原有的 Segment 分段锁实现,采用了 CAS + synchronized 来保证并发的安全性。

JDK1.8 中的 ConcurrentHashMap 对节点Node类中的共享变量,和 JDK1.7 一样,使用volatile关键字,保证多线程操作时,变量的可见行!

保证线程安全的三种方式_第11张图片

其他的细节,与 JDK1.8 中的 HashMap 类似

4,总结

虽然 HashMap 在多线程环境下操作不安全,但是在 java.util.concurrent 包下,java 为我们提供了 ConcurrentHashMap 类,保证在多线程下 HashMap 操作安全!

在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。

但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于 8 时,会将链表转化成红黑二叉树结构。

在 JDK1.8 中,与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多。

如果有理解不对的地方,欢迎各位网友指出!

参考:
文章

你可能感兴趣的:(Java基础,java,多线程)