您的位置:首页 > 编程语言 > Java开发

《java并发编程实战》 第三章 对象如何共享

2019-01-18 18:48 274 查看

《java并发编程实战笔记》

  • 不安全发布
  • 安全发布
  • 第三章 对象的共享

    从第三章开始,(这本书就开始变态起来了),如何共享和发布对象,从而使它们能安全的被多个线程同时访问。

    可见性

    同步代码块和同步方法可以确保以原子方式执行操作,一种常见的误解,认为关键字synchronized只能用于实现原子性或者确定“临界区”。不仅是只有原子性功能,还有内存可见性,某个线程正在使用对象状态,而另一个线程正在同时修改该状态,有了内存可见性,两个线程之间的对象状态能及时同步更新。

    public class NoVisibilty{
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread{
    public void run(){
    while(!ready)
    {
    Thread.yield();
    System.out.println(number);
    }
    }
    }
    public static void main(String[] args)
    {
    new ReaderThread().start();
    number = 42;
    ready = true;
    }
    }

    由于重排序现象,ReaderThread 线程很有可能输出的number为0。

    失效数据

    失效数据更好的解释是:过时的数据。以上面NoVisibilty类为例,main主线程更改number值后,更改后的值只是停留在主线程的高速缓存中,在number值从高速缓存区更新至内存,再更新到ReaderThread 线程这段时间中,number的值相对于ReaderThread 就是过时的、失效的。失效数据可能导致一些令人困惑的故障,例如意料之外的异常、被破坏的数据结构、不精确的计算、无线循环等。
    典型的失效数据出现的情况:

    @NotThreadSafe
    public class MutableInteger{
    private int value;
    public int get(){return value;}
    public void set(int value){this.value = value;}
    }

    由于get和set都是在没有同步的情况下访问value,某个线程调用了set,另外正在调用get的线程可能看到更新后的value,也可能看不到。
    正确的代码:

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

    最低安全性

    当线程在没有同步情况下读取变量,可能会得到一个失效值,但是至少也是某个线程设置的值,而不是随机的。这种安全性保证也称为最低安全性,即至少读这个过程总是原子的、可靠的。最低安全性适用于绝大多数的变量,但是对非volatile类型的64位数值变量(double和long)例外。对于非volatile类型的64位数值变量(double和long)变量JVM会将64位的读、写操作分解成两个32位操作。当读取一个非volatile类型的long变量时,如果对该变量的读和写操作在不同的线程中,那么很有可能会读取到某个值的高32位和低32位。除非用关键字volatile或者锁保护起来。

    volatile变量

    当把共享变量声明为volatile类型后,编译器与允许时都会注意到这个变量是共享的,因此不会讲该变量上的操作与其他内存变量操作仪器重排序。相比于加锁机制,volatile不会使用执行线程堵塞,因此volatile变量是一种比sychronized关键字更加轻量化的同步机制。
    volatile的典型用法:检查其他线程是否达到自己线程想达到的状态并标记。通常用作某个操作是否完成、终端、或者状态的标志。
    例如:

    volatile boolean wakeFlag;
    ...
    while(!wakeFlag)
    {.....}

    但是需要注意!!
    volatile变量只能保证可见性,并不能保证原子性。使用时不能使用count++这种操作,原子变量有提供自己的“读-改-写”操作方式。举个例子,A线程在执行count++。在读取count值时,B线程也执行了读取count操作,B线程也想执行count++操作 ,最后A B线程执行完后count也只加了1。当且仅当满足下面所有条件是,才可用volatile变量:

    1. 对变量读写操作不依赖变量本身当前值
    2. 该变量不会和其他状态变量一起纳入不变性条件中
    3. 在访问变量时不需要加锁

    对象的发布

    书上是这么定义:将一个指向该对象的引用保存至其他方法可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。
    例如:将新建的HashSet<>对象的引用保存至其他代码可以访问的地方。

    public static Set<Secret> knownSecrets;
    public void initialize(){
    knownSecrets = new HashSet<Secret>();
    }

    再例如:

    对象的逸出

    定义:某个不应该发布的对象被发布时,这种情况被称为逸出。
    例如:在非私有的方法返回私有变量的引用,内部可变状态逸出了

    class UnsafeStates{
    private String[] states = new String[] {
    "AK","AL",....
    };
    public String[] getStates() {return states;}
    }

    例如:隐式的适用this引用逸出

    public	class ThisEscape{
    public ThisEscape(EventSource source){
    source.registerListener(
    new EventListener(){
    public void onEvent(Event e){
    doSomething(e);
    }
    });
    }
    }

    内部类在编译完成后会隐含保存一个它外围类的引用,"ThisEscape.this”,然而构造函数还没完成,ThisEscape在执行构造函数,其本身对象还没构造完,this引用就立刻间接被传递到其他类的方法中,这当然是不应该的,所以是隐式的逸出。正确的做法是构造函数返回时,this引用才能从线程中逸出,才能在该线程中被其他类的方法使用。

    public class SafeListener{
    private final EventListener listener;
    private SafeListener(){
    listener  = new EventListener(){
    public void onEvent(Event e)
    {
    doSomething(e);
    }; //在构造函数结束之前,外部类this的引用并没有被其他类的方法引用,并没有被发布,所以没有逸出
    }
    public static SafeListener newInstance(EventSource source){
    SafeListener safe  = new SafeListener();
    souce registerListener(safe.listener);
    return safe;
    }
    }

    只有构造函数返回后,外部类的this才能被引用,此时外部类的对象已经构造完整。

    线程封闭

    当访问共享的可变数据一般都需要使用同步,避免使用同步的方式就是不共享数据,即在单线程中访问数据,这种技术称为线程封闭。线程安全性实现的最简单方式之一。

    1、Ad-hoc线程封闭:

    指维护线程封闭性的职责完全由程序实现承担,书中没有举例如何实现,一脸懵逼,鬼知道怎么怎么承担会非常脆弱。实际上没有一种特定的修饰符,可以将线程封闭到目标线程上。虽然有volatile变量上有一种特殊的线程封闭,但是是确保只有单个线程对共享的volatile变量进行写入操作,并不能封闭到指定的线程上。

    2、栈封闭:

    书中原话,只能通过局部变量才能访问对象。言下之意只用局部变量访问对象,而且这个对象也是单个线程中的局部对象。关键在于确保某个对象只能由单个线程访问。书中举了个找animals中可能凑一对的数目,懵逼了半天看的我。自己的理解写的程序

    public class StudentDao {
    public String getStudentIdByName(String name)
    {
    Student[] students = new Student[]{new Student("michael","211"),new Student("caroline","111")};
    String studentId = null;
    for(Student student:students)
    {
    if(name == null){break;}
    if(name == student.name){studentId = student.StudentId;}
    }
    return studentId;
    }
    }
    public class TestFengbiThread extends Thread{
    StudentDao studentDao = new StudentDao();
    public void run()
    {
    for(int i= 0; i<100;i++){
    System.out.println(studentDao.getStudentIdByName("michael"));
    }
    }
    public static void main(String[] args) {
    new testFengbiThread().start();
    new testFengbiThread().start();
    new testFengbiThread().start();
    new testFengbiThread().start();
    new testFengbiThread().start();
    
    }
    }

    getStudentIdByName()方法中实例化了Student对象数组,并将指向该对象数组引用放在students中,显然只有一个局部变量student使用了students。局部变量存在于执行线程栈,其他线程无法访问这个栈,从而确保线程安全。(每一个线程都有一个工作内存,相当于CPU高级缓冲区,工作内存中包括有栈,局部的基本类型变量是处于栈中,引用类型的引用处于栈中,而引用指向的对象处于堆中,之所以叫栈封闭,个人认为,你都用局部变量访问局部的对象,主要都在栈中,所以叫栈封闭???)。 上述中Student[]对象的引用放在student中,每个线程独享栈,因此多个线程无法同时操作同一个Student[]对象数组。

    多线程的数据共享机制 同一进程间的线程究竟共享哪些资源呢,而又各自独享哪些资源呢? 共享的资源有 a. 堆
    由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的) b.
    全局变量 它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的 c. 静态变量
    虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的 d.
    文件等公用资源 这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。
    独享的资源有 a. 栈 栈是独享的 b. 寄存器
    这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC

    3、ThreadLocal类

    官方给的ThreadLocal类的定义:ThreadLocal类为线程提供了局部变量 thread-local variables,这些变量不同于在每个线程普通的通过set get方法就能访问到的变量,thread-local variables的副本初始化过程独立,并且通常来说这些thread-local variables一般都是想关联一个线程的状态,例如用户ID、事务ID,并且通常都是定义成private static的。以下是从官方提供的例子自己测试,为每个线程生成唯一的标识,

    public class ThreadLocal<T>extends Object
    /*This class provides thread-local variables.
    These variables differ from their normal counterparts in that each thread
    that accesses one (via its get or set method) has its own,
    independently initialized copy of the variable.
    ThreadLocal instances are typically private static fields in classes
    that wish to associate state with a thread (e.g., a user ID or Transaction ID).
    For example, the class below generates unique identifiers local to each thread.
    A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.
    */
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);
    
    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
    new ThreadLocal<Integer>() {
    @Override protected Integer initialValue() {
    return nextId.getAndIncrement();
    }
    };
    
    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
    return threadId.get();
    }
    }
    public class TestThreadIdThread extends Thread{
    private String threadName;
    public TestThreadIdThread(String threadName) {
    super();
    this.threadName = threadName;
    }
    @Override
    public void run() {
    for(int i = 0; i<100;i++)
    {
    System.out.println(threadName+"id is : "+ThreadId.get());
    }
    }
    public static void main(String[] args) {
    new TestThreadIdThread("thread1").start();
    new TestThreadIdThread("thread2").start();
    new TestThreadIdThread("thread3").start();
    new TestThreadIdThread("thread4").start();
    }
    }

    最终输出结果,thread1不一定对应ThreadId就是1,但是每个线程的ThreadId始终一致。
    ThreadLocal类提供了get和set方法,这些方法能为每个使用该变量的线程都存有一份独立的副本。
    例如ThreadId类的threadId 变量为每个线程都提供了独立并且唯一的值,原因在于每个线程第一次调用ThreadLocal类变量的get方法时,会调用initialValue来获取初始值,而我们要做的是在新建ThreadLocal变量时重写initialValue方法,并且initialValue方法返回值就是ThreadLocal变量绑定该线程的值。ThreadLocal变量基本上是定义为private static ,定义成private static final更好

    书上的例子:JDBC连接的原理,每个线程都有属于自己的连接

    private static  ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>{
    public Connection initialValue(){
    return DriverManager.getConnection(DB_URL);
    }
    public static Connection getConnection(){
    return connectionHolder.get();
    }
    }

    书上的例子:J2EE容器需要一个事务上下文(Transaction Context)与某个线程关联起来,通过事务上下文保存在静态的ThreadLocal对象中,当框架代码需要判断当前运行的是哪个事务,只需从ThreadLocal对象中读取事务上下文。这种机制非常巧妙的避免在调用每个方法时都要传递上下文信息。
    具体为何每次执行ThreadLocal变量、对象的get方法就能获得与该线程绑定的ThreadLocal变量、对象值?
    get()方法返回当前线程的线程局部变量的副本,如果线程局部变量没有值会先执行initialValue方法进行初始化。

    /**
    * Returns the value in the current thread's copy of this
    * thread-local variable.  If the variable has no value for the
    * current thread, it is first initialized to the value returned
    * by an invocation of the {@link #initialValue} method.
    *
    * @return the current thread's value of this thread-local
    */
    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
    }
    }
    return setInitialValue();
    }

    通过ThreadLocal的getMap(thread);方法可以获得与相应线程的Map<Thread,T>对象,其中保存了特定于该线程的值。

    4、不可变对象(final、volatile)

    满足同步需求的另一种方法就是使用不可变对象,如果一个对象的状态始终不改变,多个线程访问某个不可变对象,就不存在什么同步问题。不可变对象一定是线程安全的。
    满足一下所有条件后,对象才是不可变的:
    a、对象的所有域都是final类型,(书上原话,从技术上看,不可变对象并不需要将所有的域都声明为final类型,例如String类)
    b、对象是正确创建的,在创建期间,this引用没有逸出
    不可变对象定义:
      如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
      还有其他版本定义:
     a、对象创建完之后其状态就不能修改
     b、对象的所有与都是 final 类型
     c、对象时正确创建的(创建期间没有 this 的逸出) 
     例如:ThreeStooges 在不可变对象的内部仍可以使用可变对象来管理它们的状态,如 ThreeStooges 所示。尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计中可以看到,在Set对象构造完成后无法对其进行修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域来访问。最后一个要求是“正确地构造对象”,这个要求很容易满足,因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。

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

    引用两段话:

    注:个人理解为,final 字段一旦被初始化完成,并且构造器没有把 this 引用传递出去,那么在其他线程中就能看到 final
    字段的值(域内变量可见性,和 volatile 类似),而且其外部可见状态永远也不会改变。它所带来的安全性是最简单最纯粹的。

    注:即使对象是可变的,通过将对象的某些域声明为final类型,仍然可以简化对状态的判断,因此限制对象的可变性也就相当于限制了该对象可能的状态集合。仅包含一个或两个可变状态的“基本不可变”对象仍然比包含多个可变状态的对象简单。通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。

    final使用
    final声明变量:final声明变量可以保证在构造器函数返回之前,这个变量的值已经被设置。详细可以看final声明的重排序规则。分为三种情况:
    final****声明基本数据类型变量
      该变量只能被赋值一次,赋值后值不再改变。
    final声明引用数据类型变量:
      final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但是这个对象的内容(对象的非final成员变量的值可以改变)完全可以发生改变(比如final int[] intArray;,intArray不允许再引用其他对象,但是intArray内的int值却可以被修改)。
    final声明方法参数或者局部变量:
      用来保证该参数或者局部变量在这个函数内部不允许被修改。
      final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。另外,final变量定义的时候,可以先声明,而不给初值,这种变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,final空白在final关键字的使用上提供了更大的灵活性。比如:

    public class FinalClass{
    private final int a;
    public FinalClass(int x) {  //final空白,必须在初始化对象的时候赋初值
    a = x;
    }
    }

    final声明方法: final声明的方法不可以被重写,但可以被继承。final不能用于修饰构造方法。使用final方法的原因有二:
      第一、把方法锁定,防止任何继承类修改它的意义和实现。
      第二、高效。因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
    final声明类:
      final声明的类不可以被继承,final类中的方法默认是final的。但是成员变量却不一定是final的,必须额外给成员变量声明为final。注意:一个类不能同时被abstract和final声明。
      在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会被扩展,那么就设计为final类。比如Java中有许多类是final的,譬如String, Interger以及其他包装类。

    final域(变量)声明的重排序规则: final域的重排序规则,编译器和处理器要遵守两个重排序规则:
      禁止把final域的写重排序到构造函数之外(即必须先对final域赋值,然后才能引用包含final域的对象)。编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障,从而禁止处理器把final域的写重排序到构造函数之外。
      初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序(即必须先读包含final域的对象,然后才能读final域)。
      而普通域是可以被重排序到构造器之外的。重排序可能导致一个线程看到一个对象的时候,这个对象还没有初始化完毕(部分初始化或者完全没有经过初始化,即读取到对象为null)。

    public class FinalExample {
    int i;                            //普通变量
    final int j;                      //final变量
    static FinalExample obj;
    
    public void FinalExample () {     //构造函数
    i = 1;                        //写普通域
    j = 2;                        //写final域
    }
    
    public static void writer () {    //写线程A执行
    obj = new FinalExample ();
    }
    
    public static void reader () {       //读线程B执行
    FinalExample object = obj;       //读对象引用
    int a = object.i;                //读普通域
    int b = object.j;                //读final域
    }
    }

    主函数中读普通域,由于重排序可能导致一个线程看到FinalExample 的时候,FinalExample 还没有初始化完毕(部分初始化或者完全没有经过初始化,即读取到普通域为null)。但是final域中的值是绝对会正确的,final域的重排序规则“限定”在了构造函数之内。
      final关键字相关知识 有待补充。。。。。

    如何理解String类的不可变性:

    String类 Integer类是一个final类,不可继承,其主要成员value是final类型的char[],value这个引用在栈中不可变,(虽然value[]数值中的值在堆中可变),以及其他的成员变量基本上都是private final类型,并且没有提供set方法,言下之意,新建一个String类的对象,其成员变量被初始化后就不能改变,可以认为内部状态不变。

    public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    private static final ObjectStreamField[] serialPersistentFields;
    private int hash;
    ......
    }
    String string1 = "string1";
    String string2 = "string2";
    string1 = string2;
    System.out.println(string1);

    不要想当然的看看上面例子说,string1的内容发生了改变,"string1"和"string2"两个对象 是存放在堆中,而对象的引用变量string1和string2在内存中,string1 = string2;过程只是string1指向了不同的对象。
    当然也不是说一定不能变,

    String s="123";
    Field valueArray=String.class.getDeclaredField("value");//通过反射方式
    valueArray.setAccessible(true);
    char[] array=(char[]) valueArray.get(s);
    array[0]='2';
    System.out.println(s);//223  摘自知乎

    使用Volatile类型来发布不可变对象
      第二章中的例子,尽管用了原子引用,但是只有lastFactors中缓存的因数之积始终等于lastNumber缓存数值,才能保证线程安全。否则,多个线程仍存在竞态条件,不安全。我们无法以原子方式同时读取或者更新这两个相关的值,同样我们用volatile类型变量保存这些值也不是线程安全的。此时,然而,在某些情况下,不可变对象能提供一种弱形式的原子性。

    @NotThreadSafe
    public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber
    = new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors
    = new AtomicReference<BigInteger[]>();
    public void service(ServletRequest req, ServletResponse resp) {
    BigInteger i = extractFromRequest(req);
    if (i.equals(lastNumber.get()))
    encodeIntoResponse(resp, lastFactors.get());
    else {
    BigInteger[] factors = factor(i);
    lastNumber.set(i);
    lastFactors.set(factors);
    encodeIntoResponse(resp, factors);
    }
    }
    }

    书上的例子:
      因式分解Servlet 执行两个原子操作包括更新缓存的结果、以及通过判断第一次缓存的数值是否等于请求的数值来决定是否直接读取缓存中的因数。每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。该oneValueCache为不可变对象,在多个线程访问service方法时,只有第一次会更改cache 引用的对象,service方法中i、cache均是service方法中的局部变量,线程安全。

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

    不安全发布

    以上线程封闭都是讨论如何确保对象不被发布,那如何安全的在多个线程中共享对象呢?以下holder对象发布看似没有没有问题,但是holder对象不一定微一个不可变对象,由于存在可见性问题,其他线程看到的holder对象将处于不一致的状态。书上该例子说holder未正确发布,确实holder对象不是一个不可变对象,在多个线程共享该对象时没有采用同步、锁的方式保证对holder对象的操作的原子性、可见性。

    public Holder holder;
    public void initialize(){
    holder = new Holder(42);
    }
    public class Holder {
    private int n;
    public Holder(int n) {this.n = n;}
    public void assertSanity(){
    if(n != n){
    throw new AssertionError("this statement is false.....");
    }
    }
    public int getN() {
    return n;
    }
    }

    但是书上的说采用该种发布方式,一个线程在调用assertSanity()会可能抛出AssertionError。个人感觉书中提供的例子不够详细,意思是那么个意思。
      我重新改了改代码测试了下:

    public class Holder {
    private int n;
    public Holder(int n) {this.n = n;}
    public void assertSanity(){
    if(n != n){
    throw new AssertionError("this statement is false.....");
    }
    }
    public int getN() {
    return n;
    }
    public void  setN(int n) {
    this.n = n;
    }
    }

    测试线程:

    public class HolderFabuThread1 implements Runnable{
    public Holder holder = new Holder(0) ;
    private int i = 0;
    public void initialize1()
    {
    holder.setN(1);
    }
    @Override
    public void run() {
    // TODO Auto-generated method stub
    for(; i<40;i++)
    {
    System.out.println(Thread.currentThread().getName()+" i is "+ i +" holder n  = "+ holder.getN());
    if(i == 30){initialize1();}
    if(i >30&&holder.getN() == 0){System.out.println("error ......."+Thread.currentThread().getName()+" i is "+ i +" holder n  = "+ holder.getN());}
    holder.assertSanity();
    }
    }
    public static void main(String[] args) {
    HolderFabuThread1 holderFabuThread1 = new HolderFabuThread1();
    new Thread(holderFabuThread1, "线程1").start();
    new Thread(holderFabuThread1, "线程2").start();
    new Thread(holderFabuThread1, "线程3").start();
    new Thread(holderFabuThread1, "线程4").start();
    new Thread(holderFabuThread1, "线程5").start();
    new Thread(holderFabuThread1, "线程6").start();
    new Thread(holderFabuThread1, "线程7").start();
    new Thread(holderFabuThread1, "线程8").start();
    new Thread(holderFabuThread1, "线程9").start();
    new Thread(holderFabuThread1, "线程10").start();
    new Thread(holderFabuThread1, "线程11").start();
    ....
    }
    }

    想测试一下holder的n更改时可见性问题,开到100个线程也没试出来,不知道是否因为功力有限测试方法不对,还是可见性问题出现的要求比较高,反正对i++的原子性、可见性问题是极容易出现的。如果holder类是不可变对象(状态不可修改、所有的域都是final、正确的构造过程),那么及时holder没有正确的发布,也不会出现问题。

    安全发布

    要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确构造的对象可以通过以下的方式安全地发布:
    a、在静态初始化函数中初始化一个对象引用
    b、将对象的引用保存到volatile类型的域或者AtomaticReferance对象中
    c、将对象的引用保存至某个正确构造对象的final类型域中
    d、将对象的引用保存到一个由锁保护的域中

    java提供了一些线程安全库中的容器类,提供了安全发布的保证。例如:Hashtable、ConcurrentMap、synchronizedMap、Vector、BlockingQueue等。
      最简单的线程安全的对象发布,采用的是通过静态方法创建,类似于工程方法:
      public static Holder hold=new Holder(42);
       对于安全发布对象的总结:
      1、线程封闭,对象范围是只在1个thread范围中,采用线程封闭技术,那么不需要同步机制,因为该对象是thread独有的
      2、只读共享,如果对象是只读的,那么也不需要同步机制,没有任何修改操作。
      3、线程安全类,如果对象是在Thread-safe结构中进行共享,如Hashtable等,那么该结构已经提供了同步机制,可以放心使用
      4、其它。则该对象如果存在读写操作,需要相应的进行锁机制、同步机制,公用同样的锁来保证数据的完整一致
    高端用法:多个线程之间将一个Date对象作为不可变对象来使用,那么在多个线程共享Date对象时,可以省去锁的使用,假设需要维护一个Map对象,保存每个用户的最近登录时间:
    public Map<String,Date> lastlogin = Collections.synchronizedMap(new HashMap<String,Date>());

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: