线程安全策略

一、不可变对象

创建后状态不能被修改的对象叫作不可变对象。不可变对象天生就是线程安全的。它们的常量(变量)是在构造函数中创建的,既然它们的状态无法被修改,那么这些常量永远不会被改变——不可变对象永远是线程安全的

1. 不可变对象需要满足的条件
1)对象创建以后其状态就不能修改

2)对象所有域都是final类型

3)对象是正确创建的(在对象创建期间,this引用没有逸出)
2. final

final关键字:类、方法、变量

1)修饰类:不能被继承,final类中的成员属性可以根据需要设置为final,但final类中所有的成员方法都被隐式指定为final方法。一般不建议将类设置为final类型。可以参考String类。

2)修饰方法:1)锁定方法不被继承类修改;2)效率

3)修饰变量:1)基本数据类型变量,初始化后便不能进行修改;2)引用类型变量,初始化之后不能再指向别的引用
3. Collections
1)java提供Collections工具类,在类中提供了多种不允许修改的方法(Collections.unmodifiableXXX:Collection、List、Set、Map...)
4. Guava
 1)需要引入jar包,谷歌的Guava提供类似Java中的Collections(​ ImmutableXXX:Collection、List、Set、Map...)

介绍了不可变对象,通过在某些情况下,将不能被修改的类对象,设置为不可变对象,来让对象在多个线程间是线程安全的。归根到底,其实是躲避开了并发的问题。除了不可变对象,还存在一个方法 就是线程封闭


二、线程封闭

把对象封装到一个线程里,只有这一个线程能看到该对象,那么就算这个对象不是线程安全的,也不会出现任何线程安全的问题,因为它只能在一个线程中被访问,如何实现线程封闭:

1. Ad-hoc 线程封闭:程序控制实现,非常脆弱、最糟糕,忽略
2. 堆栈封闭:简单的说就是局部变量,无并发问题。多个线程访问同一个方式的时候,方法中的局部变量都会被拷贝一份到线程栈中,方法的局部变量是不被多个线程共享的,因此不会出现线程安全问题,能用局部变量就不推荐使用全局变量,全局变量容易引起并发问题,注意,全局的变量而不是全局的常量。
3. ThreadLocal 线程封闭:特别好的封闭方法
1)概述

	(1)ThreadLocal提供线程级别的变量.这些变量不同于它们正常下的变量副本,在每一个线程中都有它自己获取方式(通过它的get和set方法),不依赖变量副本的初始化。它的实例通常都是私有的静态的,用于关联线程的上下文

	(2)这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量

	(3)ThreadLocal的作用是提供线程内部的局部变量,这种变量只存在线程的生命周期

	(4)声明方式:private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>;

2)工作原理

	(1)声明全局的ThreadLocal变量,private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>

	(2)每个线程中都有属于自己的ThreadLocalMap,互不干扰	

	(3)全局只有一个threadLocal,当通过set填充数据时,通过获取当前操作线程的threadLocalMap,将threadLocal作为threadLocalMap中的key,需要填充的值作为value

	(4)当需要从threadLocal获取值时,通过获取当前操作线程的threadLocalMap,并返回key为threadLocal对象的value

3)应用场景

	 ThreadLocal中存放的变量只在线程的生命周期内起作用,应用场景主要有两个方面

	(1)提供一个线程内公共变量(比如本次请求的用户信息、实体参数),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度

	(2)为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响	

三、常见线程不安全类与写法

什么是线程不安全的类呢?简单的说,如果一个类的对象同时可以被多个线程访问,如果不做特殊的同步或并发处理,那么就很容易表现出线程不安全的现象,比如异常、逻辑处理错误等等,这种类称之为线程不安全的类

1. StringBuilder 与 StringBuffer
1)StringBuffer为线程安全类,StringBuilder为线程不安全类

2)StringBuffer在方法的实现上使用了synchronized关键字对方法进行同步,因此是线程安全的,而StringBuilder则没有进行特殊的同步或并发处理

3)StringBuffer使用了同步锁,同一时间只能有一个线程进行访问,因为在系统性能会有损耗,适用于多线程环境下使用

4)通常情况下,字符串拼接出现在方法内,使用StringBuilder进行字符串的拼接会大大提高性能,属于堆栈封闭,单个线程的操作对象,因此不存在线程不安全问题,优先选择使用StringBuilder(类和方法声明变量时的区别)

5)两种字符串拼接类分别适用不同的场景,这就是为什么JAVA同时提供了这两种类
2. SimpleDateFormat 与 JodaTime
1)SimpleDateFormat是JAVA提供的一个日期转换类,SimpleDateFormat在多线程下共享使用就会出现线程不安全情况。建议将SimpleDateFormat声明为局部变量,这样才会避免线程不安全所带来的异常

2)线程安全的日期格式化,需要引入jar包

3)在使用日期转换的时候,更建议使用JodaTime所提供的日期转换类,不仅是因为它是线程安全的,而且在类实际处理转换中有其他的优势
3. ArrayList、HashSet、HashMap 等 Collections
通常使用以上类,都是声明在方法内,作为局部变量使用,一般很少碰上线程不安全的问题。但如果定义为可以多个线程修改的时候,就会出现线程安全问题

四、同步容器

在上面线程不安全类中,提到了ArrayList、HashSet、HashMap非线程安全的容器,如果有多个线程并发的访问,就会出现线程安全问题,因此在编写程序的时候,必须要求开发人员手动的在任何访问这些容器的地方进行同步处理,导致使用这些容器非常不便,因此JAVA中提供同步容器

1. ArrayList -> Vector、Stack
1)`Vector`实现`List`接口,底层和`ArrayList`类似,但是`Vector`中的方法都是使用`synchronized`修饰,即进行了同步的措施。 但是,`Vector`并不是线程安全的。

2)同步容器不一定是线程安全的

3)在使用同步容器的时候,并不是所有的场合下都能够做到线程安全
2. HashMap -> HashTable(key、value均不能为null)
3. Collections.synchronizedXXX(List、Set、Map)

同步容器中的方法主要采取synchronized进行同步,因此执行的性能会收到受到影响,并且同步容器并不一定能做到真正的线程安全


五、并发容器 J.U.C

所谓的J.U.C其实是JDK所提供的一个包名,全程为java.util.concurrent,里面提供了许多线程安全的集合

1. CopyOnWriteArrayList(ArrayList -> CopyOnWriteArrayList )
1)概述

	(1)CopyOnWriteArrayList相比于ArrayList是线程安全的,从字面意思理解,即为写操作时复制

	(2)CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组

	(3)CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的

	(4)CopyOnWriteArrayList的读操作是可以不用加锁的

2)缺点	

	(1)由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc

	(2)不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList能做到最终一致性,但是还是没法满足实时性要求

	(3)CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用  因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障

3)设计核心

	(1)读写分离,读和写分开

	(2)最终一致性。最终保证List的结果是对的

	(3)使用另外开辟空间的思路,来解决并发冲突
2. CopyOnWriteArraySet( HashSet -> CopyOnWriteArraySet)
1)CopyOnWriteArraySet底层实现是采用CopyOnWriteArrayList,合适比较小的集合,其中所有可变操作(add、set、remove等等)都是通过对底层数组进行一次新的复制来实现的,一般需要很大的开销

2)迭代器支持hasNext(), next()等不可变操作,不支持可变的remove操作;使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照
3. ConcurrentSkipListSet(TreeSet -> ConcurrentSkipListSet)
1)ConcurrentSkipListSet<E>是jdk6新增的类,位于java.util.concurrent并发库下

2)ConcurrentSkipListSet<E>和TreeSet一样,都是支持自然排序,并且可以在构造的时候定义Comparator<E>的比较器,该类的方法基本和TreeSet中方法一样(方法签名一样)

3)和其他的Set集合一样,ConcurrentSkipListSet<E>都是基于Map集合的,ConcurrentSkipListMap便是它的底层实现

4)在多线程的环境下,ConcurrentSkipListSet<E>中的contains、add、remove操作是安全的,多个线程可以安全地并发执行插入、移除和访问操作。但是对于批量操作addAll、removeAll、retainAll 和 containsAll并不能保证以原子方式执行。理由很简单,因为addAll、removeAll、retainAll底层调用的还是contains、add、remove的方法,在批量操作时,只能保证每一次的contains、add、remove的操作是原子性的(即在进行contains、add、remove三个操作时,不会被其他线程打断),而不能保证每一次批量的操作都不会被其他线程打断。因此,在addAll、removeAll、retainAll 和 containsAll操作时,需要添加额外的同步操作

5)此类不允许使用 null 元素,因为无法可靠地将 null 参数及返回值与不存在的元素区分开来
4. ConcurrentHashMap (​ HashMap -> ConcurrentHashMap)
1)不允许null值,绝大部分使用Map都是读取操作,而且读操作大多数都是成功的,因此,ConcurrentHashMap针对读操作进行了大量的优化。在高并发的场景下,有很大的优势

2)其实HashTable有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMapJDK1.7版本的核心思想
5. ConcurrentSkipListMap(TreeMap -> ConcurrentSkipListMap)
1)ConcurrentSkipListMap,内部使用``SkipList`结构实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(log n)

2)跳表(SkipList):使用“空间换时间”的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点
6. concurrentHashMap与ConcurrentSkipListMap性能测试
1)在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右

但ConcurrentSkipListMap有几个ConcurrentHashMap不能比拟的优点:

2)ConcurrentSkipListMap 的key是有序的,而ConcurrentHashMap是做不到的

3)ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势

六、安全共享策略总结

以下策略是通过线程安全策略中的不可变对象、线程封闭、同步容器以及并发容器相关知识总结而得:

1. 线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改

2. 共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它

3. 线程安全对象:一个线程安全的对象或容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它

4. 被守护对象:被守护对象只能通过获取特定的锁来访问

参考网址

JAVA并发编程与高并发解决方案 - 并发编程 三

注:文章是经过参考其他的文章然后自己整理出来的,有可能是小部分参考,也有可能是大部分参考,但绝对不是直接转载,觉得侵权了我会删,我只是把这个用于自己的笔记,顺便整理下知识的同时,能帮到一部分人。
ps : 有错误的还望各位大佬指正,小弟不胜感激

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值