资料来源

http://www.ibm.com/developerworks/library/j-jtp0618/

http://www.javaspecialists.eu/archive/Issue192.html

http://stackoverflow.com/questions/1621435/not-thread-safe-object-publishing

对象构造函数要做到线程安全

竞态条件

首先看看竞态条件的定义:

data race, or race condition, occurs when multiple threads or processes are reading and writing a shared data item, and the final result depends on the order in which the threads are scheduled。

也就是说,当有多个线程的时候,其中有线程写,并且另外有线程读,那么就会发生竞态条件,如果是多个线程都是读,那么就没有竞态条件(race condition)一个数据竞争的例子:

public class DataRace {
static int a = 0; public static void main() {
new MyThread().start();
a = 1;
} public static class MyThread extends Thread {
public void run() {
System.out.println(a);
}
}
}

上面由于线程调度的规则问题,使得存在race condition,实际上这里还有一个问题就是因为没有同步操作,存在可见性问题。

发布和逸出的定义

发布一个对象的意思指:是指对象能够在当前作用域之外的代码中使用。例如:

  • 将一个指向该对象的引用保存到其他代码能够访问的地方(非常重要)
  • 在某一个非私有的方法中返回该引用(非常重要)
  • 将该引用传递到其它类的方法。(非常重要)

比如第一种情况对应的发布是:

public class Test {
// 所有的类都共享,可以在别的作用域中使用
public static Integer n;
}

第二种情况是:

public class Test {
prvivate String[] test = new String[] {
"Hello",
"World",
};
public String[] getHelloOrWorld(){
return test;

}

第三种情况:

// 在构造函数中引入race condition
public class EventListener {
public EventListener(EventSource eventSource) {
// do our initialization
... // register ourselves with the event source
// 将自己发布给eventSource
eventSource.registerListener(this);
} public onEvent(Event e) {
// handle the event
}
}

逸出:当某个不应该发布的对象被发布的时候,这种情况就叫做逸出。

不要在构造期间发布this引用

一个构建线程安全类构造器的技巧,就是不要在构造器中发布this。也就是在构造器中,不要将this暴露给另外一个线程。有时这个过程是很明显的,比如你将this存到静态域中,或者是一个集合中,有时候是不明显的,你可能将一个一个非静态内部对象中实例在构造器中发布。构造器不是普通的方法,对于初始化的安全性,它们有着特殊的语义。一个对象被构造完成后,就应该认为是处于一致的,可预测的状态。如果发布一个没有完全构造完的对象,那么就会处于危险的状态。 比如下面的例子就引入了race condition。

当且仅当对象构造函数调用完成后,我们才认为对象处于一致的状态。

// 在构造函数中引入race condition
public class EventListener {
public EventListener(EventSource eventSource) {
// do our initialization
... // register ourselves with the event source
eventSource.registerListener(this);
} public onEvent(Event e) {
// handle the event
}
}

事件监听器在最后一个将自己暴露给事件源(因为EventSource eventSource对于别的线程是可能是可见的,所以别的线程可能看到了不完整的构造对象),看起来没有什么错误,实际上在不考虑重排序和可见性等问题,这段代码仍然有暴露不完整的EventListener对象的给别的线程。下面,我们将继承EventListener。

public class RecordingEventListener extends EventListener {
private final ArrayList list; public RecordingEventListener(EventSource eventSource) {
super(eventSource);
list = Collections.synchronizedList(new ArrayList());
} public onEvent(Event e) {
list.add(e);
super.onEvent(e);
} public Event[] getEvents() {
return (Event[]) list.toArray(new Event[0]);
}
}

由于JLS要求子类的构造函数在第一句中使用super来调用父类的构造函数,我们还没有构造完成的事件监听器就已经注册事件了(因为先执行父类的构造函数,在构造函数中调用了注册监听器的功能)。现在对于list字段就存在race condition。如果在这个时候发生了一个时间,在onEvent将会得到调用,那么list可能为null,这样就会抛出nullPointerException 异常。因为对于onEvent是没有必要检查list为空的情况,因为其是一个final的字段,我们假定构造完成后,那么它就应该正确的初始化了。

不要隐式的暴露this引用

也有可能在显式的使用this的情况下,暴露了this引用。非静态的内部类,也包含了this引用的复制品,如果创建里一个匿名内部类对象,并且将该对象暴露给其他线程,那么就和暴露this引用是样。

public class EventListener2 {
public EventListener2(EventSource eventSource) { eventSource.registerListener(
new EventListener() {
public void onEvent(Event e) {
eventReceived(e);
}
});
} public void eventReceived(Event e) {
}
}

这样EventListen2类和EventListen有同样的毛病,同样this引用被publish。

不要在构造器中启动线程

在上面的代码中,出现上述问题的一个特殊情况是在构造器中启动线程,因为当一个对象拥有线程时候,要么线程是这个对象的内部类,或者我们将this传递给Thread的构造函数。在这种情况下,我们应该提供一个start()方法,通过start()方法来启动线程,而不是在构造函数中启动。

publish的意思

不是所有在构造器中对this的引用都是有害的,只有那些可以对别的线程可以看到的引用才是有害的。决定是否与其他对象共享this引用,需要你非常了解对象的可见性和这个对象会对这个引用干什么。

public class Safe { 

  private Object me;
private Set set = new HashSet();
private Thread thread; public Safe() {
// Safe because "me" is not visible from any other thread
me = this; // Safe because "set" is not visible from any other thread
set.add(this); // Safe because MyThread won't start until construction is complete
// and the constructor doesn't publish the reference
thread = new MyThread(this);
} public void start() {
thread.start();
} private class MyThread(Object o) {
private Object theObject; public MyThread(Object o) {
this.theObject = o;
} ...
}
} public class Unsafe {
public static Unsafe anInstance;
public static Set set = new HashSet();
private Set mySet = new HashSet(); public Unsafe() {
// Unsafe because anInstance is globally visible
anInstance = this; // Unsafe because SomeOtherClass.anInstance is globally visible
SomeOtherClass.anInstance = this; // Unsafe because SomeOtherClass might save the "this" reference
// where another thread could see it
SomeOtherClass.registerObject(this); // Unsafe because set is globally visible
set.add(this); // Unsafe because we are publishing a reference to mySet
mySet.add(this);
SomeOtherClass.someMethod(mySet); // Unsafe because the "this" object will be visible from the new
// thread before the constructor completes
thread = new MyThread(this);
thread.start();
} public Unsafe(Collection c) {
// Unsafe because "c" may be visible from other threads
c.add(this);
}
}

正如上面看到的,安全的构造函数和非安全的构造函数和安全的构造函数很多地方很像,决定一个this是否对别的线程是否可见是非常困难的。最好的办法是在构造函数中不使用this引用(包含显示或隐式)。在构造器中创建非static的、内部类的对象,一定要注意this的使用。

更多不要this逃逸的理由

上面的例子,让我们看到了同步的必要性。当线程A启动线程B的时候,JLS保证线程A所有可见变量对线程B都可见。这是JLS提供的一种隐式的同步策略。如果我们在构造函数中启动线程,那么当前构建对象都没有完成,那么B看见的都是不完整的。

更多的 this 逃逸

public class Test
{
private static Test lastCreatedInstance; public Test()
{
// .... 很多初始化工作
lastCreatedInstance = this;
}
}

如果lastCreatedInstance 前面有很多的初始化工作,但是另一个线程在初始化完成的时候使用了lastCreatedInstance,由于重排序(完全有可能)

另一个隐式this逃逸的例子

import java.util.*;

public class ThisEscape {
private final int num; public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
} private void doSomething(Event e) {
if (num != 42) {
System.out.println("Race condition detected at " +
new Date());
}
}
} public class Event { }
public interface EventListener {
public void onEvent(Event e);
}

对于EventSource是比较复杂的,在我们的例子EventSource是一个Thread,用来不断的发送事件给最新的监听器。因为我们是产生race condition,所以我们只是发送一个事件给listenner。

import java.util.concurrent.*;

public class EventSource extends Thread {
private final BlockingQueue<EventListener> listeners =
new LinkedBlockingQueue<EventListener>(); public void run() {
while (true) {
try {
listeners.take().onEvent(null);
} catch (InterruptedException e) {
break;
}
}
} public void registerListener(EventListener eventListener) {
listeners.add(eventListener);
}
}

开始测试

public class ThisEscapeTest {
public static void main(String[] args) {
EventSource es = new EventSource();
es.start();
while(true) {
new ThisEscape(es);
}
}
}

如果打印出来为不是42,就发生了race condition。 其实举这些例子,是自己不知道如何将this泄漏给其他的线程。

构建线程安全构造器总结

让一个不完整的对象被其他线程看到,显然我们是不愿意看到的。然而,如果将this(隐式或者显示)发布出去,那么就会导致不完整的对象构造,不是说发布了this就一定会导致现在安全的问题,而是best practice 就是尽量不要在构造函数中将this暴露出去,如果暴露了,就应该避免其他的线程能够看到。

如何正确的发布一个对象

正确发布一个对象遇到的两个问题:

  • 引用本身要被其他线程看到
  • 对象的状态要被其他线程看到

在多线程编程中,首要的原则,就是要避免对象的共享,因为如果没有对象的共享,那么多线程编写要轻松得多,但是,如果要共享对象,那么除了能够正确的将构造函数书写正确外,如何正确的发布也是一个很重要的问题。发布的概念见上面。

下面的代码:

public Holder holder;
public Holder {
int n;
public Holder(int n) { this.n = n };
public void assertSanity() {
if(n != n)
throw new AssertionError("This statement is false.");
}
}
// Thread 1
holder = new Holder(42); // Thread 2
hold.assertSanity(); //

由于没有使用同步的方法来却确保Holder对象(包含引用和对象状态都没有)对其他线程可见,因此将Holder成为未正确的发布。问题不在于Holder本身,而是其没有正确的发布。上面没有正确发布的可能导致的问题:

  • 别的线程对于holder字段,可能会看到过时的值,这样就会导致空引用,或者是过时的值(即使holder已经被设置了)(引用本身没有被别的线程看到)
  • 更可怕的是,对于已经更新holder,及时能够看到引用的更新,但是对于对象的状态,看到的却可能是旧值,对于上面的代码,可能会抛出AssertionError异常。具体分析如下;

比如在上面 holder = new Holder(42),这句代码中,分开理想的情况如下:

Alloc Memory to pointer1
Write 42 to pointer1 at offset 0
Write pointer1 to someStaticVariable

但是由于Java是一个弱一致模型,上面的步骤,可能分成这样:

Alloc Memory to pointer1
Write pointer1 to someStaticVariable
Write 42 to pointer1 at offset 0

上面发布的时候,由于没有同步的操作,这样照成Thread2在将将holder设置成42时,先调用assertSanity(),这是看到的n是一个垃圾值,然后在获取右边n的值时,如果42被设置,那么就可能抛出异常。总结如下:

  • 即使通过new 来发布,那么引用的值别的线程也不一定看到。
  • 即使别的线程能够看到引用的新值,然而new创建对象中的普通的成员变量(没有用final修饰),可能也看不到。(也就是说通过构造函数设置了普通成员变量的值,但是构造函数返回了,由于重排序导致别的线程看到的是以前的垃圾值)。

安全发布的常用模式

要安全的发布一个对象,对象的引用和对象的状态必须同时对其他线程可见。一般一个正确构造的对象(构造函数不发生this逃逸),可以通过如下方式来正确发布:

  • 在静态初始化函数中初始化一个对象引用
  • 将一个对象引用保存在volatile类型的域或者是AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的final类型的域中。
  • 将对象的引用保存到一个由锁保护的域中

在线程安全容器内部同步意味着,在将对象放到某个容器中,比如Vector中,将满足上面的最后一条需求。如果线程A将对象X放到一个线程安全的容器中,随后线程B读取这个对象,那么可以确保可以确保B看到A设置的X状态,即便是这段读/写X的应用程序代码没有包含显示的同步。下面容器内提供了安全发布的保证:

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全将它发布给任何从这些容器中访问它的线程。
  • 通过将某个元素放到Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchroizedList,可以将该元素安全的发布到任何从这些容器中访问该元素的线程。
  • 通过将元素放到BlockingQueue或者是ConcrrentLinkedQueue中,可以将该元素安全的发布到任何从这些访问队列中访问该元素的线程。

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态初始化器:

public static Holder =  new Holder(42);

静态初始化器由JVM在类的初始化阶段执行,由于JVM内部存在同步机制,所以这种方式初始化对象都可以被安全的发布。对于可变对象,安全的发布之时确保在发布当时状态的可见性,而在随后的每次对象的访问时,同样需要使用同步来确保修改操作的可见性。

Java多线程之构造与发布的更多相关文章

  1. Java多线程——不变性与安全发布

    1.不变性 某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象,不可变对象一定是线程安全的.不可变对象很简单.他们只有一种状态,并且该状态由构造函数来控制. 当满足以下条件时,对象才是 ...

  2. Java多线程——volatile关键字、发布和逸出

    1.volatile关键字 Java语言提供了一种稍弱的同步机制,即volatile变量.被volatile关键字修饰的变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在每次读取volatit ...

  3. Java多线程编程的常见陷阱(转)

    Java多线程编程的常见陷阱 2009-06-16 13:48 killme2008 blogjava 字号:T | T 本文介绍了Java多线程编程中的常见陷阱,如在构造函数中启动线程,不完全的同步 ...

  4. java多线程面试题整理及回答

    1)现在有T1.T2.T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行? 这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉.这个多线程问题比 ...

  5. 我也学习JAVA多线程-join

    在工作中,挺少遇到join关键字,但很多多线程资料和面试过程中,初中级开发工程师总会遇到join. 今天一起学习下join. join的作用:等待指定的时间(当为0时,一直等待),直到这个线程执行结束 ...

  6. JAVA多线程提高十:同步工具CyclicBarrier与CountDownLatch

    今天继续学习其它的同步工具:CyclicBarrier与CountDownLatch 一.CyclicBarrier CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公 ...

  7. Java多线程编程(4)--线程同步机制

    一.锁 1.锁的概念   线程安全问题的产生是因为多个线程并发访问共享数据造成的,如果能将多个线程对共享数据的并发访问改为串行访问,即一个共享数据同一时刻只能被一个线程访问,就可以避免线程安全问题.锁 ...

  8. Java多线程基础知识篇

    这篇是Java多线程基本用法的一个总结. 本篇文章会从一下几个方面来说明Java多线程的基本用法: 如何使用多线程 如何得到多线程的一些信息 如何停止线程 如何暂停线程 线程的一些其他用法 所有的代码 ...

  9. 第一章 Java多线程技能

    1.初步了解"进程"."线程"."多线程" 说到多线程,大多都会联系到"进程"和"线程".那么这两者 ...

随机推荐

  1. ctypes 调用 dll

    1. 加载 Windows API 和 C 运行库 先看例子 from ctypes import * u32 = windll.LoadLibrary('user32.dll') #加载user32 ...

  2. Linux 基础命令

    man      帮助命令   命令 --help   命令的简单帮助 help      命令的帮助(bash的内置命令) mkdir    创建目录  如makdir /data mkdir   ...

  3. 由表单验证说起,关于在C#中尝试链式编程的实践

    在web开发中必不可少的会遇到表单验证的问题,为避免数据在写入到数据库时出现异常,一般比较安全的做法是前端会先做一次验证,通过后把数据提交到后端再验证一次,因为仅仅靠前端验证是不安全的,有太多的htt ...

  4. Tomcat的设定

    tomcat 版本  apache-tomcat-7.0.68-windows-x64 1.解压文件到 eclipse文件夹中,这个放哪都可以,个人习惯而已 2.tomcat目录结构 图片为盗图- 3 ...

  5. iOS coredata 数据库的加密(待研究)

    https://github.com/project-imas/encrypted-core-data 使用起来很方便,底层还是使用了SQLCipher,有时间要研究一下! 数据库的密码不能用固定字符 ...

  6. #Java编程思想笔记(一)——static

    Java编程思想笔记(一)--static 看<Java编程思想>已经有一段时间了,一直以来都把笔记做在印象笔记上,今天开始写博客来记录. 第一篇笔记来写static关键字. static ...

  7. win8 app GridView点击子项布局变更

    要触发点击必须设置IsItemClickEnabled="True" 要变更布局代码如下: private void gridView_ItemClick_1(object sen ...

  8. linux 文件系统

    / 根目录 /bin 存放着启动时所需要的普通程序.很多程序在启动以后也很有用,它们放在这个目录下是因为它们经常要被其他程序调用 /boot 很多Linux系统把内核映像和其他一些和启动有关的文件都放 ...

  9. October 27th Week 44th Thursday 2016

    The art of being wise is the art of knowing what to overlook. 智慧之道在于懂得该忽略什么. Always do your best. Wh ...

  10. 51nod1102(数塔)

    题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1002 题意:中文题诶- 思路:简单dp 从底层往上递推,每个 ...