使用:
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)
volatile变量的一种典型用法:检查某个专题标记以判断是否退出循环,eg:
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();
...
当且仅当满足以下所有条件时,才应该使用 volatile 变量:
1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2. 该变量不会与其他状态变量一起纳入不变性条件中。
3. 在访问变量时不需要加锁
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据就不需要同步。这种技术被称为线程封闭(Thread Confinement)。
它是实现线程安全的最简单方式之一。
Ad-hoc线程封闭
Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序来承担。Ad-hoc线程封闭是非常脆弱的,因此在程序中尽量少用,在可能的情况下,应该使用更强的线程封闭技术(例如,栈封闭 或 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变量类似于全局变量,它能降低代码的可重用行,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
当满足以下条件时,对象才是不可变的:
1.对象创建以后其状态就不能修改
2.对象的所有域都是final类型
3.对象是正确创建的(在对象的创建期间,this引用没有逸出)
@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); } }
另一方面,即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象,为了维持这种初始化安全性的保证,必须满足不可变性的所有需求:状态不可修改,所有域都是final类型,以及正确的构造过程。
这种保证还将延伸到被正确创建对象中所有final类型的域。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:
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>());
如果对象在构造后可以修改,那么安全发布只能确保"发布当时"状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。要安全的共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。
对象的发布需求取决于它的可变性:
1.不可变对象可以通过任意机制来发布
2.事实不可变对象必须通过安全方式来发布
3.可变对象必须通过安全的方式来发布,并且必须是线程安全的或者由某个锁保护起来
当获得对象的一个引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都由于没有理解共享对象的这些"既定规则"而导致的。当发布一个对象时,必须明确地说明对象的访问方式。
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
1.线程封闭。 线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
2.只读共享。 在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问。但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
3.线程安全共享。 线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
4.保护对象。 被保护的对象只能通过持有特定的锁来访问。保护对象也包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
相关推荐
第六章的内容在“VC++面向对象与可视化程序设计(第三版)黄维通”中主要聚焦于面向对象编程(OOP)的概念以及如何利用Microsoft Visual C++(VC++)进行可视化程序设计。黄维通老师的这本书是许多学习者深入理解C++...
《java并发编程实战》读书笔记-第3章-对象的共享,脑图形式,使用xmind8制作 包括可见性、发布与逸出、线程封闭、不可变性、安全发布等内容
静态成员属于类本身而不是类的任何特定对象,这意味着所有对象共享相同的静态成员。静态数据成员和静态成员函数可以在不创建对象的情况下访问。 友元函数和友元类可以访问类的私有和保护成员,打破了封装原则,但...
【Java语言程序设计:第三章 对象和类】 在Java编程中,对象和类是核心概念,它们构成了面向对象编程的基础。本章主要探讨了Java中的类和对象以及如何使用它们进行程序设计。 首先,我们要了解什么是类和对象。类...
这个"03第三章面向对象的设计与实现.ppt共60页.pdf.zip"文件很可能是一个详细的讲座或教程,涵盖了面向对象的核心原理和技术。让我们深入探讨一下这个主题。 1. 面向对象的基本概念: - **对象**:面向对象编程...
这一章的主题"第十一章 持有对象"可能探讨的是如何在程序中有效地管理和控制对象的生命周期,特别是在Java、C++或者.NET等面向对象编程语言中的应用场景。持有对象涉及到的关键知识点包括引用计数、智能指针、垃圾...
第12章“现代信息集成技术”关注的是如何在不同系统间集成和共享信息,包括数据仓库、数据挖掘和企业信息门户等。数据仓库是为企业决策支持系统设计的大规模、集中式存储,而数据挖掘则利用统计和机器学习方法从大量...
在本章“Java语言程序设计ppt第八章对象和类”中,主要讲解的是Java中的核心概念——面向对象编程(OOP)。OOP是基于对象和类的编程范式,允许我们以更接近真实世界的方式组织代码,使得程序设计更加灵活且易于维护...
《第一行代码:Java源代码第3章 面向对象基础知识》是针对初学者的一份宝贵学习资源,旨在深入浅出地介绍Java编程中的核心概念——面向对象编程。本章内容涵盖了61页的详细讲解,通过实例解析,帮助读者理解和掌握...
本章主要讨论了三个核心的面向对象特性:静态变量和方法、final关键字以及abstract(抽象)类和方法。 首先,我们来探讨静态变量和方法。静态变量(`static`)在类中定义时,其生命周期跨越了类实例的创建和销毁,...
在第3章“类和对象”中,主要探讨了以下几个方面: 1. **类(Class)**:类是C++中定义用户自定义数据类型的关键,它将数据和操作数据的方法结合在一起,形成了一个抽象的数据结构。类是一种模板,用于创建对象,是...
静态数据成员是属于类的所有对象共享的数据成员,静态成员函数是属于类的所有对象共享的成员函数。 4.4 友元 友元有3种形式:友元函数、友元成员和友元类。友元函数是指在类外部定义的函数,可以访问类的所有成员...
在C++中,面向对象编程主要涉及三个核心概念:封装、继承和多态。本章将深入探讨类和对象的声明与实现,以及构造函数、析构函数、类的继承和派生等重要概念。 首先,类是面向对象编程的基础,它代表一类具有相同...
通过观看"07第3章 面向对象方法、软件架构、应用集成技术.mp4"的视频,并结合"课时07:第3章:面向对象方法、软件架构、应用集成技术(彩色共享版).pdf"的课件,学员可以更深入地学习这些关键概念,提高自己的专业...
本章主要回顾了C++中面向对象的一些关键知识点。 1. **类与对象**: 类是C++中定义数据结构和操作数据结构的方法的蓝图。它不是数据对象,而是一种类型定义。每个类的实例称为对象,每个对象都是类中定义的数据...
这一章主要涵盖了四个关键知识点:面向对象程序设计方法、面向对象数据模型、面向对象数据库建模以及对象-关系数据库。 1. 面向对象程序设计方法: 面向对象编程(Object-Oriented Programming, OOP)是一种模块化...
本章主要关注其中的几个关键对象:Response、Request、Application、Session、Cookie和Server,以及Cache对象。 1. **Response对象**: - **Response对象**允许开发者控制发送到客户端浏览器的HTTP响应。它包含一...
"C++第三章"通常会涉及面向对象编程的基础知识,这是C++的核心特性之一。以下是对这个章节可能涵盖的知识点的详细解释: 1. 面向对象编程基础:面向对象编程(Object-Oriented Programming, OOP)是C++的主要设计...