什么是线程安全?
当多个线程去访问某个类时, 如果类会表现出我们预期出现的行为, 那么可以称这个类是线程安全的.
什么时候会出现线程不安全?
操作并非原子. 多个线程执行某段代码, 如果这段代码产生的结果受不同线程之间的执行时序影响, 而产生非预期的结果, 即发生了竞态条件, 就会出现线程不安全;
常见场景:
count++. 它本身包含三个操作, 读取, 修改, 写入, 多线程时, 由于线程执行的时序不同, 有可能导致两个线程执行后 count 只加了 1, 而原有的目标确实希望每次执行都加 1;
单例. 多个线程可能同时执行到 instance == null 成立, 然后新建了两个对象, 而原有目标是希望这个对象永远只有一个;
- public MyObj getInstance(){ if (instance == null){
- instance = new MyObj();
- }
- return instance
- }
复制代码
解决方式是: 当前线程在操作这段代码时, 其它线程不能对进行操作
常见方案:
单个状态使用 java.util.concurrent.atomic 包中的一些原子变量类, 注意如果是多个状态就算每个操作是原子的, 复合使用的时候并不是原子的;
加锁. 比如使用 synchronized 包围对应代码块, 保证多线程之间是互斥的, 注意应尽可能的只包含在需要作为原子处理的代码块上;
synchronized 的可重入性
当线程要去获取它自己已经持有的锁是会成功的, 这样的锁是可重入的, synchronized 是可重入的
- class Paxi {
- public synchronized void sayHello(){
- System.out.println("hello");
- }
- }
- class MyClass extends Paxi{
- public synchronized void dosomething(){
- System.out.println("do thing ..");
- super.sayHello();
- System.out.println("over");
- }
- }
复制代码
它的输出为
- do thing ..
- hello
- over
复制代码
修改不可见. 读线程无法感知到其它线程写入的值
常见场景:
重排序. 在没有同步的情况下, 编译器, 处理器以及运行时等都有可能对操作的执行顺序进行调整, 即写的代码顺序和真正的执行顺序不一样, 导致读到的是一个失效的值
读取 long,double 等类型的变量. JVM 允许将一个 64 位的操作分解成两个 32 位的操作, 读写在不同的线程中时, 可能读到错误的高低位组合
常见方案:
加锁. 所有线程都能看到共享变量的最新值;
使用 Volatile 关键字声明变量. 只要对这个变量产生了写操作, 那么所有的读操作都会看到这个修改;
注意: Volatile 并不能保证操作的原子性, 比如 count++ 操作同样有风险, 它仅保证读取时返回最新的值. 使用的好处在于访问 Volatile 变量并不会执行加锁操作, 也就不会阻塞线程.
不同步的情况下如何做到线程安全?
线程封闭. 即仅在单线程内访问数据, 线程封闭技术有以下几种:
Ad-hoc 线程封闭. 即靠自己写程序来实现, 比如保证程序只在单线程上对 volatile 进行
读取 - 修改 - 写入
栈封闭. 所有的操作都反生执行线程的栈中, 比如在方法中的一个局部变量
ThreadLocal 类. 内部维护了每个线程和变量的一个独立副本
只读共享. 即使用不可变的对象.
使用 final 去修饰字段, 这样这个字段的 "值" 是不可改变的
注意 final 如果修饰的是一个对象引用, 比如 set, 它本身包含的值是可变的
创建一个不可变的类, 来包含多个可变的数据.
- class OneValue{
- // 创建不可变对象, 创建之后无法修改, 事实上这里也没有提供修改的方法
- private final BigInteger last;
- private final BigInteger[] lastfactor;
- public OneValue(BigInteger i,BigInteger[] lastfactor){
- this.last=i;
- this.lastfactor=Arrays.copy(lastfactor,lastfactor.length);
- }
- public BigInteger[] getF(BigInteger i){
- if(last==null || !last.equals(i)){
- return null;
- }else{
- return Arrays.copy(lastfactor,lastfactor.length)
- }
- }
- }
- class MyService {
- //volatile 使得 cache 一经更改, 就能被所有线程感知到
- private volatile OneValue cache=new OneValue(null,null);
- public void handle(BigInteger i){
- BigInteger[] lastfactor=cache.getF(i);
- if(lastfactor==null){
- lastfactor=factor(i);
- // 每次都封装最新的值
- cache=new OneValue(i,lastfactor)
- }
- nextHandle(lastfactor)
- }
- }
复制代码
如何构造线程安全的类?
实例封闭. 将一个对象封装到另一个对象中, 这样能够访问被封装对象的所有代码路径都是已知的, 通过合适的加锁策略可以确保被封装对象的访问是线程安全的.
java 中的 Collections.synchronizedList 使用的原理就是这样. 部分代码为
- public static <T> List<T> synchronizedList(List<T> list) {
- return (list instanceof RandomAccess ?
- new SynchronizedRandomAccessList<>(list) :
- new SynchronizedList<>(list));
- }
复制代码
SynchronizedList 的实现, 注意此处用到的 mutex 是内置锁
- static class SynchronizedList<E>
- extends SynchronizedCollection<E>
- implements List<E> {
- private static final long serialVersionUID = -7754090372962971524L;
- final List<E> list;
- public E get(int index) {
- synchronized (mutex) {return list.get(index);}
- }
- public E set(int index, E element) {
- synchronized (mutex) {return list.set(index, element);}
- }
- public void add(int index, E element) {
- synchronized (mutex) {list.add(index, element);}
- }
- public E remove(int index) {
- synchronized (mutex) {return list.remove(index);}
- }
- }
复制代码
mutex 的实现
- static class SynchronizedCollection<E> implements Collection<E>,>Serializable {
- private static final long serialVersionUID = 3053995032091335093L;
- final Collection<E> c; // Backing Collection
- final Object mutex; // Object on which to synchronize
- SynchronizedCollection(Collection<E> c) {
- if (c==null)
- throw new NullPointerException();
- this.c = c;
- mutex = this; // mutex 实际上就是对象本身
- }
复制代码
java 的监视器模式, 将对象所有可变状态都封装起来, 并由对象自己的内置锁来保护, 即是一种实例封闭. 比如 HashTable 就是运用的监视器模式. 它的 get 操作就是用的 synchronized, 内置锁, 来实现的线程安全
- public synchronized V get(Object key) {
- Entry tab[] = table;
- int hash = hash(key);
- int index = (hash & 0x7FFFFFFF) % tab.length;
- for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
- if ((e.hash == hash) && e.key.equals(key)) {
- return e.value;
- }
- }
- return null;
- }
复制代码
内置锁
每个对象都有内置锁. 内置锁也称为监视器锁. 或者可以简称为监视器
线程执行一个对象的用 synchronized 修饰的方法时, 会自动的获取这个对象的内置锁, 方法返回时自动释放内置锁, 执行过程中就算抛出异常也会自动释放.
以下两种写法等效:
- synchronized void myMethdo(){
- //do something
- }
- void myMethdo(){
- synchronized(this){
- //do somthding
- }
- }
复制代码
官方文档
私有锁
形式如下
- public class PrivateLock{
- private Object mylock = new Object(); // 私有锁
- void myMethod(){
- synchronized(mylock){
- //do something
- }
- }
- }
复制代码
它也可以用来保护对象, 相对内置锁, 优势在于私有锁可以有多个, 同时可以让客户端代码显示的获取私有锁
类锁
在 staic 方法上修饰的, 一个类的所有对象共用一把锁
把线程安全性委托给线程安全的类
如果一个类中的各个组件都是线程安全的, 该类是否要处理线程安全问题?
视情况而定.
只有单个组件, 且它是线程安全的.
- public class DVT{ private final ConcurrentMap<String,Point> locations; private final Map<String,Point> unmodifiableMap;
- public DVT(Map<String,Point> points){
- locations=new ConcurrentHashMap<String,Point>(points);
- unmodifiableMap=Collections.unmodifiableMap(locations);
- }
- public Map<String,Point> getLocations(){
- return unmodifiableMap;
- }
- public Point getLocation(String id){
- return locations.get(id);
- }
- public void setLocation(String id,int x,int y){
- if(locations.replace(id,new Point(x,y))==null){
- throw new IllegalArgumentException("invalid"+id);
- }
- }
- }
- public class Point{
- public final int x,y;
- public Point(int x,int y){
- this.x=x;
- this.y=y;
- }
- }
复制代码
线程安全性分析
Point 类本身是无法更改的, 所以它是线程安全的, DVT 返回的 Point 方法也是线程安全的
DVT 的方法 getLocations 返回的对象是不可修改的, 是线程安全的
setLocation 实际操作的是 ConcurrentHashMap 它也是线程安全的
综上, DVT 的安全交给了'locations', 它本身是线程安全的, DVT 本身虽没有任何显示的同步, 也是线程安全. 这种情况下, 就是 DVT 的线程安全实际是委托给了'locations', 整个 DVT 表现出了线程安全.
线程安全性委托给了多个状态变量
只要多个状态变量之间彼此独立, 组合的类并不会在其包含的多个状态变量上增加不变性.
依赖的增加则无法保证线程安全
- public class NumberRange{
- private final AtomicInteger lower = new AtomicInteger(0);
- private final AtomicInteger upper = new AtomicInteger(0);
- public void setLower(int i){
- // 先检查后执行, 存在隐患
- if (i>upper.get(i)){
- throw new IllegalArgumentException('can not ..');
- }
- lower.set(i);
- }
- public void setUpper(int i){
- // 先检查后执行, 存在隐患
- if(i<lower.get(i)){
- throw new IllegalArgumentException('can not ..');
- }
- upper.set(i);
- }
- }
复制代码
setLower 和 setUpper 都是'先检查后执行'的操作, 但是没有足够的加锁机制保证操作的原子性. 假设原始范围是 (0,10), 一个线程调用 setLower(5), 一个设置 setUpper(4) 错误的执行时序将可能导致结果为(5,4)
如何对现有的线程安全类进行扩展?
假设需要扩展的功能为 '没有就添加'.
直接修改原有的代码. 但通常没有办法修改源代码
扩展类. 继承原有的代码, 添加新的功能. 但是同步策略保存在两份文件中, 如果底层同步策略变更, 很容易出问题
客户端加锁. 将类放入一个辅助类中, 通过辅助类的操作代码. 比如扩展 Collections.synchronizedList. 期间需要注意锁的机制, 错误方式为
- public class ListHelper<E>{
- public List<E> list=Collections.synchronizedList(new ArrayList<E>());
- ...
- public synchronized boolean putIfAbsent(E x){
- boolean absent = !list.contains(x);
- if(absent){
- list.add(x);
- }
- return absent;
- }
- }
这里的 putIfAbsent 并不能带来线程安全, 原因是 list 的内置锁并不是 ListHelper, 也就是 putIfAbsent 相对 list 的其它方法并不是原子的. Collections.synchronizedList 是锁在 list 本身的, 正确方式为
- public boolean putIfAbsent(E x){
- synchronized(list){
- boolean absent = !list.contains(x);
- if(absent){
- list.add(x);
- }
- return absent;
- }
- }
4. 组合. 不管要操作的类是否是线程安全, 对类统一添加一层额外的锁.
> 实现参考 Collections.synchronizedList 方法
来源: https://juejin.im/post/5b7d68f66fb9a019d80a9002