《Java 并发编程实战》对象的共享

2019-02-18  本文已影响6人  Theodore的技术站

目录

1.可见性
2.发布与溢出
3.线程封闭
4.不变性
5.安全发布

1.可见性

package com.javaConcurrency;

public class codelist3_1__3 {
    //3-1 没有同步机制 不可见
    public class NoVisibility{
        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;
        }
    }

    //3-2 失效数据
    @NotThreadSafe
    public class MutableInteger{
        private int value;

        public int get() {return value;}
        public void set(int value) {this.value = value;}
    }

    //3-3 安全
    @ThreadSafe
    public class SynchronizedInteger{
        @GuardedBy("this") private int value;
        public synchronized int get(){return value;}
        public synchronized void set(int value){this.value = value;}
    }
}

非原子 64 位操作,JVM 允许将 64 位读写操作分为两个 32 位操作,加上 volatile 就行了

2.发布和溢出

如果在对象构造完成之前就发布对象,就会破坏线程安全,称为溢出。

package com.javaConcurrency;

import java.util.EventListener;

public class codelist3_5__7 {
    //1.发布 将对象的引用保存到一个公有静态变量中
    //当发布 knownSecrets  集合的时候 Secret 也被发布了,任何代码都能遍历这个集合。
    public static Set<Secret> knownSecrets;

    public void initialize(){
        knownSecrets = new HashSet<Secret>();
    }
    //----------
    //2.发布本应为私有的数组 任何调用者都能修改
    //当发布一个对象的时候,该对象的非私有域中引用的所有对象都会被发布
    class UnsafeStates{
        private String[] states = new String[]{
                "ak","al"...
        }
        public String[] getStates (){return states;}
    }
    //--------
    //3.隐式的使this引用溢出
    public class ThisEscape{
        public ThisEscape(EventSource source){
            source.registerListener(new EventListener(){
                public void onEvent(Event e){
                    doSomething(e);
                }
            });
        }
    }
}

1.发布
将对象的引用存储到公共静态域。
在非私有方法中返回引用。
2.溢出
一个对象在尚未准备好就将它发布出去——溢出。
“内部类发布也会引发溢出”,因为只有当对象通过构造函数返回之后,才处于稳定状态。这种发布会导致this溢出。
“即使在构造函数的最后一行发布也会有该问题”,指令重排序可能会引发一些奇怪的问题。而且该引用已经不是null了,但是内容还没有初始化完毕也有可能。
“不要让this在构造期溢出!”

安全的对象构造过程

使用工厂方法来防止 this 引用在构造过程中溢出

import java.util.EventListener;

/*
*使用工厂方法来防止 this 引用在构造过程中溢出
* */
public class codelist3_8 {
    public class SafeListener{
        private final EventListener listener;

        private SafeListener(){
            listener = new EventListener() {
                public void onEvent(Event e){
                    doSomething(e);
                }
            };
        }

        public static SafeListener newInstance(EventSource source){
            SafeListener safe = new SafeListener();
            source.registerListener(safe.listener);
            return safe;
        }
    }
}

3.线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免同步的方式就是不共享数据。

仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭(Thread Confinement)。

典型应用:①Swing的可视化组件和数据模型对象都不是线程安全的,Swing通过将它们封闭到Swing的实际分发线程中来实现线程安全;②JDBC的Connection对象。还有局部变量和 ThreadLocal。

线程封闭技术:

①Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担。

在volatile变量上存在一个特殊的线程封闭:能确保只有单个线程对共享的volatile变量执行写入操作(其他线程有读取volatile变量),那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作,而其他读取volatile变量的线程也能看到最新的值。

②栈封闭:在栈封闭中,只能通过局部变量才能访问对象。
例子:

package com.javaConcurrency;

import java.util.SortedSet;
import java.util.TreeSet;

/*
* 基本类型的局部变量与引用变量的线程封闭性
* */

public class codelist3_9 {
    public int loadTheArk(Collection<Animal> candidates){
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;

        //animals 被封闭在方法中,不要使它们溢出
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals){
            if (candidate == null || !candidate,isPotentialMate(a)){
                candidate = a;
            }else{
                ark.load(new AnimalPair(candidate,a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }
}

③ThreadLocal类:这是线程封闭的更规范方法,这个类能使线程中的某个值与保存值的对象关联起来。

提供get()和set()方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。怎么理解呢?还是JDBC的Connection对象,防止共享,所以通常将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。

package com.javaConcurrency;

import java.sql.Connection;
import java.sql.DriverManager;
/*
* 使用 ThreadLocal 来维持线程封闭性
* */

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

    public static Connection getConnection(){
        return connectionHolder.get();
    }
}

4.不变性:

满足同步需求的另一种方法是使用 不可变对象(Immutable Object)。
  不可变对象一定是线程安全的。

当满足以下这些条件的时候,对象才是不可变的:
1.对象创建以后其状态就不能被修改;
2.对象的所有域都是 final 类型;
3.对象是正确创建的(在对象的创建期间,this引用没有逸出)。

两种很好的编程习惯:
1.除非使用更高的可见性,否则应将所有的域都声明为私有的;
2.除非需要某个域是可变的,否则都应该声明为final域;

//实例1:
package com.javaConcurrency;

import jdk.nashorn.internal.ir.annotations.Immutable;

import java.util.HashSet;
import java.util.Set;

public class codelist3_11 {
    //在可变对象的基础上构建不可变类
    @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);
        }
    }
}
//----------
//实例2:对数值及其因数分解结果进行缓存的不可变容器类
package com.javaConcurrency;

import jdk.nashorn.internal.ir.annotations.Immutable;

import java.math.BigInteger;
import java.util.Arrays;

public class codelist3_12 {
    @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);
        }
    }
}
//---------
//实例3:使用指向不可变容器对象的 volatile 类型引用以缓存最新的结果
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet{
    @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);
        }
    }
}

5.安全发布

因为可变对象必须以安全的方式发布,这就意味着发布和使用该对象的线程时都必须使用同步。

要安全的发布一个对象,对象的引用以及对象的状态必须同时对其它线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

在静态初始化函数中初始化一个对象引用。
    将对象的引用保存到volatile类型的于或者AtomicReferance对象中。
    将对象的引用保存到某个正确构造对象的final类型域中。
    将对象的引用保存到一个由锁保护的域中。

线程安全容器:

通过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap 中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。

通过将某个元素放入 Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet 中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。

通过将某个元素放入 BlockingQueue 或者 ConcurrentLinkedQueue 中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

安全地共享对象:

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改;

只读共享。在没有额外的同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。

线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的共有接口来进行访问而不需要进一步的同步。

保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

上一篇下一篇

猜你喜欢

热点阅读