线程安全策略大致上有如下几种方式:
不可变对象需要满足的条件
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.unmodifiableXXX |
Collection 、List 、Set 、Map 、etc |
Guava:ImmutableXXX |
Collection 、List 、Set 、Map 、etc |
它其实是把对象封装到一个线程里,只有这一个线程能够看到这个对象,那么即便是这个对象不是线程安全的,也不会出现任何线程安全方面的问题了,因为它只能在一个线程里面进行访问。
线程封闭的种类
方式 | 特点 |
---|---|
Ad-hoc 线程封闭 |
程序控制实现,最糟糕,忽略 |
堆栈封闭 | 局部变量,无并发问题 |
ThreadLocal 线程封闭 |
特别好的线程封闭方法 |
名词解释:
堆栈封闭:多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份,到线程的栈中。所以局部变量是不会被多个线程所共享的,因此也就不会出现并发问题。
ThreadLocal
线程封闭:每个Thread
线程内部都有一个map
,这个map
是以线程本地对象作为key
,以线程的变量副本作为Value
。同时这个map
是由TreadLoal
来维护的,由ThreadLocal
负责向map
里设置线程的变量值以及获取值。所以对于不同的线程,每次获取副本值的时候,别的线程并不能获取到当前线程的副本值,于是就形成了线程间副本的隔离,做到了多个线程互不干扰。
非同步容器及其与其对应的同步容器
非同步容器 | 同步容器 |
---|---|
ArrayList |
Vector 、Stack |
HashMap |
HashTable (key 、value 不能为null ) |
java.util 包下的List 、Set 、Map |
Collections .synchronizedXXX (List 、Set 、Map ) |
非并发容器及其与其对应的并发容器
非并发容器 | 并发容器 |
---|---|
ArrayList |
CopyOnWriteArrayList |
HashSet 、TreeSet |
CopyOnWriteArraySet 、ConcurrentSkipListSet |
HashMap 、TreeMap |
ConcurrentHashMap 、ConcurrentSkipListMap |
CopyOnWriteArrayList
是线程安全的,字面上的理解就是写操作时复制,当有新元素添加到CopyOnWriteArrayList
时,它先从原有的数组里面拷贝一份出来,然后在新的数组上做写操作。写完之后,再将原来的数组指向新的数组。CopyOnWriteArrayList
的整个add
操作都是在锁的保护下进行的,这么做主要是为了避免在多线程并发做add
操作时复制出多个副本出来把数据搞乱了,导致最终的数组数据不是我们所期望的。
CopyOnWriteArrayList
的几个缺点,第一个缺点由于它做写操作的时候需要拷贝数组,就会消耗内存,如果原数组的内容比较多的情况下,可能会导致Yong GC
和Full GC
它的第二个缺点就是它不能用于实时读的场景,比如说拷贝数组,新增数据都需要时间,所以当我们调用一个set
操作后,读取到的数据可能还是旧的,虽然CopyOnWriteArrayList
它能够做到最终的一致性,但是它没法满足我们实时性的要求。因此CopyOnWriteArrayList
它更适合读多写少的场景。当然,如果你无法保证CopyOnWriteArrayList
中到底要放置多少数据,也不知道到底要add
或set
多少次操作,那么这个类建议慎用。因为如果数据稍微有点多,每次操作的时候都要重新操作数组,这个代价可能特别的高昂。在高性能的互联网应用中,这种操作可能会分分钟引起故障。
由于实际上通常线程操作的List
不是很大,修改操作也会很少,因此在绝大多数场景下,CopyOnWriteArrayList
数组都可以很容易的代替ArrayList
满足线程安全。
CopyOnWriteArrayList
的设计思想:
Copy
的过程中它可能会需要一些时间,它保证最终这个List
的结果是对的; CopyOnWriteArrayList
读操作是在原数组上进行的,是不需要加锁的,而写操作时需要加锁,它为了避免多个线程并发修改复制出多个副本出来把数据搞乱。
它是线程安全的,它的底层实现是使用了我们刚才所介绍的CopyOnWriteArrayList
,因此它也适合于大小通常是很小的set
集合,只读操作远大于可变的操作。因为它通常需要复制整个基础数组,所以对于可变的操作,包括Add
,Set
和Remove
等等相对仍大一些,然后迭代器不支持可变的Remove
操作。它使用迭代器进行遍历的时候,速度很快,而且不会与其它线程发生冲突。
它是JDK6
新增的类,它和TreeSet
一样,它是支持自然排序的,并且在构造的时候它可以自己定义比较细,和其它Set
集合一样,ConcurrentSkipListSet
它是基于Map
集合的,在多线程环境下,ConcurrentSkipListSet
它里面的Contains
方法,Add
,Remove
操作都是线程安全的,多个线程可以安全的并发的执行插入、移除和访问操作。但是对于那些批量操作,比如AddAll
、RemoveAll
,Returnall
和ContainsALL
等并不能保证以原子方式执行。因为AddAll
、RemoveAll
,Returnall
和ContainsALL
等底层调用的还是Contains
,Add
,Remove
方法。在批量操作时,只能保证每一次的Contains
、Add
、Remove
操作是原子性的,它代表的是在进行Contains
、Add
、Remove
三个操作时不会被其它线程打断。但是它不能保证每一次批量操作都不会被其它线程打断。在使用ConcurrentSkipListSet
的AddAll
、RemoveAll
、ReturnAll
、ContainsAll
等这些方法的时候,还是需要自己手动做一些同步操作才可以。比如加上锁,保证在同一时间内只允许在一个线程调用批量操作。同时关于ConcurrentSkipListSet
,它这种类是不允许使用空元素的,就是我们Java
里的null
,因为它无法可靠的将参数及返回值与不存在的元素区分开来。
ConcurrentHashMap
它是HashMap
的线程安全版本,要注意的是ConcurrentHashMap
不允许空值,在实际的应用中,除了少数的插入操作和删除操作外,绝大部分我们使用Map
都是读取操作,而且读操作在大多数都是成功的,基于这个前提ConcurrentHashMap
针对读操作做了大量的优化,因此这个类具有特别高的并发性,高并发场景下有特别好的表现。
ConcurrentSkipListMap
它是TreeMap
的线程安全版本,内部是使用SkipList
这种跳表结构来实现的。
有人拿ConcurrentHashMap
与ConcurrentSkipListMap
做过性能测试,在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