《java并发编程实战》读书笔记2--对象的共享,可见性,安全发布,线程封闭,不变性

这章的主要内容是:如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。

内存的可见性

确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

上面的程序中NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。一种更奇怪的现象是NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序”。多线程之指令重排序

失效数据

简而言之就是在缺乏同步的程序中可能会读取到过期的数据,也就是失效数据,就像上面的例子一样,当度线程查看ready变量时可能会的得到一个失效的值。

非原子的64位操作

虽然得到的可能是一个失效值,但至少这个值是由之前每个线程设置的,而不是一个随机值。这种安全性保证也被称为最低安全性。最低安全性适用于绝大多数变量,但是对于非volatile类型的64位数值变量(double和long)并非如此。java变量的读操作和写操作都是原子的,但是JVM允许将64位的读操作和写操作分解为2个32为的操作。这样当读取一个非volatile的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32为和另一个值的低32位。

加锁与可见性

Volatile变量

确保将变量的更新操作通知到其他线程,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。在读取volatile类型的变量时总会返回最新的写入值。下面的程序给出了volatile变量的一种典型用法:检查每个状态标记以判断是否退出循环。

volatile的语义不足以确保递增操作(count++)的原子性。当且仅当满足一下所有条件是,才应该使用volatile变量:

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

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

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

Java中Volatile关键字详解&&Java并发编程:volatile关键字解析

发布与逸出

“发布”一个对象是指:使对象能够在当前作用域之外的代码中使用。当某个不该发布的对象被发布时,这种情况就被称为逸出。发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中;

安全的对象构造过程

不要在构造过程中使this引用逸出。在构造过程中使this引用逸出的一个常见的错误是:在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显示创建还是隐士创建,this引用都会被新创建的线程共享。(简而言之在对象的构造函数中的别的对象能够拿到当前对象的this引用从而造成逸出)。

线程封闭

不共享数据,仅在单线程内访问数据,避免使用同步的方式。这种技术被大量使用喻Swing和JDBC的Connection对象。

Ad-hoc线程封闭

指维护线程封闭性的职责完全由程序实现来承担。很脆弱,尽量使用更强的线程封闭技术(如栈封闭或ThreadLocal类)

栈封闭

只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。他们位于执行线程的栈中,其他线程无法访问这个栈。

ThreadLocal类

这个类能使线程中的某个值与保存值的对象关联起来。ThisLocal提供了get和set等访问接口或方法,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal对象通常用于防止对可变的单实例或全局变量进行共享。通过将JDBC的连接保存到ThreadLocal对象中(因为JDBC的连接对象不一定是线程安全的),每个线程都会有属于自己的连接。

当某个频繁执行的操作需要一个临时对象,例如缓冲区,同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。当某个线程初次调用ThreadLocal.get方法时,救会调用initialValue来获取初始值。

不变性

满足同步需求的另一种方法是使用不可变对象,即对象创建以后状态不能修改,它的所有域都是final类型,并且对象是正确创建的(this引用没有逸出)。在不可变对象的内部仍可以使用可变对象来管理它们的状态,如

Final域

final类型的域是不能修改的,但如果final域所引用的对象是可变的(如上例),那么这些被引用的对象是可以修改的。

示例:使用volatile类型来发布不可变对象

继续前面因式分解的例子。因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以创建一个不可变的类来包含这些数据,如:

对数值及其因数分解结果进行缓存的不可变容器类

@Immutable
class OneValueCache {
  private final BigInteger lastNumber;
  private final BigInteger[] lastFactors;
  /**
   * 如果在构造函数中没有使用 Arrays.copyOf()方法,那么域内不可变对象 lastFactors却能被域外代码改变
   * 那么 OneValueCache 就不是不可变的。
   */
  public OneValueCache(BigInteger i,
             BigInteger[] factors) {
    lastNumber  = i;
    lastFactors = Arrays.copyOf(factors, factors.length);
  }
  public BigInteger[] getFactors(BigInteger i) {
    if (lastNumber == null || !lastNumber.equals(i))
      return null;
    else
      return Arrays.copyOf(lastFactors, lastFactors.length);
  }
}

安全发布

不正确的发布,正确的对象被破坏

不可变对象与初始化安全性

任何线程都可以子在不需要额外同步的情况下安全地访问不可变对象(状态不可变,域都是final类型,正确的构造过程)

安全发布的常用模式

可变对象必须通过安全的方式来发布。

事实不可变对象

本身可变但对象在发布后不会被修改。在没有额外的同步情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

可变对象

不仅需要安全发布,而且需要额外的同步和线程安全来保护

安全地共享对象

时间: 2024-11-04 14:02:30

《java并发编程实战》读书笔记2--对象的共享,可见性,安全发布,线程封闭,不变性的相关文章

JAVA并发编程实战 读书笔记(二)对象的共享

<java并发编程实战>读书摘要 birdhack 2015年1月2日 对象的共享 JAVA并发编程实战读书笔记 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键之synchronized只能用于实现原子性或者确定临界区.同步还有另一个重要的方面:内存可见性. 1.可见性 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 在没有同步的情况下,编译器.处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整.在缺乏足够同步的多线程程

读书笔记-----Java并发编程实战(二)对象的共享

1 public class NoVisibility{ 2 private static boolean ready; 3 private static int number; 4 private static class ReaderThread extends Thread{ 5 public void run(){ 6 while(!ready) 7 Thread.yield(); 8 System.out.println(number); 9 } 10 } 11 12 public s

java并发编程实战学习笔记之对象的组合与基础构建模块

第四章 对象的组合 4.1 构建安全的类 4.2 实例封闭 @ThreadSafe public class PersonSet {     @GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>();     public synchronized void addPerson(Person p) {         mySet.add(p);     }     pub

JAVA并发编程实战 读书笔记(一)线程安全性

线程安全性   1.什么是线程安全 在线程安全的定义中,最核心的概念是正确性.正确性的含义是,某个类的行为与规范完全一致.当对正确性有了一个比较清晰的定义后,就可以定义线程安全性:当多个线程访问某个类时,这个类始终能表现出正确的行为,那这个类就是线程安全的. 举例:无状态对象一定是线程安全的. 大多数Servlet都是无状态的,当Servlet在处理请求时需要保存一些信息时,线程安全才会成为一个问题. 2.原子性 举个例子:语句 ++i:虽然递增操作++i是一种紧凑的语法,使其看上去是一个操作,

《java并发编程实战》笔记(一)

最近在看<java并发编程实战>,希望自己有毅力把它读完. 线程本身有很多优势,比如可以发挥多处理器的强大能力.建模更加简单.简化异步事件的处理.使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题.看下面例子: public class UnsafeSequence { private int value; /*返回一个唯一的数值*/ public int getNext(){ return value++; } } UnsafeSequence的问题在于,如果执行时机不对,那么

java并发编程实战学习笔记之基础知识与对象的共享

第二章:线程安全性 2.1 什么是线程安全性 可以被多个线程调用,并且在线程之间不会出现错误的交互 方法内的局部变量不需要保护,因为它存储在栈中,是每个线程独有的 2.2 原子性 一个共享变量可以定义为原子变量:atomic 多个共享变量时,之间可能存在某种依赖关系,分别定义为原子变量会由于竞态条件,出现错误,比如先检查后执行. 竞态条件:某个计算的正确性取决于多个线程的交替执行时序时 这就需要复合操作,将多个变量的读写操作作为一个程序块,接下来采用 2.3 加锁机制 将一个复合程序块采用内置锁

Java并发编程实践读书笔记--第一部分 基础知识

目前关于线程安全性没有一个统一的定义,作者自己总结了一个定义,如下:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 在并发编程中,由于不恰当的执行时序而出现不确定的结果的情况被称为竞态条件(Race Condition).最常见的竞态条件就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能已经失效的观察来决定下一步的动作.比较简单的例子就是两

java并发编程实战学习笔记之第三部分:活跃性、性能与测试

第十章 避免活跃性危险 锁顺序死锁:定义锁的顺序,可以通过某种方法决定每个锁的顺序,比如hashcode或者序列号之类的 在锁的调用顺序不是很明显的情况下,在持有锁的情况下调用其他外部方法一定要注意,可以通过开放调用,避免发生死锁的危险,即使用同步代码块保护仅仅保护那些共享变量即可,但这种降低锁粒度的方法可能会使得原本大的代码块失去原子性,解决办法为:将服务的状态改为关闭之前一直持有锁,状态改变之后,其他线程也就能够看到关闭信息从而不会再次执行关闭操作... 死锁的诊断与避免:    通过try

Java并发编程实践读书笔记(5) 线程池的使用

Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到线程安全问题.如果你认为只会在单任务线程的Executor中运行的话,从设计上讲这就已经耦合了. 3,长时间的任务有可能会影响到其他任务的执行效率,可以让其他线程在等待的时候限定一下等待时间.不要无限制地等待下去. 确定线程池的大小 给出如下定义: 要使CPU达到期望的使用率,线程池的大小应设置为:

《Java并发编程实战》笔记-Happens-Before规则

Happens-Before规则 程序顺序规则.如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行. 监视器锁规则.在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行. volatile变量规则.对volatile变量的写入操作必须在对该变量的读操作之前执行. 线程启动规则.在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行. 线程结束规则.线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或