读书笔记-----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 static void main(String[] args){
13            new ReaderThread().start();
14            number = 42;
15            ready = true;
16     }
17 }

在两个线程之间共享访问变量是不安全的,执行结果顺序没法预料。

我们常用的get和set方法有时是非线程安全的

1 @NotThreadSafe
2 public class MutableInteger{
3       private int value;
4       public int get(){return value;}
5       public void set(int value){this.value = value;}
6 }      

如果某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到

将其变成线程安全的类

@ThreadSafe
public class SynchronizedInteger{
       @GuardBy("this") private int value;
       public synchronized int get(){return value;}
       public synchronized void set(int value){this.value = value;}
}

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

Volatile变量

用来确保将变量的更新操作通知到其他线程。volatile变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

Volatile变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile变量的最新值。Volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile变量而不是锁。当使用 volatile变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile变量还可以提供优于锁的性能优势。

但如果在代码中依赖Volatile变量来控制状态的可见性,通常比使用锁的代码更加脆弱,也更难以理解。

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

典型用法:检查某个状态标记以判断是否退出循环。

volatile boolean asleep;
...
   while(!asleep)
        countSomeSheep();

volatile不足以确保递增操作的原子性。

加锁机制既可以确保可见性又可以确保原子性。而volatile变量只能确保可见性。

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

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
  • 该变量不会与其他状态的变量一起纳入不变性条件中
  • 访问变量时不需要加锁

发布和逸出 

所谓发布对象是指使一个对象能够被当前范围之外的代码所使用。所谓逸出是指一种错误的发布情况,当一个对象还没有构造完成时,就使它被其他线程所见,这种情况就称为对象逸出。在我们的日常开发中,经常要发布一些对象,比如通过类的非私有方法返回对象的引用,或者通过公有静态变量发布对象。如下面这些代码所示:

 1 class UnSafeStates{
 2     private String[] states={"AK","AL"};
 3     public String[] getStates(){ return states; }
 4     public static void main(String[] args) {
 5           UnSafeStates safe = new UnSafeStates();
 6            System.out.println(Arrays.toString(safe.getStates()));
 7            safe.getStates()[1] = "c";
 8            System.out.println(Arrays.toString(safe.getStates()));
 9    }
10 }

输出结果[AL,KL]

[AL,c]

以上代码通过public访问级别发布了类的域,在类的外部任何线程都可以访问这些域,这样发布对象是不安全的,因为我们无法假设,其他线程不会修改这些域,从而造成类状态的错误。还有一种逸出是在构造对象时发生的,它会使类的this引用发生逸出,从而使线程看到一个构造不完整的对象,如下面代码所示:

public class Escape{
    private int thisCanBeEscape = 0;
    public Escape(){
        new InnerClass();
    }
    private  class InnerClass {
        public InnerClass() {
            //这里可以在Escape对象完成构造前提前引用到Escape的private变量
            System.out.println(Escape.this.thisCanBeEscape);
       }
    }
    public static void main(String[] args) {
        new Escape();
    }
}

上面的内部类的实例包含了对封装实例隐含的引用,这样在对象没有被正确构造之前,就会被发布,有可能会有不安全因素。

一个导致this引用在构造期间逸出的错误,是在构造函数中启动一个线程,无论是隐式启动线程,还是显式启动线程,都会造成this引用逸出,新线程总会在所属对象构造完毕前看到它。所以如果要在构造函数中创建线程,那么不要启动它,而应该采用一个专有的start或initialize方法来统一启动线程。我们可以采用工厂方法和私有构造函数来完成对象创建和监听器的注册,这样就可以避免不正确的创建。记住,我们的目的是,在对象未完成构造之前,不可以将其发布。

正确的方式

 1 public class SafeListener{
 2     private final EventListener listener;
 3
 4      private SafeListener(){
 5               listener = new EventListener(){
 6                    public void onEvent(Event e) {
 7                          doSomething(e);
 8                   }
 9              }
10       }
11
12       public static SafeListener newInstance(EventSource source){
13               SafeListener safe  = new SafeListener();
14               source.registerListener(safe.listener);
15               return safe;
16      }
17 }

不要在构造过程中使用this逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。

线程封闭

1.栈封闭。

即使用局部变量,对于基本类型的局部变量一定不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用,确保了基本类型的局部变量始终封闭在线程内。

在维持对象引用的栈封闭时,需要额外的操作防止引用的对象不被逸出。

2.ThreadLocal类

这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set方法为每个使用该变量的线程都存有一份独立的副本。

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

例子:

1 private static ThreadLocal<Connection> connectionHolder
2      = new ThreadLocal<Connection>(){
3           public Connection initialValue(){
4                 return DriverManager.getConnection(DB_URL);
5          }
6      };
7 public static Connection getConnection(){
8       return connectionHolder.get();
9 }

将JDBC的连接保存到ThreadLocal对象中,调用get方法使得每个线程都拥有属于自己的连接副本。

可以将ThreadLocal<T>看做Map<Thread,T>对象。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。

不变性

不可变的对象一定是线程安全的。

不可变对象一定满足以下条件:

1.对象创建后其状态(成员变量)就不能修改。

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

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

例子:

@Immutable
public final class ThreeStooges{
    private final Set<String> stooges = new HashSet<String>();
    public ThreeStooges(){
         stooges.add("Moe");
         stooges.add("Larry");
         stooges.add("Ted");
    }
    public boolean isStooge(String name){
          return stooges.contains(name);
    }
}

不可变性并不等于将对象所有的域都声明为final类型,即使对象中所有的域都是final类型,这个对象仍然也可以是可变的,因为在final域中可以保持对可变对象的引用。

对象创建出来后(通过构造方法)无论如何对此对象进行非暴力操作(不用反射),此对象的状态(成员变量的值)都不会发生变化,那么此对象就是不可变的,相应类就是不可变类,跟是否用 final 修饰没关系,final 修饰类是防止此类被继承。

final域

final域不能修改的(尽管如果final域指向的对象是不可变的,这个对象仍然可被修改),然而它在Java内存模式中还有着特殊语义。final域使得确保被始化安全性成为可能,初始化安全性让不可变性对象不需要同步就能自由地被访问和共享。

正如“将所有的域声明为私有的,除非它们需要更高的可见性”一样“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践。

示例:使用volatile发布不可变的对象

下面是一个不可变对象,进(构造时传进的参数)出(使用时)都对状态进行了拷贝。因为BigInteger是不可变的,所以直接使用了Arrays.copyOf来进行拷贝了,如果状态所指引的对象不是不可变对象时,就不能使用这项技术了,因为外界可以对这些状态所指引的对象进行修改,如果这样只能使用new或深度克隆技术来进行拷贝了。

@Immutable
class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    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);
    }
}

下面开始发布上面不可变对象,其中volatile起关键作用,如果没有它,即使OneValueCache是不可变类,其最新的状态也无法被其他线程可见。

@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为volatile,所以最新值立即能让其它线程可见
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }
}

不可变对象与初始化安全

Java内存模型为共享不可变对象提供了特殊的初始化安全性的保证,即对象在完全初始化之后才能被外界引用。

即使发布对象引用进没有使用同步,不可变对象仍然可以被安全地访问。为了获得这种初始化安全性的保证上,应该满足所有不可变性的条件:不可修改的状态、所有域都是final类型的以及正确的构造。

不可变对象呆以在没有额外同步的情况下,安全地用于任意线程;甚至发布它们时亦不需要同步。

可变对象与安全发布

如果一个对象不是不可变的,它就必须要被安全的发布,通常发布线程与消费线程都必须同步化。我们要确保消费线程能够看到处于发布当时的对象状态。

为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:

1、  通过静态初始化器初始化对象引用;

2、  将它的引用存储到volatile域或AtomicReference;

3、  将它的引用存储到正确创建的对象的Final域中;

4、  或者将它的引用存储到由锁正确保护的域中。

线程安全中的容器提供了线程安全保证,正是遵守了上述最后一条要求。

通常,以最简单和最安全的方式发布一个被静态创建的对象,就是使用静态初始化器:

public static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始阶段执行,由于JVM内在的同步,该机制确保了以这种方式初始化的对象可以被安全地发布。

对象可变性与安全发布

发布对象的必要条件依赖于对象的可变性:

1、  不可变对象可以通过任意机制发布;

2、  高效不可变对象(指对象本身是可变的,但只要发布后状态不再做修改)必须要安全发布;

3、  可变对象必须要安全发布,同时必须要线程安全或者是被锁保护;

 

时间: 2024-12-04 12:38:34

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

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

线程安全类:在线程安全类中封装了必要的同步机制,客户端无须进一步采取同步措施 示例:一个无状态的Servlet 1 @ThreadSafe 2 public class StatelessFactorizer implements Servlet{ 3 public void service(ServletRequest req,ServletResponse resp){ 4 BigInteger i = extractFromRequest(req); 5 BigInteger[] fact

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

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

《Java并发编程实战》第三章 对象的共享 读书笔记

一.可见性 什么是可见性? Java线程安全须要防止某个线程正在使用对象状态而还有一个线程在同一时候改动该状态,并且须要确保当一个线程改动了对象的状态后,其它线程能够看到发生的状态变化. 后者就是可见性的描写叙述即多线程能够实时获取其它线程改动后的状态. *** 待补充   两个工人同一时候记录生产产品总数问题 1. 失效数据 可见性出现故障就是其它线程没有获取到改动后的状态,更直观的描写叙述就是其它线程获取到的数据是失效数据. 2. 非原子64位操作 3. 加锁与可见性 比如在一个变量的读取与

《Java并发编程实战》第十六章 Java内存模型 读书笔记

Java内存模型是保障多线程安全的根基,这里仅仅是认识型的理解总结并未深入研究. 一.什么是内存模型,为什么需要它 Java内存模型(Java Memory Model)并发相关的安全发布,同步策略的规范.一致性等都来自于JMM. 1 平台的内存模型 在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏或栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证. JVM通过在适当的位置上插入内存栅栏来屏蔽在JVM与底层平台内存模型之间的

《Java并发编程实战》第十一章 性能与可伸缩性 读书笔记

造成开销的操作包括: 1. 线程之间的协调(例如:锁.触发信号以及内存同步等) 2. 增加的上下文切换 3. 线程的创建和销毁 4. 线程的调度 一.对性能的思考 1 性能与可伸缩性 运行速度涉及以下两个指标: 某个指定的任务单元需要"多快"才能处理完成.计算资源一定的情况下,能完成"多少"工作. 可伸缩性: 当增加计算资源时(例如:CPU.内存.存储容器或I/O带宽),程序的吞吐量或者处理能力能相应地增加. 2 评估各种性能权衡因素 避免不成熟的优化.首先使程序正

《Java并发编程实战》读书笔记

Subsections 线程安全(Thread safety) 锁(lock) 共享对象 对象组合 基础构建模块 任务执行 取消和关闭 线程池的使用 性能与可伸缩性 并发程序的测试 显示锁 原子变量和非阻塞同步机制 一.线程安全(Thread safety) 无论何时,只要多于一个线程访问给定的状态变量.而且其中某个线程会写入该变量,此时必须使用同步来协助线程对该变量的访问. 线程安全是指多个线程在访问一个类时,如果不需要额外的同步,这个类的行为仍然是正确的. 线程安全的实例: (1).一个无状

《Java并发编程实战》第二章 线程安全性 读书笔记

一.什么是线程安全性 编写线程安全的代码 核心在于要对状态访问操作进行管理. 共享,可变的状态的访问 - 前者表示多个线程访问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与其规范完全一致. 多个线程同时操作共享的变量,造成线程安全性问题. * 编写线程安全性代码的三种方法: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 Java同步机制工具: synchronized volatile类型变量 显示锁(Explicit Lock

《Java并发编程实战》第八章 线程池的使用 读书笔记

一.在任务与执行策略之间的隐性解耦 有些类型的任务需要明确地指定执行策略,包括: . 依赖性任务.依赖关系对执行策略造成约束,需要注意活跃性问题.要求线程池足够大,确保任务都能放入. . 使用线程封闭机制的任务.需要串行执行. . 对响应时间敏感的任务. . 使用ThreadLocal的任务. 1. 线程饥饿死锁 线程池中如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,这种现象称为线程饥饿死锁. 2. 运行时间较长的任务 Java提供了限时版本与无限时版本.例如Thread

《Java并发编程实战》第十五章 原子变量与非阻塞同步机制 读书笔记

一.锁的劣势 锁定后如果未释放,再次请求锁时会造成阻塞,多线程调度通常遇到阻塞会进行上下文切换,造成更多的开销. 在挂起与恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断. 锁可能导致优先级反转,即使较高优先级的线程可以抢先执行,但仍然需要等待锁被释放,从而导致它的优先级会降至低优先级线程的级别. 二.硬件对并发的支持 处理器填写了一些特殊指令,例如:比较并交换.关联加载/条件存储. 1 比较并交换 CAS的含义是:"我认为V的值应该为A,如果是,那么将V的值更新为B,否则不需要修