Java 并发编程(三)设计线程安全的类-实例封闭

到目前为止,我们已经介绍了关于线程安全与同步的一些基础知识。然而,我们并不希望对每一次内存访问都进行分析以确保是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组合为更大规模的组件或程序。之后,我们会讲一些设计线程安全类的一些基本概念,介绍一些组合模式。

一、设计线程安全的类

在设计线程安全类的过程中,需要包含以下三个基本要素:

1、找出构成对象状态的所有变量

2、找出约束状态变量的不变性条件

3、建立对象状态的并发访问管理策略

要分析对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。如果域中引用了其他的对象,那么该对象的状态将包含被引用对象的域。例如,LinkedList 的状态就包含了链表中所有节点对象的状态。

同步策略定义了如何在不违背对象不变条件或后验条件的情况下对状态的访问操作进行协同。同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护。

1、收集同步需求

要确保类的线程安全型,就要确保它的不变性条件不会在并发访问的情况下被破坏。对象与变量都有一个状态空间,即所有可能的取值。状态空间越小,就越容易判断线程的状态。final 类型的域使用的越多,就越能简化对象可能状态的分析过程。

在许多类中都定义了一些不可变条件,用于判断状态是有效的还是无效的。 比如 long 类型的变量 ,其状态空间为从 Long.MIN_VALUE 到 Long.MAX_VALUE ,或者一些表数量的变量值不能为负值。

同样,在操作中还会包含一些后验条件来判断状态迁移是否有效。 比如变量 counter 当前值为 17,下一个状态只能为18。当下一个状态需要依赖当前状态时,这个操作就必须使一个复合操作。并非所哟肚饿操作都会在状态转换上施加限制。例如,当更新一个保存当前温度的变量时,该变量之前的状态并不会影响计算结果。

由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能会使对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等要求,已获得更高的灵活性或性能。

2、依赖状态的操作

类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效地。在某些对象的方法中还包含了一些基于状态的先验条件(Precondition)。例如,不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态。如果在某个操作中包含有基于状态的先验条件,那么这个操作就成为依赖状态的操作。

3、状态的所有权

许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即拥有它封装的状态的所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占的控制权,最多是”共享控制权“。对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权。

容器类通常便显出一种所有权分离的形式,其中容器类拥有自身的状态,而客户代码则拥有容器中各个对象的状态。

二、实例封闭

如果某对象不是线程安全的,那么可以通过多种技术使其可以在多线程程序中安全的被使用。也可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。

封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(Instance Confinement),通常也简称为”封闭“。当一个对象被封闭到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更容易对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。

实力封闭是构建线程安全类的一个最简单方式,他还使得在锁策略的选择上拥有了更多的灵活性。

在 Java 平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类装化为线程安全的类。一些基本的容器类并非线程安全的,例如 ArrayList 和 HashMap,但类库提供了包装器工厂方法(如 Collections.sychronizedList 及其类似方法),使得这些非线程安全的类可以再多线程环境中安全的使用。这些工厂方法通过”装饰器“(Decorator)模式将容器类风状态一个同步的包装器对象中,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象上。只要包装器对象拥有对底层容易对象的唯一引用,那么它就是线程安全的。

1、Java 监视器模式

从线程封闭原则及其逻辑推论可以得出 Java 监视器模式。即把对象的所有可变状态都封装起来,并由对象自己的内置锁保护。

许多类中都使用了 Java 监视器模式,例如 Vector 和 HashTable。

Java 监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该所对象,都可以用来保护对象的状态。如 PrivateLock 给出了如何使用私有锁来保护状态。

public class PrivateLock {
	private final Object myLock = new Object();
	Date date;

	void someMethod(){
		synchronized(myLock){
			//访问或修改 date 的状态
		}
	}
}

使用私有锁对象而不是对象的内置锁,有许多优点。

私有锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码才可以通过公有方法来访问锁,以便(正确或者不正确的)参与到它的同步策略中。此外,要想验证某个公有访问的锁在程序中是否被正确的使用,则需要检查整个程序,而不是单个类,降低了验证的复杂度

时间: 2024-08-02 11:02:08

Java 并发编程(三)设计线程安全的类-实例封闭的相关文章

Java并发编程系列(一)-线程的基本使用

最近在学习java并发编程基础.一切从简,以能理解概念为主. 并发编程肯定绕不过线程.这是最基础的. 那么就从在java中,如何使用线程开始. 继承Thread类 继承Thread类,重写run方法,new出对象,调用start方法. 在新启的线程里运行的就是重写的run方法. 1 /** 2 * 集成Thread类 实现run() 3 */ 4 public class C1 extends Thread { 5 6 @Override 7 public void run() { 8 try

Java并发编程三个性质:原子性、可见性、有序性

并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确 线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的CPU调度顺序,线程个数.指令重排的影响,偶然触发 线程安全的定义 比如说一个类,不论通过怎样的调度执行顺序,并且调用处不用对其进行同步操作,其都能表现出正确的行为,则这个类就是线程安全的 并发编程三个概念 原子性: 一个操作或多个操作要么全部执行且执行过程不被中断,要么不执行 可见性: 多个线程修改同一

【java并发编程实战】-----线程基本概念

学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习Java并发编程,共同进步,互相指导. 在学习Java并发之前我们需要先理解一些基本的概念:共享.可变.线程安全性.线程同步.原子性.可见性.有序性. 共享和可变 要编写线程安全的代码,其核心在于对共享的和可变的状态进行访问. "共享"就意味着变量可以被多个线程同时访问.我们知道系统中的资

Java并发编程(01):线程的创建方式,状态周期管理

本文源码:GitHub·点这里 || GitEE·点这里 一.并发编程简介 1.基础概念 程序 与计算机系统操作有关的计算机程序.规程.规则,以及可能有的文件.文档及数据. 进程 进程是计算机中的程序,关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.在早期面向进程设计的计算机结构中,进程是程序的基本执行实体:在面向线程设计的计算机结构中,进程是线程的容器.程序是指令.数据及其组织形式的描述,进程是程序的实体. 线程 线程是操作系统能够进行运算调度的最小单

Java并发编程(02):线程核心机制,基础概念扩展

本文源码:GitHub·点这里 || GitEE·点这里 一.线程基本机制 1.概念描述 并发编程的特点是:可以将程序划分为多个分离且独立运行的任务,通过线程来驱动这些独立的任务执行,从而提升整体的效率.下面提供一个基础的演示案例. 2.应用案例 场景:假设有一个容器集合,需要拿出容器中的每个元素,进行加工处理,一般情况下直接遍历就好,如果数据偏大,可以根据线程数量对集合切割,每个线程处理一部分数据,这样处理时间就会减少很多. public class ExtendThread01 { publ

Java并发编程学习:线程安全与锁优化

本文参考<深入理解java虚拟机第二版> 一.什么是线程安全? 这里我借<Java Concurrency In Practice>里面的话:当多个线程访问一个对象,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的. 我的理解:多线程访问一个对象,任何情况下,都能保持正确行为,就是对象就是安全的. 我们可以将Java语言中各种操作共享的数据分为以下5类:不可变.

Java并发编程-如何终止线程

我们知道使用stop().suspend()等方法在终止与恢复线程有弊端,会造成线程不安全,那么问题来了,应该如何正确终止与恢复线程呢?这里可以使用两种方法: 1.使用interrupt()中断方法. 2.使用volatile boolean变量进行控制. 在使用interrupt方法之前,有必要介绍一下中断以及与interrupt相关的方法.中断可以理解为线程的一个标志位属性,表示一个运行中的线程是否被其他线程进行了中断操作.这里提到了其他线程,所以可以认为中断是线程之间进行通信的一种方式,简

JAVA - 并发编程 - 执行器和线程池

思考? 1 为什么要使用执行器和线程池? 2 执行器和线程是什么?怎么使用 执行器 线程执行器分离了任务的创建和执行,提高了线程的性能 线程池 避免了频繁地创建和销毁线程,达到线程对象的重用,可以根据项目灵活地控制并发的数量 ExecutorService (java.util.concurrent) 1 Executors.newCachedThreadPool() 可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程 2 Executors.newFixedT

java并发编程实战笔记-线程安全性

什么是线程安全性 线程安全性定义中最核心的概念就是:**正确性**.我们将单线程的正确性近似 定义为"所见即所知",当多个线程访问这个类的时候,始终能表现出正确的行为, 那么这个类就是线程安全类. 当多个线程访问某个类时,不管运行时环境采用什么调度方式或者这些线程将如何 交替运行,并且调用代码时,不需要额外的同步,就可以产生正确的结果.这个类 就是线程安全类. 在线程安全类上执行任何串行或者并行的操作都不会使对象处于无效状态. 可重入代码:就是这段代码,和其他代码不存在共享状态,只包含