`

第3章 对象的共享

 
阅读更多
volatile 是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。
当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

使用:

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)

 

volatile变量的一种典型用法:检查某个专题标记以判断是否退出循环,eg:

volatile boolean asleep;

...

while(!asleep)

    countSomeSheep();

...

 
volatile变量通常用作某个操作完成,发生终端或者状态的标志。尽管volatile变量也可以用于表示其他的状态信息,但是volatile语义不足以确保递增操作(count++)的原子性,除非你能确保只又一个线程对变量执行写操作。

当且仅当满足以下所有条件时,才应该使用 volatile 变量:

1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

2. 该变量不会与其他状态变量一起纳入不变性条件中。

3. 在访问变量时不需要加锁

 
 
 
线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据就不需要同步。这种技术被称为线程封闭(Thread Confinement)。

它是实现线程安全的最简单方式之一。

 
线程封闭技术常见的例子就是JDBC的Connection对象,JDBC规范并不要求Connection对象必须是线程安全的。在典型的服务器应用程序中,线程从连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。由于大多数请求(例如Servlet请求或EJB调用多等)都是由单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池不会再将它分配给其他线程。因此,这种连接管理模式再处理请求时隐含的将Connection对象封闭在线程中。
Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类,但即便如此,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。

Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序来承担。Ad-hoc线程封闭是非常脆弱的,因此在程序中尽量少用,在可能的情况下,应该使用更强的线程封闭技术(例如,栈封闭 或 ThreadLocal类)。

 

栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用,不要与核心类库中的ThreadLocal混淆)比Ad-hoc线程封闭更易于维护,也更加健壮。
 
ThreadLocal类
维持线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量(Single)或全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<connection>(){
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };
public static Connection getConnection(){
    return connectionHolder.get();
}
 
ThreadLocal变量类似于全局变量,它能降低代码的可重用行,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
3.4 不变性
满足同步的另一种方法是使用不可变对象(Immutable Object)。不可变对象一定是线程安全的。
虽然Java语言规范和Java内存模型中都没有给出不可变性的正式定义,但不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。

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

1.对象创建以后其状态就不能修改

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

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

 
使用volatile类型来发布不可变对象
@Immutable
class OneValueCache{
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i,BigInteger[] factors){
        lastNumber = i;
        lastFactors = factors;
    }

    public BigInteger[] getFactors(BigInteger i){
        if(lastNumber==null || !lastNumbers.equal(i)) return null;
        else return Arrays.copyOf(lastFactors,lastFactors.length);
    }
}
 对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部存在一个不可变的对象中来消除。如果是一个可变的对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当现场获得了对该对象的引用后,就不必担心另一个线程会修改该对象的状态。如果要更新这些变量,那么可以创建一个新的容易对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet{
    private volatile OneValueCache cache = new OneValueCache(null,null);
    public void service(ServletRequest req,ServletResponse resp){
          BigInteger i = extractFromRequest(req);
          BigInteger[] factors = cache.getFactors(i);
          if(factors==null){
                 factors = factor(i);
                 cache = new OneValueCache(i,factors);
          }
          encodeIntoResponse(resp,factors);
     }
}
与cache 相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用确保可见性,使得在没有使用锁的情况下仍然是线程安全的。
3.5.2 不可变对象与初始化安全性
Java内存模型为不可变对象的共享提供了一种特殊的初始化安全保证。我们已经知道,即使某个对象的引用对其他线程是可见的,也并不意味着对象状态对于该对象的线程来说一定是可见的。为了确保对象状态能呈现出一只的视图,就必须使用同步。
另一方面,即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象,为了维持这种初始化安全性的保证,必须满足不可变性的所有需求:状态不可修改,所有域都是final类型,以及正确的构造过程。
这种保证还将延伸到被正确创建对象中所有final类型的域。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
3.5.3 安全发布的常用模式
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:
1.在静态初始化函数中初始化一个对象引用
2.将对象的引用保存到volatile类型的域或者AtomicReference对象中
3.将对象的引用保存到某个正确构造对象的final类型域中
4.将对象的引用保存到一个由锁保护的域中(包含 线程安全容器内部的同步)

线程安全库中的容器提供了一下的全发布保证:

1.通过将一个键或者值放入HashTable,synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)

2.通过将某个元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,synchronizedList或synchronizedSet中,可以将该元素安全地帆布到任何从这些容器中访问该元素的线程

3.通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

类库中的其他数据传递机制(例如Future和Exchanger)同样能实现安全发布。

 

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

public static Holder holder = new Holder(42);

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

 

3.5.4 事实不可变对象

所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的,并且如果对象状态不会再改变,那么就足以确保任何访问都是安全的。

如果对象从技术上来看是可变的,但其状态再发布后不会再改变,那么把这种对象称为"事实不可变对象(Effectively Immutable Object)"。

当满足以下条件时,对象才是不可变的:
1.对象创建以后其状态就不能修改
2.对象的所有域都是final类型
3.对象是正确创建的(在对象的创建期间,this引用没有逸出)

事实不可变对象不需要满足不可变性的严格定义。在这些对象发布后,程序只需将它们视为不可变对象即可。在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

eg: Date本身是可变的,但如果将它作为不可变对象来使用吗,那么在多个线程之间共享Date对象时,就可以省去对锁的使用。
public Map<String,Date> lastLogin = Collections.synchronizedMap(new HashMap<String,Date>());


3.5.5 可变对象
如果对象在构造后可以修改,那么安全发布只能确保"发布当时"状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。要安全的共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。

对象的发布需求取决于它的可变性:
1.不可变对象可以通过任意机制来发布
2.事实不可变对象必须通过安全方式来发布
3.可变对象必须通过安全的方式来发布,并且必须是线程安全的或者由某个锁保护起来

 

3.5.6 安全地共享对象

当获得对象的一个引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都由于没有理解共享对象的这些"既定规则"而导致的。当发布一个对象时,必须明确地说明对象的访问方式。

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
1.线程封闭。 线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
2.只读共享。 在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问。但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
3.线程安全共享。 线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
4.保护对象。 被保护的对象只能通过持有特定的锁来访问。保护对象也包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

分享到:
评论

相关推荐

    《java并发编程实战》读书笔记-第3章-对象的共享

    《java并发编程实战》读书笔记-第3章-对象的共享,脑图形式,使用xmind8制作 包括可见性、发布与逸出、线程封闭、不可变性、安全发布等内容

    Java并发编程实战2019.zip

    Java并发编程实战,第1章 简介,第2章 线程安全性 第3章 对象的共享 第4章 对象的组合 第5章 基础构建模块 第6章 任务执行 第7章 取消与关闭 第8章 线程池的使用 第9章 图形用户界面应用程序 第10章 避免...

    A面向对象分析与设计(UML.2.0版)

    第3章 继承 3.1 引言 3.2 设计类层次结构 3.3 给类层次结构添加实现代码 3.4 抽象类 3.5 重定义方法 3.6 实现栈类 3.7 多重继承 3.8 使用继承的规则 3.9 小结 3.10 课外阅读 3.11 复习题 3.12 复习题答案 第4...

    《java并发编程实战》读书笔记-第4章-对象的组合

    《java并发编程实战》读书笔记-第3章-对象的共享,脑图形式,使用xmind8制作 包括线程安全类设计、实例封闭、线程安全性委托、现有线程安全类中添加功能和文档化同步策略等内容

    完全掌握Project.2007【part 1】

    第3章 创建项目 第4章 创建和编辑任务 第5章 项目资源与工作分配 第6章 管理项目成本 第7章 项目进度跟踪 第8章 优化项目 第9章 美化项目信息 第10章 项目报表和打印 第11章 多重项目管理 第12章 项目文件的共享 第...

    C++面向对象程序设计第6版答案

    第六版答案~~好容易才从网上找来的 拿来给大家共享下!!

    写给大家看的面向对象编程书(第3版).[美]Matt Weisfeld(带详细书签).pdf

    第3章 高级面向对象概念 39 3.1 构造函数 39 3.1.1 何时调用构造函数 39 3.1.2 构造函数中有什么 40 3.1.3 默认构造函数 40 3.1.4 使用多个构造函数 41 3.1.5 构造函数的设计 44 3.2 错误处理 44 3.2.1 忽略...

    PYTHON 面向对象编程指南

    Python面向对象编程指南深入介绍Python语言的面向对象特性,全书分3个部分共18章。...第3部分讲述测试、调试、部署和维护,分别介绍了Logging和Warning模块、可测试性的设计、使用命令行、模块和包的设计、质量和文档。

    深度探索模C++对象模型PDF

    第3章 Data语意学(The Semantics of Data) 3.1 Data Member的绑定(The Binding of a Data Member) 3.2 Data Member的布局(Data Member Layout) 3.3 Data Member的存取 Static Data Members Nonstatic Data ...

    《C--面向对象程序设计》习题与上机解答-提交稿-陈维兴

    第3章 类和对象 3.1 习题参考解答 3.2 上机实验题参考解答 第4章 类和对象的进一步讨论 4.1 习题参考解答 4.2 上机实验题参考解答 第5章 继承与派生 5.1 习题参考解答 5.2 上机实验题参考解答 第6章 多态性与虚函数 ...

    深度探索C++对象模型 超清版

    第3章 Data语意学(The Semantics of Data) 3.1 Data Member的绑定(The Binding of a Data Member) 3.2 Data Member的布局(Data Member Layout) 3.3 Data Member的存取 Static Data Members Nonstatic Data ...

    JAVA并发编程实践_中文版(1-16章全)_1/4

    第3章 共享对象 3.1 可见性 3.2 发布和逸出 3.3 线程封闭 3.4 不可变性 3.5 安全发布 . 第4章 组合对象 4.1 设计线程安全的类 4.2 实例限制 4.3 委托线程安全 4.4 向已有的线程安全类添加功能 4.5 同步策略的文档化 ...

    数据库系统第4章.pptx

    非法使用数据库的情况 编写合法程序绕过数据库管理系统及其授权机制 直接或编写应用程序执行非授权操作 通过多次合法查询数据库从中推导出一些保密数据 数据库系统第4章全文共81页,当前为第3页。 数据库安全性控制...

    Python面向对象编程指南

    第3部分讲述测试、调试、部署和维护,分别介绍了Logging和Warning模块、可测试性的设计、使用命令行、模块和包的设计、质量和文档。 本书深入剖析Python,帮助读者全面掌握Python并构建出更好的应用程序,非常适合...

    Javascript 入门教程

    第3章 JavaScript事件处理.pdf 第4章 JavaScript基于对象编程.pdf 第5章 文档对象模型(DOM).pdf 第6章 String、Math、Array等数据对象.pdf 第7章 Window及相关顶级对象.pdf 第8章 Document对象.pdf 内容 绝对清楚 ...

    学习 Object-C入门教程

    第 3 章,类的声明和定义 第 4 章,继承 第 5 章,Class 类型,选择器 Selector 以及函数指针 第 6 章,NSObject 的奥秘 第 7 章,对象的初始化以及实例变量的作用域 第 8 章,类方法以及私有方法 第 9 章,内存管理...

    AppleScript 简明基础教程

    第三章 AppleScrip语言初步! 第一节 对象、属性和命令! 第二节 标识符和关键字! 第三节 数据类型! 第四节 强制数据类型转换! 第五节 运算符! 第六节 提取对象中的元素! 第七节 添加注释和括号! 第八节 代码缩写! 第...

    c++语言程序设计(第三版)

    第3章 函数 3.1 函数的定义与使用 3.2 内联函数 3.3 带默认形参值的函数 3.4 函数重载 3.5 函数模板 3.6 使用C++系统函数 3.7 小结 习题 第4章 类与对象 4.1 面向对象的思想 4.2 面向对象程序设计的基本特点 4.3 ...

    疯狂Android讲义(第2版)源代码 第6章~第9章

    第3章、Android事件处理,包括按键响应机制和消息传递机制 3.2、基于监听器的事件处理: 3.3、基于回调的事件的处理: 3.4、响应系统设置的事件: 3.5、Handler消息传递机制: 第4章、深入理解Activity 4.1、...

    《Java Web程序设计任务教程》-章节习题.docx 第1章网页开发基础 2.Java Web概述等

    第3章Servlet基础 1.在Servlet开发中,实现了多个Servlet之间数据共享的对象是【 】。 2.在Servlet容器启动每一个web应用时,就会创建一个唯一的ServletContext对象,该对象和web应用具有相同的【 】。 第4章请求和...

Global site tag (gtag.js) - Google Analytics