Java并发编程实战-第3章 对象的共享- 高飞网

第3章 对象的共享

2016-03-29 08:50:46.0

可见性

    通常我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

    如下面的代码,读线程读取ready变量,根据ready变量的状态,显示number的值;主线程负责赋值。于是出现了下面的情况,即使主线程改变了ready和number的值,读线程并不能看到。发生这种情况的原因是,JVM会对非同步情况的代码进行重排序,因此程序在多线程环境中,不会看我们看到的顺序去执行。

public class NoVisibility{
    private static boolean ready;
    private static int number;

    static class ReadThread implements Runnable{
        public void run(){
            while(!ready){
                System.out.println("check.");
                Thread.yield();
            }   
            System.out.println(number);
        }   
    }   

    public static void main(String[] args)throws Exception{
        new Thread(new ReadThread()).start();
        Thread.sleep(200);
        ready = true;
        Thread.sleep(200);
        number = 4;
    }   
}

     在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

    只要有数据在多个线程之间共享,就使用正确的同步。

    非原子的64位操作:Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long类型的数据时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能读取到某个值的高32位和另一个值的低32位。

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

    volatile变量:编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方。因此读取volatile类型的变量时总会返回最新的写入值。正确的用法:确保它们自身状态的可见性;确保它们所引用对象的状态可见性;以及标识一些重要的程序生命周期事件的发生(如初始化或关闭)。如一个读线程一个写线程,读线程读完时写一个操作标识标记读取完成,通知写线程也该停止了,此时应用volatile修饰。

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

当且仅当满足以下所有条件时,才应使用volatile变量:
1)对变量的写操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值
2)该变量不会与其他状态一起纳入不变性条件中
3)在访问变量时不需要加锁。

发布与逸出

    发布一个对象是指:使对象能够在当前作用域之外的代码中使用。发布内部状态可能会破坏封闭性(如将一个private的成员变量在一个public的方法中返回出去),并使得线程难以维持不变性(因为外部代码也可以改变这个变量的内容了)。

线程封闭

    当访问共享的可变数据时,通常需要使用同步 。一种使用同步的方式就是不共享数据,如果仅在单线程访问数据,就不需要同步。这种技术被称为线程封闭。主要有两种方式,栈封闭和ThreadLocal。

    栈封闭:在栈封闭中,只能通过局部变量才能访问对象。即在方法内部声明的变量,只在线程访问内部有访问权限(前提是该变量不会逸出方法)。

    ThreadLocal:这个类能使线程中的某个值与保存值的对象关联过眼起来。如果ServletAPI中的Request就是与线程绑定的 。

不变性

    不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:
    · 对象创建以后其状态就不能修改;
    · 对象的所有域都是final类型
    · 对象我是正确创建的(在对象的创建期间,this引用没有逸出)

    对于有多个可变属性的类,对多个未被同步的变量操作,会有线程安全问题,除了对其加锁同步以外,也可以通过将这些变量封装在一个容器中,然后将容器用volatile修饰。