1. 定义

  • 发布对象(Publish): 使一个对象能够被当前范围之外的代码所使用
  • 对象逸出(Escape): 一种错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见

1.1 发布对象

public class UnsafePublish {
private String[] states = {"a","b","c"};
public String[] getStates(){
return states;
} /**
* 通过new UnsafePublish()发布了一个UnsafePublish类的实例
* 通过实例的public方法得到了私有域states数组的引用
* 可以在其他任何线程里修改这个数组里的值
* 这样在其他线程中想使用states数组时,它的值是不完全确定的
* 因此这样发布的对象是线程不安全的,因为无法保证是否有其他线程对数组里的值进行了修改
* @param args
*/
public static void main(String[] args) {
UnsafePublish unsafePublish = new UnsafePublish();
for (String i : unsafePublish.getStates()) {
System.out.print(i+" ");
}
unsafePublish.getStates()[0] = "d";
System.out.println();
for (String i : unsafePublish.getStates()) {
System.out.print(i+" ");
}
}
}

1.2 对象逸出

@NotThreadSafe
public class Escape {
private int thisCanBeEscape = 0;
public Escape(){
new InnerClass();
} /**
* 内部类的实例里面包含了对封装内容thisCanBeEscape的隐含引用
* 这样在对象没有被正确构造之前,他就会被发布,有可能有不安全的因素在
* 一个导致this引用在构造期间逸出的错误 是在构造的函数过程中启动了一个线程
* 无论是隐式的启动还是显示地启动都会造成this引用的逸出,新线程总是会在对象构造完毕
* 之前就已经看到this引用 所以要再构造函数中使用线程,就不要启动它而应该专有的start或初始化的方法来统一启动线程,
* 可以采用工厂方法和私有构造函数来完成对象创建和监听器的注册等
*
*
* 在对象未完成构造之前 不可以将其发布
*/
private class InnerClass{
public InnerClass(){
System.out.println(Escape.this.thisCanBeEscape);
}
} public static void main(String[] args) {
new Escape();
}
}

2. 问题(引用+状态,构造函数+正确发布)

不正确的发布可变对象导致的两种错误:

1、发布线程之外的所有线程都可以看到被发布对象的过期的值【引用过期】
2、线程看到的被发布对象的引用是最新的,然而被发布对象的状态却是过期的【状态过期】

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

  (1)引用本身要被其他线程看到;

  (2)对象的状态要被其他线程看到。

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

  

public class Client {
public Holder holder; public void initialize(){
holder = new Holder(42);//这个代码不是原子的
}
} public class Holder {
int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if(n != n)
throw new AssertionError("This statement is false.");
}
} /**
在Client类中,Holder对象被发布了,但是这是一个不正确的发布。由于可见性问题,其他线程看到的Holder对象将处于不一致的状态,即使在该对象的构成构函数中已经正确的该构建了不变性条件,这种不正确的发布导致其他线程看到尚未创建完成的对象。主要是Holder对象的创建不是原子性的,可能还未构造完成,其他线程就开始调用Holder对象。 由于没有使用同步的方法来却确保Holder对象(包含引用和对象状态都没有)对其他线程可见,因此将Holder成为未正确发布。问题不在于Holder本身,而是其没有正确的发布。上面没有正确发布的可能导致的问题: 别的线程对于holder字段,可能会看到过时的值,这样就会 导致空引用,或者是过时的值(即使holder已经被设置了)(引用本身没有被别的线程看到)
更可怕的是,对于已经更新holder,及时能够看到引用的更新,但是对于对象的状态,看到的却可能是旧值,对于上面的代码,可能会抛出AssertionError异常
主要是holder = new Holder(42);这个代码不是原子性的,可能在构造未完成时,其他线程就会调用holder对象引用,从而导致不可预测的结果。
*/

3. 安全对象的构造过程

不要在构造函数内显式或者隐式的的公布this引用。

(1)在对象构造期间,不要公布this引用

  如果要在构造函数中创建内部类,那么就不能在构造函数中把他发布了,应该在构造函数外发布,即等构造函数执行完毕,初始化工作已全部完成,再发布内部类。

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 }
}
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]);
}
}

  

(2)不要隐式地暴露“this”引用

public class EventListener2 {
public EventListener2(EventSource eventSource) { eventSource.registerListener(
new EventListener() {
public onEvent(Event e) {
eventReceived(e);
}
});
}
public eventReceived(Event e) { }
}
同样也是子类化问题

(3)不要从构造函数内启动线程
     a)在构造函数中启动线程时,构造函数还未执行完毕,不能保证此对象已经完全构造
     b)如果在启动的线程中访问此对象,不能保证访问到的是完全构造好的对象

3. 安全发布常用模式

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

  (1)在静态初始化函数中初始化一个对象引用

  (2)将一个对象引用保存在volatile类型的域或者是AtomicReference对象中

  (3)将对象的引用保存到某个正确构造对象的final类型的域中。

  (4)将对象的引用保存到一个由锁保护的域

/**

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

  当满足以下条件时,对象才是不可变的:

    1)对象创建以后其状态就不能改变;

    2)对象的所有域都是final类型;

    3)对象是正确创造的(在对象创建期间,this引用没有溢出)。

(1)Java中存在三种对象
a)不变对象:对象状态创建后不能再修改,对象的所有域为final,对象是正确构造的
b)基本不变对象:不满足不变对象的约束,但是初始化后不再变化
c)可变对象:不满足上述不变对象和基本不变对象的约束 (2)安全发布技术
a)即确保对象引用和状态对其他线程正确可见
b)方式
静态初始化器初始化对象引用
将引用存储到volatile域
将引用存储到正确创建对象的final域
将引用存储到由锁正确保护的域 (3)三种对象安全发布方式
a)不变对象:任何形式机制发布
b)基本不变对象:保证安全发布即可
c)可变对象:不仅要保证安全发布,而且要确保对象状态的正确改变(即用锁或其他方式,保证对象状态的正确改变)

  通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态初始化器: public static Holder = new Holder(42);

  静态初始化器由JVM在类的初始化阶段执行,由于JVM内部存在同步机制,所以这种方式初始化对象都可以被安全的发布。

  对于可变对象,安全的发布之时确保在发布当时状态的可见性,而在随后的每次对象的访问时,同样需要使用同步来确保修改操作的可见性。(状态可见性+同步)

**/

4. 容器安全发布保证

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

  (1)通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全将它发布给任何从这些容器中访问它的线程。

  (2)通过将某个元素放到Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchroizedList,可以将该元素安全的发布到任何从这些容器中访问该元素的线程。

  (3)通过将元素放到BlockingQueue或者是ConcrrentLinkedQueue中,可以将该元素安全的发布到任何从这些访问队列中访问该元素的线程。

5. 网址

  1. 安全发布对象(一)

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

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

  4. 第三章 对象的共享(三)

Java 并发系列之十三:安全发布的更多相关文章

  1. Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析

    学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, Cyc ...

  2. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

  3. Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...

  4. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  5. Java 设计模式系列(十三)模板方法

    Java 设计模式系列(十三)模板方法 模板方法模式是类的行为模式.准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑.不同的子类可以以不同的 ...

  6. Java 并发系列之二:java 并发机制的底层实现原理

    1. 处理器实现原子操作 2. volatile /** 补充: 主要作用:内存可见性,是变量在多个线程中可见,修饰变量,解决一写多读的问题. 轻量级的synchronized,不会造成阻塞.性能比s ...

  7. Java 并发系列之一

    Java 并发系列之一 简单的总结了一些 Java 常用的集合之后,发现许多集合都针对多线程提供了支持,比如 ConcurrentHashMap 使用分段锁来提高多线程环境下的性能表现与安全表现.所以 ...

  8. java并发系列 - 第28天:实战篇,微服务日志的伤痛,一并帮你解决掉

    这是java高并发系列第28篇文章. 环境:jdk1.8. 本文内容 日志有什么用? 日志存在的痛点? 构建日志系统 日志有什么用? 系统出现故障的时候,可以通过日志信息快速定位问题,修复bug,恢复 ...

  9. java并发系列 - 第29天:高并发中常见的限流方式

    这是java高并发系列第29篇. 环境:jdk1.8. 本文内容 介绍常见的限流算法 通过控制最大并发数来进行限流 通过漏桶算法来进行限流 通过令牌桶算法来进行限流 限流工具类RateLimiter ...

随机推荐

  1. 使用ArcPy拓扑检查的基本步骤

    拓扑检查是GIS的特性,在ArcGIS可使用多种方法进行检查,包括: 1.在数据集上右键按向导建立: 2.使用拓扑工具箱的一系列工具分步建立: 3.创建模型工具,制作专门的拓扑工具: 4.利用ArcP ...

  2. DirectShow 简介

    一.DirectShow 简介 DirectShow(简称 DShow) 是一个 Windows 平台上的流媒体框架,提供了高质量的多媒体流采集和回放功能.它支持多种多样的媒体文件格式,包括 ASF. ...

  3. Feign原理 (图解)

    疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 疯狂创客圈 正在进行分布式和高并发基础原理的研习,进行已经发布一些基础性的文章: 一.版本1 :springcloud ...

  4. 05Shell循环语句

    循环语句 for 语法结构 for 变量名 [ in 取值列表 ] do 循环体 done 注意 当for对文件内容进行逐行处理时,会忽略空行 示例 例1 ping 主机的脚本(初始版):缺点执行过程 ...

  5. virtualbox FAIL(0x80004005) VirtualBox VT-x is not available (VERR_VMX_NO_VMX)

    virtualbox启动虚拟机报错: FAIL(0x80004005) VirtualBox VT-x is not available (VERR_VMX_NO_VMX),无法创建新任务 这是win ...

  6. 转载:ubuntu下编译安装nginx及注册服务

    原文地址:https://www.cnblogs.com/EasonJim/p/7806879.html 安装gcc g++的依赖库 sudo apt-get install build-essent ...

  7. Java入门系列之字符串特性(二)

    前言 上一节我们讲解到字符串本质上就是字符数组,同时详细讲解了字符串判断相等需要注意的地方,本节我们来深入探讨字符串特性,下面我们一起来看看. 不可变性 我们依然借助初始化字符串的方式来探讨字符串的不 ...

  8. Windows 10 powershell 中文乱码解决方案

    Windows 10 powershell 中文乱码解决方案 Intro 我装的系统是英文版的 win 10 操作系统,最近使用命令行测试接口,发现中文显示一直异常, 使用网上的各种解决方案都没有效果 ...

  9. [b0028] python 归纳 (十三)_队列Queue在多线程中使用

    # -*- coding: UTF-8 -*- """ 多线程同时读队列 总结: 1. 会阻塞 if self._jobq.qsize() > 0 进入逻辑,此时被 ...

  10. Troubleshooting ORA-30036 - Unable To Extend Undo Tablespace (Doc ID 460481.1)

    Troubleshooting ORA-30036 - Unable To Extend Undo Tablespace (Doc ID 460481.1) APPLIES TO: Oracle Da ...