对象的公布与逸出

“公布(Publish)“一个对象是指使对象可以在当前作用域之外的代码中使用。可以通过 公有静态变量非私有方法构造方法内隐含引用 三种方式。

假设对象构造完毕之前就公布该对象,就会破坏线程安全性。当某个不应该公布的对象被公布时。这样的情况就被称为逸出(Escape)。

以下我们首先来看看一个对象是怎样逸出的。

公布对象最简单的方法便是将对象的引用保存到一个共同拥有的静态变量中,以便不论什么类和线程都能看见对象,如以下代码。

public static Set<String> mySet;

	public void initialize() {
mySet = new HashSet<String>();
}

当公布某个对象时,可能会间接地公布其它对象。假设将一个 String 对象加入到集合 mySet 中,那么相同会公布这个对象,由于不论什么代码都能够遍历这个集合。并获得对这个 String 对象的引用。

相同,假设从非私有方法中返回一个引用,那么相同会公布返回的对象。

如以下代码 UnsafeStates 公布了本应为私有的状态数组。

class UnsafeState {
private String[] states = new String[] { "AK", "AL" }; public String[] getStates() {
return states;
}
}

假设依照上诉方法来公布 states。就会出问题,由于不论什么调用者都能改动这个数组的内容。数组 states 已经溢出了它所在的作用域了,由于这个本应是私有的变量已经被公布了。当私有变量被公布出去之后,这个类就无法知道”外部方法“会进行何种操作。

        不管其它的线程会对义公布的引用运行何种操作,事实上都不重要。由于误用该引用的风险始终存在。当hadoop某个对象逸出后,你必须如果有某个类或者线程可能会误用该对象。

这正是须要使用封装的的最基本的原因:封装能使得对正确性分析变得可能,并使降低无意中破坏设计约束条件的行为。

最后一种公布对象或其内部状态的机制就是公布一个内部的类实例,如以下类 ThisEscape 所看到的。

当 ThisEscape 公布 EventListener 时。也隐含的公布了 ThisEscape 实例本身,由于在这个内部类的实例中包括了对 ThisEscape 实例的隐含对象。

public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}

安全的构造过程

ThisEscape 中给出了逸出的一个特殊演示样例,即 this 引用在构造函数中逸出。内部 EventListener 实例公布时,在外部封装的 ThisEscape 实例也逸出了。

当且仅当对象的构造函数返回时。对象才处于可预測的和一致的状态。因此,当从对象的构造函数中公布对象时,仅仅是公布了一个尚未构造完毕的对象。即使公布对象的语句位于构造函数的最后一行也是如此。假设 this 引用在构造过程中逸出,那么这样的对象就被觉得是不对构造

在构造过程中使 this 引用逸出的一个常见错误是。在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,不管是显示创建(通过将它传给构造函数)还是隐式创建(因为 Thread 或 Runnable 是该对象的一个内部类), this 引用都会被新创建的线程共享。

在对象尚未被创建完毕之前,新的线程就能够看见它。

在构造函数中创建线程并没有错误。但最好不要马上启动它,而是通过一个 start 或 initialize 方法来启动。

在构造函数中调用一个可改写的实例方法时,相同会导致 this
引用在构造过程中逸出。

假设想在构造函数中注冊一个事件监听器或启动线程,那么能够使用一个私有的构造函数和一个公共的工厂方法(Factory Method)。从而避免不对的构造过程,如以下的 SafeListener 。

public class SafeListener{
private final EventListener listener;
private SafeListener(){
listener = new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}

线程封闭

当訪问共享的可变数据时,通常须要使用同步。一种避免使用同步的方式就是不共享数据。假设仅在单线程内訪问数据,就不须要同步。

这样的技术被称为线程封闭(Thread Confinement),它是实现线程安全型的最简单方式之中的一个。

当某个对象封闭在一个线程中时。这样的使用方法将自己主动实现线程安全性。即使被封闭的对象本身不是线程安全的。

线程封闭的一种常见的应用是 JDBC 的 Connection 对象。JDBC 规范并不要求 Connection 对象必须是线程安全的。在典型的server应用程序中,线程从连接池中获得一个 Connection 对象,而且用该对象来处理请求,使用完之后再将对象返还给连接池。因为大多数请求(比如 Servlet 请求或 EJB 调用等)都是由单个线程採用同步的方式来处理。而且在 Connection 对象返回之前。连接池都不会将它分配给其他线程。因此,这样的连接管理模式在处理请求时隐含的将 Connection
对象封闭在线程中。

Java 语言及其核心库提供了一些机制来帮助维持线程封闭性。比如局部变量和 ThreadLocal 类,即便如此,程序猿仍然须要确保封闭在线程中的对象不会从线程中逸出。

Ad-hoc 线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责全然由程序实现来承担。

Ad-hoc线程封闭是很脆弱的,由于没有不论什么一种语言特性,比如可见性修饰符或局部变量,能将对象封闭到目标线程上。其实。对线程封闭对象(比如。GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。

当决定使用线程封闭技术时,一般是由于要将某个特定的子系统实现为一个单线程子系统。在某些情况下。单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。

在volatile变量上存在一种特殊的线程封闭。

仅仅要你能确保仅仅有单个线程对共享的volatile变量运行写入操作,那么就能够安全地在这些共享的volatile变量上运行“读取-改动-写入”的操作。在这样的情况下,相当于将改动操作封闭在单个线程中以防止发生竞态条件。而且volatile变量的可见性保证还确保了其它线程能看到最新的值。

因为Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术(比如,栈封闭或ThreadLocal类)。

栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中。仅仅能通过局部变量才干訪问对象。正如封装能使代码更easy维持不变性条件那样。同步变量也能使对象更易于封闭在线程中。

对于基本类型的局部变量。比如以下 loadTheArk 方法的 numPairs 。不管怎样都不会破坏栈封闭性。因为不论什么方法都不发获得对基本类型的引用。因此 Java 语言的这样的语义确保了基本类型的局部变量始终封闭在线程内。

public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null; // animals被封闭在方法中,不要使它们逸出! animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a))
candidate = a;
else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}

在维持对象引用的栈封闭性时,程序猿须要多做一些工作以确保被引用的对象不会逸出。在loadTheArk中实例化一个TreeSet对象,并将指向该对象的一个引用保存到animals中。

此时,仅仅有一个引用指向集合animals。这个引用被封闭在局部变量中,因此也被封闭在运行线程中。

然而,假设公布了对集合animals(或者该对象中的不论什么内部数据)的引用,那么封闭性将被破坏,并导致对象animals的逸出。

假设在线程内部(Within-Thread)上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。然而,要小心的是,仅仅有编写代码的开发者才知道哪些对象须要被封闭到运行线程中。以及被封闭的对象是否是线程安全的。假设没有明白地说明这些需求。那么兴许的维护人员非常easy错误地使对象逸出。

ThreadLocal 类

维持线程封闭性的一种更规范方法是使用ThreadLocal。这个类能使线程中的某个值与保存值的对象关联起来。

ThreadLocal提供了get与set等訪问接口或方法,这些方法为每一个使用该变量的线程都存有一份独立的副本。因此get总是返回由当前运行线程在调用set时设置的最新值。

ThreadLocal对象通经常使用于防止对可变的单实例变量(Singleton)或全局变量进行共享。

比如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象。从而避免在调用每一个方法时都要传递一个Connection对象。

因为JDBC的连接对象不一定是线程安全的。因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每一个线程都会拥有属于自己的连接。

当某个频繁运行的操作须要一个暂时对象,比如一个缓冲区,而同一时候又希望避免在每次运行时都又一次分配该暂时对象。就能够使用这项技术。比如,在Java 5.0之前,Integer.toString()方法使用ThreadLocal对象来保存一个12字节大小的缓冲区,用于对结果进行格式化。而不是使用共享的静态缓冲区(这须要使用锁机制)或者在每次调用时都分配一个新的缓冲区。

当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。

从概念上看,你能够将ThreadLocal<T>视为包括了Map< Thread,T>对象,当中保存了特定于该线程的值。但ThreadLocal的实现并不是如此。这些特定于线程的值保存在Thread对象中。当线程终止后。这些值会作为垃圾回收。

假设你须要将一个单线程应用程序移植到多线程环境中。通过将共享的全局变量转换为ThreadLocal对象(假设全局变量的语义同意)。能够维持线程安全性。然而,假设将应用程序范围内的缓存转换为线程局部的缓存,就不会有太大作用。

在实现应用程序框架时大量使用了ThreadLocal。比如。在EJB调用期间,J2EE容器须要将一个事务上下文(Transaction Context)与某个运行中的线程关联起来。通过将事务上下文保存在静态的ThreadLocal对象中,能够非常easy地实现这个功能:当框架代码须要推断当前运行的是哪一个事务时,仅仅需从这个ThreadLocal对象中读取事务上下文。这样的机制非常方便,由于它避免了在调用每一个方法时都要传递运行上下文信息。然而这也将使用该机制的代码与框架耦合在一起。

开发者常常滥用ThreadLocal,比如将全部全局变量都作为ThreadLocal对象。或者作为一种“隐藏”方法參数的手段。ThreadLocal变量类似于全局变量,它能减少代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

Java 并发编程(二)对象的公布逸出和线程封闭的更多相关文章

  1. Java并发编程二三事

    Java并发编程二三事 转自我的Github 近日重新翻了一下<Java Concurrency in Practice>故以此文记之. 我觉得Java的并发可以从下面三个点去理解: * ...

  2. 【Java并发编程二】同步容器和并发容器

    一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并 ...

  3. Java 并发编程(二):如何保证共享变量的原子性?

    线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...

  4. Java并发编程 (二) 并发基础

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.CPU多级缓存-缓存一致性 1.CPU多级缓存 ​ 上图展示的是CPU高级缓存的配置,数据的读取和存 ...

  5. 【Java并发编程二】Java并发包

    1.Java容器 1.1.同步容器 Vector ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问.数组的缺点是每个元素之间不能有间隔,当数组大小不满足时 ...

  6. java并发编程JUC第十一篇:如何在线程之间进行对等数据交换

    java.util.concurrent.Exchanger可以用来进行数据交换,或者被称为"数据交换器".两个线程可以使用Exchanger交换数据,下图用来说明Exchange ...

  7. 【Java并发编程】:使用wait/notify/notifyAll实现线程间通信

    在java中,可以通过配合调用Object对象的wait()方法和notify()方法或notifyAll()方法来实现线程间的通信.在线程中调用wait()方法,将阻塞等待其他线程的通知(其他线程调 ...

  8. Java并发编程(二):volatile关键字

    volatile是Java虚拟机提供的轻量级的同步机制.volatile关键字有如下两个作用,一句话概括就是内存可见性和禁止重排序. 1)保证被volatile修饰的共享变量对所有线程总是可见的,也就 ...

  9. Java 并发编程中的 CountDownLatch 锁用于多个线程同时开始运行或主线程等待子线程结束

    Java 5 开始引入的 Concurrent 并发软件包里面的 CountDownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是 ...

随机推荐

  1. python爬虫(房天下)

    房天下 import requests res = requests.get('http://esf.sz.fang.com/') #res.text from bs4 import Beautifu ...

  2. 使用CAShapeLayer的path属性与UIBezierPath画出扫描框

    1.CAShapeLayer CAShapeLayer具有path属性,(是CGPath对象),可以使用这个属性与UIBezierPath画出想要的图形.该子类根据其fill color和stroke ...

  3. SDK_列表控件的使用

    列表控件的使用 列表控件是通用控件,响应WM_NOTIFY 消息 主要包含了 4 种风格,我们学的是 report 风格 如何设置列表的扩展风格 LVS_EX_GRIDLINES: 列表拥有表格线 L ...

  4. 梦想CAD控件 2018.10.15更新

    下载地址: http://www.mxdraw.com/ndetail_10105.html 1. 完善com接口的ToCurves函数,转换CAD文字,多行文字到曲线 2. 修改DrawImage接 ...

  5. CF 429B B.Working out (四角dp)

    题意: 两个人一个从左上角一个从左下角分别开始走分别走向右下角和右上角,(矩阵每个格子有数)问到达终点后可以得到的最大数是多少,并且条件是他们两个相遇的时候那个点的数不能算 思路: 首先这道题如果暴力 ...

  6. WebStorm 格式化代码快捷键

    原文链接:https://kaifazhinan.com/webstorm-formatting-code-shortcuts/ 现在平时都是使用 VS Code 作为日常开发工具,偶尔会打开 Web ...

  7. Mysql Group by 分组取最小的实现方法

    表结构如下图:

  8. python socket实现文件传输(防粘包)

    1.文件传输的要点: 采用iterator(迭代器对象)迭代读取,提高读取以及存取效率: 通过for line in file_handles逐行conn.send(): 2.socket粘包问题: ...

  9. BC in fluent

    Boundary conditions in Fluent Table of Contents 1. Boundary Conditions (BC) 1.1. Turbulence Paramete ...

  10. 在centos7中使用yum安装mysql数据库并使用navicat连接

    1.安装 1.查看yum列表,发现没有mysql [root@server-mysql src]# yum list mysql 已加载插件:fastestmirror Repodata is ove ...