线程安全策略与安全发布

线程安全策略

线程安全策略种类

  线程安全策略大致上有如下几种方式:

  • 尽量使用不可变对象
  • 尽量使用线程封闭
  • 尽量使用同步容器
  • 尽量使用并发容器

不可变对象

  不可变对象需要满足的条件

  • 对象创建以后其状态就不能被修改
  • 对象所有域都是final类型
  • 对象是正确的创建(在对象创建期间,this引用没有逸出)

  要想使一个对象成为不可能对象,可以通过以下几种方式:

  • 将类声明为final,这样它就不能被继承了
  • 将所有成员声明为私有,就不允许直接访问这些成员
  • 对变量不提供set方法,将所有可变的成员变量命名为final,这样只能对它们赋值一次
  • 通过构造器初始化所有成员,进行深度拷贝
  • get方法中不直接返回对象本身,而是克隆对象并返回对象的拷贝

  举例:

  如下方式是通过保证共享对象不会被任何线程更新,方法是使共享对象不可变,从而保证线程安全。

public class ImmutableValue{

  private int value = 0;

  public ImmutableValue(int value){
    this.value = value;
  }

  public int getValue(){
    return this.value;
  }
}

  在上述举例中,我们需要注意ImmutableValue实例的值是如何在构造函数中传递的。另请注意没有setter方法。一旦ImmutableValue实例被创建,我们就不能改变它的值。这是不可改变的。但是,我们可以使用该getValue()方法获取它。

  如果需要对ImmutableValue实例执行操作,可以通过返回具有操作产生的值的新实例来执行此操作。以下是添加操作的示例:

public class ImmutableValue{

  private int value = 0;

  public ImmutableValue(int value){
    this.value = value;
  }

  public int getValue(){
    return this.value;
  }

  
      public ImmutableValue add(int valueToAdd){
      return new ImmutableValue(this.value + valueToAdd);
      }
  
}

  注意该add()方法如何返回ImmutableValue带有add操作结果的新实例,而不是将值添加到自身。

  final关键字

修饰地点特点
修饰类不能被继承
修饰方法锁定方法不能被继承类修改;一个类中的private方法会被隐式的指定为final方法
修饰变量如果是基本数据类型变量,那么它的数值一旦被初始化之后便不能再修改;如果是引用类型变量,则在对其进行初始化之后,便不能让它再指向另外一个对象

  其它不可变对象

来源举例
Collections.unmodifiableXXXCollectionListSetMapetc
Guava:ImmutableXXXCollectionListSetMapetc

线程封闭

  它其实是把对象封装到一个线程里,只有这一个线程能够看到这个对象,那么即便是这个对象不是线程安全的,也不会出现任何线程安全方面的问题了,因为它只能在一个线程里面进行访问。

  线程封闭的种类

方式特点
Ad-hoc线程封闭程序控制实现,最糟糕,忽略
堆栈封闭局部变量,无并发问题
ThreadLocal线程封闭特别好的线程封闭方法

  名词解释:

  堆栈封闭:多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份,到线程的栈中。所以局部变量是不会被多个线程所共享的,因此也就不会出现并发问题。

  ThreadLocal线程封闭:每个Thread线程内部都有一个map,这个map是以线程本地对象作为key,以线程的变量副本作为Value。同时这个map是由TreadLoal来维护的,由ThreadLocal负责向map里设置线程的变量值以及获取值。所以对于不同的线程,每次获取副本值的时候,别的线程并不能获取到当前线程的副本值,于是就形成了线程间副本的隔离,做到了多个线程互不干扰。

同步容器

  非同步容器及其与其对应的同步容器

非同步容器同步容器
ArrayListVectorStack
HashMapHashTablekeyvalue不能为null
java.util包下的ListSetMapCollections.synchronizedXXX(ListSetMap)

并发容器

  非并发容器及其与其对应的并发容器

非并发容器并发容器
ArrayListCopyOnWriteArrayList
HashSetTreeSetCopyOnWriteArraySetConcurrentSkipListSet
HashMapTreeMapConcurrentHashMapConcurrentSkipListMap

CopyOnWriteArrayList

  CopyOnWriteArrayList是线程安全的,字面上的理解就是写操作时复制,当有新元素添加到CopyOnWriteArrayList时,它先从原有的数组里面拷贝一份出来,然后在新的数组上做写操作。写完之后,再将原来的数组指向新的数组。CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的,这么做主要是为了避免在多线程并发做add操作时复制出多个副本出来把数据搞乱了,导致最终的数组数据不是我们所期望的。

  CopyOnWriteArrayList的几个缺点,第一个缺点由于它做写操作的时候需要拷贝数组,就会消耗内存,如果原数组的内容比较多的情况下,可能会导致Yong GCFull GC

  它的第二个缺点就是它不能用于实时读的场景,比如说拷贝数组,新增数据都需要时间,所以当我们调用一个set操作后,读取到的数据可能还是旧的,虽然CopyOnWriteArrayList它能够做到最终的一致性,但是它没法满足我们实时性的要求。因此CopyOnWriteArrayList它更适合读多写少的场景。当然,如果你无法保证CopyOnWriteArrayList中到底要放置多少数据,也不知道到底要addset多少次操作,那么这个类建议慎用。因为如果数据稍微有点多,每次操作的时候都要重新操作数组,这个代价可能特别的高昂。在高性能的互联网应用中,这种操作可能会分分钟引起故障。

  由于实际上通常线程操作的List不是很大,修改操作也会很少,因此在绝大多数场景下,CopyOnWriteArrayList数组都可以很容易的代替ArrayList满足线程安全。

  CopyOnWriteArrayList的设计思想:

  • 读写分离,让读和写分开;
  • 最终一致性,因为我在Copy的过程中它可能会需要一些时间,它保证最终这个List的结果是对的;
  • 使用时另外开辟空间,通过这种方式,来解决掉并发冲突。

  CopyOnWriteArrayList读操作是在原数组上进行的,是不需要加锁的,而写操作时需要加锁,它为了避免多个线程并发修改复制出多个副本出来把数据搞乱。

CopyOnWriteArrayset

  它是线程安全的,它的底层实现是使用了我们刚才所介绍的CopyOnWriteArrayList,因此它也适合于大小通常是很小的set集合,只读操作远大于可变的操作。因为它通常需要复制整个基础数组,所以对于可变的操作,包括Add,SetRemove等等相对仍大一些,然后迭代器不支持可变的Remove操作。它使用迭代器进行遍历的时候,速度很快,而且不会与其它线程发生冲突。

ConcurrentSkipListSet

  它是JDK6新增的类,它和TreeSet一样,它是支持自然排序的,并且在构造的时候它可以自己定义比较细,和其它Set集合一样,ConcurrentSkipListSet它是基于Map集合的,在多线程环境下,ConcurrentSkipListSet它里面的Contains方法,Add,Remove操作都是线程安全的,多个线程可以安全的并发的执行插入、移除和访问操作。但是对于那些批量操作,比如AddAllRemoveAll,ReturnallContainsALL等并不能保证以原子方式执行。因为AddAllRemoveAll,ReturnallContainsALL等底层调用的还是ContainsAddRemove方法。在批量操作时,只能保证每一次的ContainsAddRemove操作是原子性的,它代表的是在进行ContainsAddRemove三个操作时不会被其它线程打断。但是它不能保证每一次批量操作都不会被其它线程打断。在使用ConcurrentSkipListSetAddAllRemoveAllReturnAllContainsAll等这些方法的时候,还是需要自己手动做一些同步操作才可以。比如加上锁,保证在同一时间内只允许在一个线程调用批量操作。同时关于ConcurrentSkipListSet,它这种类是不允许使用空元素的,就是我们Java里的null,因为它无法可靠的将参数及返回值与不存在的元素区分开来。

ConcurrentHashMap

  ConcurrentHashMap它是HashMap的线程安全版本,要注意的是ConcurrentHashMap不允许空值,在实际的应用中,除了少数的插入操作和删除操作外,绝大部分我们使用Map都是读取操作,而且读操作在大多数都是成功的,基于这个前提ConcurrentHashMap针对读操作做了大量的优化,因此这个类具有特别高的并发性,高并发场景下有特别好的表现。

ConcurrentSkipListMap

  ConcurrentSkipListMap它是TreeMap的线程安全版本,内部是使用SkipList这种跳表结构来实现的。

  有人拿ConcurrentHashMapConcurrentSkipListMap做过性能测试,在4个线程,1.6万数据量的情况下,ConcurrentHashMap存取速度是ConcurrentSkipListMap速度的4倍左右,但是ConcurrentSkipListMap有几个ConcurrentHashMap不能比拟的有点

  • ConcurrentSkipListMap它的Key是有序的,这个ConcurrentHashMap是做不到的,
  • ConcurrentSkipListMap它支持更高的并发,它的存取时间和线程数几乎没有关系的,也就是说在数据量一定的情况下,并发的线程越多ConcurrentSkipListMap越能体现出它的优势来。在非多线程的情况下,我们应该尽量使用TreeMap,此外对于并发性较低的程序,我们也可以使用Collections里面的类,它有一个方法叫做SynchronizedSortedMap,它是将TreeMap进行包装,也可以提供较好的效率。

  所以在高并发场景中,如果需要对Map的键值进行排序时,也要尽量使用ConcurrentSkipListMap,可以得到更好的并发度。

安全发布对象

  安全发布对象的四种方式

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到Volatile类型域或者AtomicReference对象中
  • 讲对象的引用保存到某个正确构造对象的final类型域中
  • 讲对象的引用保存到一个由锁保护的域中

参考:thread-safety-and-immutability

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值