并发基础

线程安全性即当多个线程访问某个类时, 这个类始终能表现出正确的行为.

编写线程安全的代码, 核心是对状态访问进行管理, 特别是对可变得和共享的状态访问, 状态存储在实例或静态与中的数据, 状态可能包括其他依赖对象的域, 如HashMap的Map.Entry

并发编程中, 由于不恰当的执行时序二出现不正确结果现象称之为竟态条件.

# 保证线程安全性

  • 使用线程安全类

使用AtomicLong, AtomicReference, ConcurrentHashMap等线程安全类.

  • 适当的加锁

每个java对象都可以用作一个实现同步的锁. 使用同步代码块时,代码快的作用范围应当尽量小, 避免出现活跃性问题. 尽量不要使用不同的加锁机制, 不仅会带来混乱, 也不会在性能和安全性上带来好处.

内置锁是可以重入的, 重入意味着获取锁的操作的粒度是线程而不是调用.

若使用同步来协调对某个变量的访问, 那么在访问这个变量的所有位置都要加锁, 且使用同一个锁.

可以将所有可变状态都封装到一个对象中, 然后使用这个对象的内置锁来对访问可变状态的代码路径进行同步.

即使每个方法都作为同步方法使用同一把锁, 但如果出现复合操作仍然需要额外的加锁机制.

  • 避免多个线程同一时间访问相同数据

# 控制对象的共享

# 对象的发布与逸出


发布指使对象能够在当前作用域之外的代码中使用. 当某个不该发布的对象被发布时, 这种情况称之为逸出.

发布对象的方式:

  • 将对象的引用保存在一个拥有的静态变量中
  • 当发布一个对象时, 在这个对象的非私有域(非private修饰)中引用的所有对象也会被发布.
  • 发布一个内部的类实例.

安全发布对象

  • 将对象的引用保存到volatile类型的域或者AtomicReference中
  • 将对象的引用保存到某个正确构造对象的final域中.
  • 将对象的引用保存到一个由锁保护的域中.
  • 在静态初始化函数中初始化一个对象引用, 由JVM内部的同步机制来保证安全发布.

不安全发布

this引用在构造方法中逸出. 当且仅当对象的构造方法返回时, 对象才处于可预测的和一致的状态

    public class A{
        public A(){
            ...
            B.doSomething(this);
            ...
        }
    }
1
2
3
4
5
6
7

在构造方法中启动一个线程, 当对象在其构造方法中创建一个线程时, 无论是显示创建还是隐式创建, this引用都会被新创建的线程共享

    public class A {
    
        public A(){
            ...
               new Thread(() -> {...}).start().
            ...
        }
    }
1
2
3
4
5
6
7
8

# 线程封闭


  • 数据只在单个线程内访问.
  • 局部变量(栈封闭), ThreadLocal.
  • volatile的特殊线程封闭, 只要能够保证只有一条线程对volatile执行写入操作, 就可以安全的在这些共享的volatile变量上执行读写

# 对象的不变性


  • 不可变对象一定是线程安全的, 它只有一种状态, 并且由构造方法控制.
  • 不可变对象创建后其状态不能再修改, 所有域都是final的.
  • 当需要一组相关数据以原子方式来执行某个操作时, 就可以考虑创建一个不可变类来包含这些数据
  • 对象创建时, this引用没有逸出.

# 对象的组合

  1. 设计线程安全类

设计线程安全类的基本要素: 找出构造对象状态的所有变量, 找出约束状态变量的不变性条件. 建立对象状态的并发访问策略. 并发访问策略规定了如何将不变性, 线程封闭和加锁机制组合起来维护线程安全性, 还规定了哪些变量需要加锁保护

  1. 实例封闭

将数据封装到对象内部, 将数据的访问限制在对象方法.

上次更新: 2022/3/11 15:12:48