06.04 java 多线程 对象的安全发布

简单解释下多线程中的重要概念


活跃性: 意味着正确的事情最终会发生

活跃性问题: 比如单线程中出现的无限循环

性能问题:和活跃性问题相对, 活跃性问题意味着正确的事情一定会发生,但是不够好,我们希望正确的事情尽快发生

线程安全性: 一个类,被多个线程同时调用,且多个线程不需要额外的同步机制,就能保证这个类的方法被正确执行,那么这个类就是安全的

原子性:count++就不是原子操作,它分为读取修改写入三个操作,boolean的读写是原子操作

竞态条件: 最常见的就是先检查后执行.意味着程序的正确性依赖于多线程执行时序的正确性

不变性条件:即一组操作,通过加锁的方式,保证这组操作以原子操作的方式执行

复合操作:访问一个状态的所有操作,要以原子性的方式访问, 即这组操作只能同时在同一个线程中执行,其他线程必须等待

重入: 同一个线程可以重入他获得的所有锁

重排序: 编译器和运行时和处理器可能对操作进行重排序(处于性能方面的考虑), 比如构造器中初始化对象中的变量和返回这个对象引用,这两个操作,可能出现构造器没有完全初始化,引用就被返回,.这种现象被称为不安全的发布

可见性问题:当前线程对共享变量的修改对其他线程不可见,内置锁可以保证同步代码块中的变量的更新对其他所有线程可见

发布:将一个对象暴露到多线程环境中

逸出:一个本不该被暴露到多线程环境的对象被错误的暴露到多线程环境中

构造器中的this指针逸出:不论是逸出到多线程还是当前线程,都会出错,可能读取到的是未完全初始化的变量值,比如int i,首先默认设置为0,然后在构造器中初始化为100,如果构造器中this指针逸出,那this可能读取到不正确的0值

安全地构造对象: 保证this指针不会再构造器中逸出即可,可以使用静态方法进行初始化

线程封闭: 栈封闭:使用局部变量,局部变量一定是当前线程私有的, threadLocal 类:ThreadLocal中的数据被封闭在当前线程中

对象的不变性: 1,对象被正确创建(正确创建指的是构造过程this指针没有逸出)

2,对象创建之后,状态不允许被修改

3,所有的于都是final类型的(这个不需要,只要保证实际不可修改即可,比如吧对象放到ConccurencyMap中,外围类只提供isContain()方法). 同时不可变对象一定是线程安全的

为什么会产生安全发布这个问题

出于性能方面的考虑. 编译器,运行时,处理器可能会对操作进行重排序,比如创建对象时,执行构造器初始化实例的变量的操作,和返回该对象的引用这两个操作,返回对象的引用可能先被执行,于是在多线程环境下,其他线程可能会看到一个部分构造的对象

[java]

view plain copy

public class Main { public static Person person; public static void main(String[] args) { person = new Person(100);//步骤2 } } class Person { int name = 0; public Person(int name) { this.name = name;//步骤1 } }

以上程序可能出现一种情况,步骤1没有执行完毕,此时name的值依然是0,步骤2先执行了,同时person被发布到多线程环境中,其他线程看到的不是一个完整的视图,他看到了name=0,这种发布被称为不安全的发布,为了保证我们看到的是一个完全初始化的对象,我们就需要进行安全的发布

对象安全发布的方式

在静态初始化函数中初始化一个对象的引用

原理::虚拟机保证静态初始化器初始化的对象一定能被安全的发布

用volatile关键字标注对象的域或者使用AtomicReference来保存该对象

原理:volatile标注的域对象,一定能保证初始化完毕之后,对象引用才被返回

将对象保存在另一个对象的final域中,并在初始化构造器中初始化或者直接在域中初始化

原理:final标注的对象的域,一定能保证对象引用返回前,该对象中的final域已经成功初始化(除非出现this指针在构造器中逸出)

将对象保存在由锁保存的域中

原理:锁保证方法块之内一个线程持有,使用这种方式要保证锁返回前,内部所有的域都要被调用一次,(按笔者个人的理解,仅仅使用syncronized关键字并不足以保证对象被完整构造,可能导致对象的的引用被提前发布)

线程安全的容器,能保证对象被安全发布(比如SyncronizedMap,ConcurrencyMap,hashTable,等等)

接下来我们结合单例模式详细分析安全发布的问题

在静态初始化函数中安全初始化一个对象的引用

首先我们定义Person类

[java]

view plain copy

class Person { private int name = 0; public Person(int name) { this.name = name; } public int getName() { return name; } }

以下是安全初始化Person类最简单的方式:

[java] view plain copy

public class Singleton { private static Person person = new Person(100); public static Person getPerson() { return person; } }

如果初始化Person对象的工作量很大,我们可以使用内部类来延迟Person的初始化工作

[java]

view plain copy

public class Singleton { private static class PersonHolder{ public static Person person = new Person(10); } public static Person getPerson() { return PersonHolder.person; } }

这种方式保证了只有getPerson()方法被初次调用的时候PersonHolder类才会被初始化,

以上两种方式都是笔者比较推崇的安全初始化的方式,

结合final或者volatile使用syncronized关键字安全地初始化一个对象

有些同学可能会用以下方式返回一个单例的看似安全的对象:

[java] view plain copy

class Singleton { private
static Person person; public static Person getPerson() { if (person==null) {// 操作1 synchronized (Person.class){ if (person==null) { person = new Person(100);//操作 2 } } } return person;// 操作3 } }

示例1

这段代码看上去每次都能返回同一个对象,实际上这段代码存在两个问题:

一. 操作1和操作3是两个普通读, 可能会被重排序,导致操作3先去读取,发现person为为null,然后另一个线程初始化了person对象,操作1读取到person非空,最后返回的却是null,这在单例模式中是不被允许的

二.同样由于重排序,操作2发布person的引用时,他的构造器可能还没有构造完成,导致其他线程获取到一个部分初始化的对象,这就是典型的不安全的发布

为了解决问题一,我们可以引入局部变量instance,如下所示

[java] view plain copy

class Singleton { private static Person person; public static Person getPerson() { Person instance = person; if (instance == null) {//操作1 synchronized (Person.class) { instance=person; if (instance == null) { instance = new Person(100);//操作2 person = instance; } } } return person;//操作3 } }

由于局变量属于线程私有的变量,线程私有的同一变量的读-写-读不会被重排序,于是操作1,操作2,操作3不会被重排序

这就保证了getPerson()方法一定不会返回null值

接着我们解决问题二,处理安全发布问题,这个是本文重点关注的问题,我们可以使用volatile或者final来保证对象的安全发布

首先使用volatile关键字,代码如下:

[java] view plain copy

class Singleton { private static volatile Person person; public static Person getPerson() { Person instance = person; if (instance == null) { synchronized (Person.class) { instance=person; if (instance == null) { instance = new Person(100); person = instance; } } } return person; } }

这里我们只是加了一个volatile关键字,这就能保证person每次拿到对象引用的时候,该对象都已经初始化完毕.

接着我们使用final关键字来保证安全初始化:

[java] view plain copy

class
Singleton { private static volatile PersonWrapper personWrapper; public static Person getPersonWrapper() { PersonWrapper instance = personWrapper; if (instance == null) { synchronized (Person.class) { instance= personWrapper; if (instance == null) { instance = new PersonWrapper(new Person(100)); personWrapper = instance; } } } return personWrapper.person; } private static class PersonWrapper { public final Person person; private PersonWrapper(Person person) { this.person = person; } } }

虽然final也能保证初始化的安全性,但是代码并不直观

只使用synchronized关键字安全地初始化一个对象的引用

首先我们看看如下代码:

[java] view plain copy

class Singleton { private static Person person; public static synchronized Person getPerson() { if (person == null) { person = new Person(100);//操作1 } return person; } }

[java] view plain copy

这段代码同样不能安全的发布person对象,原因同示例1,那如果我们不想使用fianl或者volatile关键字,该怎么办呢,

我们可以在操作1之后,显示读取person对象中所有的域,这是因为线程的私有变量的写和读操作不能被重排序,这就能保证这些域在person引用发布

时,都已经完全初始化了,具体代码如下所示

[java] view plain copy

class Singleton22 { private static Person person; public static synchronized Person getPerson() { if (person == null) { if (person == null) { person = new Person(100); person.getName();// 显示获取所有对象域 } } return person; } }

安全发布的内容我们就讲完了

volatile和final关键字简单比较

共同点:都能保证一个对象被安全的发布,

不同点:

final.final的域,一旦初始化后不允许被修改,随意每次都能读到同一个值,(但是构造器中this指针逸出可能会出问题,不要这么做)

final标注在类上,表示该类不允许被继承,同时所有方法都被标注成final的,private的方法默认就是final的

final标注在方法上表示该方法不允许被重写

volatile

volatile能保证long和double写入操作是一个原子操作

volatile能保证内存可见性,volatile变量的写操作多所有线程可见,但不能保证count++这种复合操作的原子性,其实count++其实是三个操作,读-修改-写入,注意:volatile 变量的写操作不应该依赖于变量的原始值