synchronized的两个作用:原子性和内存可见性。
- 在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整(重排序)。
- 失效数据
- 非原子的64位操作
加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
volatile
- 不进行重排序。
- 不会被缓存在寄存器或者对其他处理器不可见的地方。
- 不加锁,不阻塞,轻量级。
正确使用方式:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中。
- 在访问变量时不需要加锁。
发布
逸出:某个不该发布的对象被发布了
发布某个对象时,会间接发布其他对象:
- 如map、数组里面保存的对象
- 被发布的对象的非私有域中引用的对象
- 可以通过非私有的变量引用和方法调用到达的其他对象
- 一个对象传递给了“外部方法”(公有方法或者可以被继承的方法)
- 内部类实例里面隐式的this引用
当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。
所以,不要在构造过程中使this引用逸出。如,在构造函数中启动一个线程。或者在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是final方法),同样会导致this引用在构造过程中逸出。
线程封闭
一种避免使用同步的方式就是不共享数据。
将某个对象封闭在一个线程中。
- Ad-hoc线程封闭技术
- 栈封闭:局部变量
- ThreadLocal类:类似于全局变量,降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
不变性
满足同步需求的另一种方法是使用不可变对象。
不可变对象一定是线程安全的。
满足以下条件时,对象才是不可变的:
- 对象创建以后其状态就不能修改
- 对象的所有域都是final类型
- 对象是正确创建的(创建期间,this引用没有逸出)
除非需要某个域是可变的,否则应将其声明为final。
对于在访问和更新多个相关变量时出现的竞态条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。
可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时必须使用同步。
一个正确构造的对象可以通过以下方式来安全地发布(只是发布,不包含共享):
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中